commit 8d805ca8dc2845106d4b0514a59c5c4295b8665a Author: Adrian Stephens Date: Thu Apr 2 09:44:38 2026 +0100 moving from svn diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90ec22b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.svn diff --git a/code/calendar-plugin.php b/code/calendar-plugin.php new file mode 100644 index 0000000..8abffcf --- /dev/null +++ b/code/calendar-plugin.php @@ -0,0 +1,22 @@ +events->listEvents() as $event) { + $resource = $this->resourceForEvent($event); + $items[] = [ + 'resource' => $resource, + 'href' => '/caldav/calendars/public/' . $resource, + 'uid' => (string) ($event['uid'] ?? ''), + 'etag' => (string) ($event['etag'] ?? ''), + 'updated_at' => (string) ($event['updated_at'] ?? ''), + 'sync_version' => (int) ($event['sync_version'] ?? 1), + ]; + } + return $items; + } + + public function getObject(string $resource): ?array + { + $event = $this->findEventByResource($resource); + if (!$event) { + return null; + } + $ics = $this->ics->buildCalendar( + [$event], + fn(int $eventId): array => $this->events->getDeletedOccurrenceKeys($eventId) + ); + + return [ + 'resource' => $resource, + 'etag' => (string) ($event['etag'] ?? ''), + 'event' => $event, + 'ics' => $ics, + ]; + } + + public function putObject(string $resource, string $icsPayload, ?string $ifMatch = null, ?string $ifNoneMatch = null, ?int $userId = null): array + { + $payload = $this->ics->parseEventFromIcs($icsPayload); + if ($payload === null) { + return ['error' => ['code' => 'invalid_ics', 'message' => 'invalid iCalendar payload', 'status' => 422]]; + } + + $existing = $this->events->getEventByResource($resource); + if (!$existing) { + $existing = $this->findEventByResource($resource); + } + if ($ifNoneMatch === '*' && $existing) { + return ['error' => ['code' => 'precondition_failed', 'message' => 'resource already exists', 'status' => 412]]; + } + if ($ifMatch !== null) { + if (!$existing) { + return ['error' => ['code' => 'precondition_failed', 'message' => 'resource does not exist', 'status' => 412]]; + } + if ((string) ($existing['etag'] ?? '') !== trim($ifMatch)) { + return ['error' => ['code' => 'precondition_failed', 'message' => 'etag mismatch', 'status' => 412]]; + } + } + + $payload['caldav_resource'] = $resource; + if ($userId !== null) { + $payload['last_modified_by_user_id'] = $userId; + } + + $deleted = (array) ($payload['deleted_occurrence_keys'] ?? []); + unset($payload['deleted_occurrence_keys']); + + if ($existing) { + $nextSyncVersion = ((int) ($existing['sync_version'] ?? 1)) + 1; + $payload['sync_version'] = $nextSyncVersion; + $payload['etag'] = $this->etagFor((string) ($payload['uid'] ?? $existing['uid'] ?? ''), $nextSyncVersion); + $event = $this->events->updateEvent((int) $existing['id'], $payload); + if (!$event) { + return ['error' => ['code' => 'update_failed', 'message' => 'failed to update object', 'status' => 500]]; + } + $this->events->syncDeletedOccurrenceKeys((int) $event['id'], $deleted, true); + $event = $this->events->getEvent((int) $event['id']) ?? $event; + + return ['status' => 204, 'created' => false, 'event' => $event]; + } + + $payload['sync_version'] = 1; + $payload['etag'] = $this->etagFor((string) ($payload['uid'] ?? ''), 1); + $event = $this->events->createEvent($payload); + $this->events->syncDeletedOccurrenceKeys((int) $event['id'], $deleted, true); + $event = $this->events->getEvent((int) $event['id']) ?? $event; + + return ['status' => 201, 'created' => true, 'event' => $event]; + } + + public function deleteObject(string $resource): array + { + $existing = $this->findEventByResource($resource); + if (!$existing) { + return ['error' => ['code' => 'not_found', 'message' => 'resource not found', 'status' => 404]]; + } + + $ok = $this->events->deleteEvent((int) $existing['id']); + if (!$ok) { + return ['error' => ['code' => 'delete_failed', 'message' => 'failed to delete resource', 'status' => 500]]; + } + + return ['status' => 204, 'deleted' => true]; + } + + public function multiget(array $resources): array + { + $out = []; + foreach ($resources as $resource) { + $resource = basename((string) $resource); + if ($resource === '') { + continue; + } + $object = $this->getObject($resource); + if ($object === null) { + $out[] = ['resource' => $resource, 'status' => 404]; + continue; + } + $out[] = [ + 'resource' => $resource, + 'status' => 200, + 'etag' => $object['etag'], + 'ics' => $object['ics'], + ]; + } + return $out; + } + + public function resourceForEvent(array $event): string + { + $resource = trim((string) ($event['caldav_resource'] ?? '')); + if ($resource !== '') { + return $resource; + } + return (string) ($event['uid'] ?? 'event-' . (string) ($event['id'] ?? 0)) . '.ics'; + } + + private function etagFor(string $uid, int $version): string + { + return '"' . substr(sha1($uid . ':' . $version . ':' . gmdate('c')), 0, 16) . '"'; + } + + private function findEventByResource(string $resource): ?array + { + foreach ($this->events->listEvents() as $event) { + if ($this->resourceForEvent($event) === $resource) { + return $event; + } + } + return null; + } +} diff --git a/code/src/Domain/EventService.php b/code/src/Domain/EventService.php new file mode 100644 index 0000000..7e99a9b --- /dev/null +++ b/code/src/Domain/EventService.php @@ -0,0 +1,709 @@ +getPrefix(); + $stem = trim($tableStem, '_'); + $this->eventsTable = $prefix . $stem . '_events'; + $this->exceptionsTable = $prefix . $stem . '_recurrence_exceptions'; + } + + public function listEvents(): array + { + $rows = $this->db->getResults("SELECT * FROM {$this->eventsTable} ORDER BY id ASC"); + return array_map([$this, 'normalizeRow'], $rows); + } + + public function getEvent(int $id): ?array + { + $sql = $this->db->prepare("SELECT * FROM {$this->eventsTable} WHERE id = %d", $id); + $row = $this->db->getRow($sql); + return $row ? $this->normalizeRow($row) : null; + } + + public function createEvent(array $payload): array + { + $now = gmdate('c'); + $uid = (string) ($payload['uid'] ?? bin2hex(random_bytes(10)) . '@calendar-plugin'); + $resource = $this->resourceFromUid($uid); + $title = trim((string) ($payload['title'] ?? 'Untitled')); + $startRaw = (string) ($payload['start_datetime'] ?? ''); + $endRaw = (string) ($payload['end_datetime'] ?? ''); + $start = $this->toLondonDateTimeString($startRaw); + $end = $this->toLondonDateTimeString($endRaw); + if ($start === '' || $end === '') { + throw new \InvalidArgumentException('start_datetime and end_datetime are required'); + } + if (new DateTimeImmutable($end) < new DateTimeImmutable($start)) { + throw new \InvalidArgumentException('end_datetime must be at or after start_datetime'); + } + $repeatType = (string) ($payload['repeat_type'] ?? 'none'); + $repeatInterval = max(1, (int) ($payload['repeat_interval'] ?? 1)); + $repeatNthMode = (string) ($payload['repeat_nth_mode'] ?? ''); + $repeatNthDay = array_key_exists('repeat_nth_day', $payload) && $payload['repeat_nth_day'] !== null && $payload['repeat_nth_day'] !== '' + ? (int) $payload['repeat_nth_day'] + : null; + $repeatNthPos = array_key_exists('repeat_nth_pos', $payload) && $payload['repeat_nth_pos'] !== null && $payload['repeat_nth_pos'] !== '' + ? (int) $payload['repeat_nth_pos'] + : null; + $repeatNthWeekday = array_key_exists('repeat_nth_weekday', $payload) && $payload['repeat_nth_weekday'] !== null && $payload['repeat_nth_weekday'] !== '' + ? (int) $payload['repeat_nth_weekday'] + : null; + [$start, $end] = $this->normalizeMonthlyAnchor( + $start, + $end, + $repeatType, + $repeatInterval, + $repeatNthMode, + $repeatNthDay, + $repeatNthPos, + $repeatNthWeekday + ); + + $data = [ + 'uid' => $uid, + 'title' => $title, + 'description' => (string) ($payload['description'] ?? ''), + 'location' => (string) ($payload['location'] ?? ''), + 'category' => (string) ($payload['category'] ?? ''), + 'all_day_event' => !empty($payload['all_day_event']) ? 1 : 0, + 'start_datetime' => $start, + 'end_datetime' => $end, + 'repeat_type' => $repeatType, + 'repeat_interval' => $repeatInterval, + 'repeat_nth_mode' => $repeatNthMode, + 'repeat_nth_day' => $repeatNthDay, + 'repeat_nth_pos' => $repeatNthPos, + 'repeat_nth_weekday' => $repeatNthWeekday, + 'repeat_range_mode' => $this->canonicalRangeMode((string) ($payload['repeat_range_mode'] ?? 'none')), + 'repeat_count' => isset($payload['repeat_count']) ? (int) $payload['repeat_count'] : null, + 'repeat_until' => !empty($payload['repeat_until']) ? (string) $payload['repeat_until'] : null, + 'timezone' => (string) ($payload['timezone'] ?? 'Europe/London'), + 'caldav_resource' => !empty($payload['caldav_resource']) ? (string) $payload['caldav_resource'] : $resource, + 'etag' => (string) ($payload['etag'] ?? $this->makeEtag($uid, 1, $now)), + 'sync_version' => (int) ($payload['sync_version'] ?? 1), + 'last_modified_by_user_id' => isset($payload['last_modified_by_user_id']) ? (int) $payload['last_modified_by_user_id'] : null, + 'created_at' => $now, + 'updated_at' => $now, + ]; + + $inserted = $this->db->insert($this->eventsTable, $data); + if ($inserted === false) { + throw new \RuntimeException('failed to create event'); + } + return (array) $this->getEvent($this->db->insertId()); + } + + public function updateEvent(int $id, array $payload): ?array + { + $existing = $this->getEvent($id); + if (!$existing) { + return null; + } + $now = gmdate('c'); + $currentResource = trim((string) ($existing['caldav_resource'] ?? '')); + $fallbackResource = $this->resourceFromUid((string) ($existing['uid'] ?? '')); + $start = array_key_exists('start_datetime', $payload) + ? $this->toLondonDateTimeString((string) $payload['start_datetime']) + : (string) $existing['start_datetime']; + $end = array_key_exists('end_datetime', $payload) + ? $this->toLondonDateTimeString((string) $payload['end_datetime']) + : (string) $existing['end_datetime']; + if ($start !== '' && $end !== '' && new DateTimeImmutable($end) < new DateTimeImmutable($start)) { + throw new \InvalidArgumentException('end_datetime must be at or after start_datetime'); + } + $repeatType = (string) ($payload['repeat_type'] ?? $existing['repeat_type']); + $repeatInterval = max(1, (int) ($payload['repeat_interval'] ?? $existing['repeat_interval'])); + $repeatNthMode = (string) ($payload['repeat_nth_mode'] ?? ($existing['repeat_nth_mode'] ?? '')); + $repeatNthDay = array_key_exists('repeat_nth_day', $payload) + ? ($payload['repeat_nth_day'] === null || $payload['repeat_nth_day'] === '' ? null : (int) $payload['repeat_nth_day']) + : ($existing['repeat_nth_day'] ?? null); + $repeatNthPos = array_key_exists('repeat_nth_pos', $payload) + ? ($payload['repeat_nth_pos'] === null || $payload['repeat_nth_pos'] === '' ? null : (int) $payload['repeat_nth_pos']) + : ($existing['repeat_nth_pos'] ?? null); + $repeatNthWeekday = array_key_exists('repeat_nth_weekday', $payload) + ? ($payload['repeat_nth_weekday'] === null || $payload['repeat_nth_weekday'] === '' ? null : (int) $payload['repeat_nth_weekday']) + : ($existing['repeat_nth_weekday'] ?? null); + [$start, $end] = $this->normalizeMonthlyAnchor( + $start, + $end, + $repeatType, + $repeatInterval, + $repeatNthMode, + $repeatNthDay, + $repeatNthPos, + $repeatNthWeekday + ); + $data = [ + 'title' => trim((string) ($payload['title'] ?? $existing['title'])), + 'description' => (string) ($payload['description'] ?? $existing['description']), + 'location' => (string) ($payload['location'] ?? $existing['location']), + 'category' => (string) ($payload['category'] ?? $existing['category']), + 'all_day_event' => array_key_exists('all_day_event', $payload) + ? (!empty($payload['all_day_event']) ? 1 : 0) + : ((bool) $existing['all_day_event'] ? 1 : 0), + 'start_datetime' => $start, + 'end_datetime' => $end, + 'repeat_type' => $repeatType, + 'repeat_interval' => $repeatInterval, + 'repeat_nth_mode' => $repeatNthMode, + 'repeat_nth_day' => $repeatNthDay, + 'repeat_nth_pos' => $repeatNthPos, + 'repeat_nth_weekday' => $repeatNthWeekday, + 'repeat_range_mode' => $this->canonicalRangeMode((string) ($payload['repeat_range_mode'] ?? $existing['repeat_range_mode'])), + 'repeat_count' => array_key_exists('repeat_count', $payload) ? (is_null($payload['repeat_count']) ? null : (int) $payload['repeat_count']) : $existing['repeat_count'], + 'repeat_until' => array_key_exists('repeat_until', $payload) ? (empty($payload['repeat_until']) ? null : (string) $payload['repeat_until']) : $existing['repeat_until'], + 'timezone' => (string) ($payload['timezone'] ?? $existing['timezone']), + 'caldav_resource' => !empty($payload['caldav_resource']) + ? (string) $payload['caldav_resource'] + : ($currentResource !== '' ? $currentResource : $fallbackResource), + 'etag' => (string) ($payload['etag'] ?? $existing['etag'] ?? $this->makeEtag((string) $existing['uid'], (int) ($existing['sync_version'] ?? 1), $now)), + 'sync_version' => (int) ($payload['sync_version'] ?? (($existing['sync_version'] ?? 1) + 1)), + 'last_modified_by_user_id' => array_key_exists('last_modified_by_user_id', $payload) + ? (is_null($payload['last_modified_by_user_id']) ? null : (int) $payload['last_modified_by_user_id']) + : ($existing['last_modified_by_user_id'] ?? null), + 'updated_at' => $now, + ]; + + $this->db->update($this->eventsTable, $data, ['id' => $id]); + return $this->getEvent($id); + } + + public function deleteEvent(int $id): bool + { + $this->db->delete($this->exceptionsTable, ['event_id' => $id]); + $deleted = $this->db->delete($this->eventsTable, ['id' => $id]); + return $deleted !== false; + } + + public function deleteOccurrence(int $eventId, string $occurrenceKey): bool + { + $event = $this->getEvent($eventId); + if (!$event) { + return false; + } + $canonical = $this->canonicalOccurrenceKey($occurrenceKey); + if ($canonical === null) { + return false; + } + if (in_array($canonical, $this->deletedKeysForEvent($eventId), true)) { + return true; + } + $now = gmdate('c'); + $inserted = $this->db->insert( + $this->exceptionsTable, + [ + 'event_id' => $eventId, + 'occurrence_key' => $canonical, + 'exception_type' => 'deleted_occurrence', + 'created_at' => $now, + 'updated_at' => $now, + ] + ); + return $inserted !== false; + } + + public function listEventOccurrences(int $eventId, string $fromDate, int $months = 3): ?array + { + $event = $this->getEvent($eventId); + if (!$event) { + return null; + } + + $tz = new DateTimeZone('Europe/London'); + $start = $this->safeDate($fromDate, $tz)->setTime(0, 0, 0); + $months = max(1, min($months, 24)); + $end = $start->modify('+' . $months . ' month'); + $deleted = $this->deletedKeysForEvent($eventId); + + $items = RecurrenceExpander::expand($event, $start, $end, $deleted); + usort( + $items, + static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start']) + ); + return $items; + } + + public function previewOccurrences(array $payload, string $fromDate, int $months = 3): array + { + $startRaw = (string) ($payload['start_datetime'] ?? ''); + $endRaw = (string) ($payload['end_datetime'] ?? ''); + $start = $this->toLondonDateTimeString($startRaw); + $end = $this->toLondonDateTimeString($endRaw); + if ($start === '' || $end === '') { + throw new \InvalidArgumentException('start_datetime and end_datetime are required'); + } + if (new DateTimeImmutable($end) < new DateTimeImmutable($start)) { + throw new \InvalidArgumentException('end_datetime must be at or after start_datetime'); + } + + $repeatType = (string) ($payload['repeat_type'] ?? 'none'); + if ($repeatType === 'none') { + return []; + } + $repeatInterval = max(1, (int) ($payload['repeat_interval'] ?? 1)); + $repeatNthMode = (string) ($payload['repeat_nth_mode'] ?? ''); + $repeatNthDay = array_key_exists('repeat_nth_day', $payload) && $payload['repeat_nth_day'] !== null && $payload['repeat_nth_day'] !== '' + ? (int) $payload['repeat_nth_day'] + : null; + $repeatNthPos = array_key_exists('repeat_nth_pos', $payload) && $payload['repeat_nth_pos'] !== null && $payload['repeat_nth_pos'] !== '' + ? (int) $payload['repeat_nth_pos'] + : null; + $repeatNthWeekday = array_key_exists('repeat_nth_weekday', $payload) && $payload['repeat_nth_weekday'] !== null && $payload['repeat_nth_weekday'] !== '' + ? (int) $payload['repeat_nth_weekday'] + : null; + [$start, $end] = $this->normalizeMonthlyAnchor( + $start, + $end, + $repeatType, + $repeatInterval, + $repeatNthMode, + $repeatNthDay, + $repeatNthPos, + $repeatNthWeekday + ); + $event = [ + 'id' => 0, + 'uid' => 'preview@calendar-plugin', + 'title' => (string) ($payload['title'] ?? ''), + 'description' => (string) ($payload['description'] ?? ''), + 'location' => (string) ($payload['location'] ?? ''), + 'category' => (string) ($payload['category'] ?? ''), + 'all_day_event' => !empty($payload['all_day_event']), + 'start_datetime' => $start, + 'end_datetime' => $end, + 'repeat_type' => $repeatType, + 'repeat_interval' => $repeatInterval, + 'repeat_nth_mode' => $repeatNthMode, + 'repeat_nth_day' => $repeatNthDay, + 'repeat_nth_pos' => $repeatNthPos, + 'repeat_nth_weekday' => $repeatNthWeekday, + 'repeat_range_mode' => $this->canonicalRangeMode((string) ($payload['repeat_range_mode'] ?? 'none')), + 'repeat_count' => isset($payload['repeat_count']) ? (int) $payload['repeat_count'] : null, + 'repeat_until' => !empty($payload['repeat_until']) ? (string) $payload['repeat_until'] : null, + 'timezone' => 'Europe/London', + ]; + + $tz = new DateTimeZone('Europe/London'); + $startWindow = $this->safeDate($fromDate, $tz)->setTime(0, 0, 0); + $months = max(1, min($months, 24)); + $endWindow = $startWindow->modify('+' . $months . ' month'); + $deleted = []; + foreach ((array) ($payload['deleted_occurrence_keys'] ?? []) as $key) { + $canonical = $this->canonicalOccurrenceKey((string) $key); + if ($canonical !== null) { + $deleted[] = $canonical; + } + } + $items = RecurrenceExpander::expand($event, $startWindow, $endWindow, $deleted); + usort( + $items, + static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start']) + ); + return $items; + } + + public function getEventByResource(string $resource): ?array + { + $sql = $this->db->prepare("SELECT * FROM {$this->eventsTable} WHERE caldav_resource = %s", $resource); + $row = $this->db->getRow($sql); + return $row ? $this->normalizeRow($row) : null; + } + + public function getDeletedOccurrenceKeys(int $eventId): array + { + return $this->deletedKeysForEvent($eventId); + } + + public function syncDeletedOccurrenceKeys(int $eventId, array $keys, bool $replace = true): void + { + if ($replace) { + $this->db->delete($this->exceptionsTable, ['event_id' => $eventId, 'exception_type' => 'deleted_occurrence']); + } + $now = gmdate('c'); + foreach ($keys as $key) { + $canonical = $this->canonicalOccurrenceKey((string) $key); + if ($canonical === null) { + continue; + } + $this->db->insert( + $this->exceptionsTable, + [ + 'event_id' => $eventId, + 'occurrence_key' => $canonical, + 'exception_type' => 'deleted_occurrence', + 'created_at' => $now, + 'updated_at' => $now, + ] + ); + } + } + + public function listPublicOccurrences(string $view, string $dateAnchor, bool $futureOnly = false): array + { + $tz = new DateTimeZone('Europe/London'); + $anchor = $this->safeDate($dateAnchor, $tz); + if (strtolower($view) === 'list') { + $windowStart = $anchor->setTime(0, 0, 0); + if ($futureOnly) { + $today = new DateTimeImmutable('today', $tz); + if ($today > $windowStart) { + $windowStart = $today; + } + } + $windowEnd = $windowStart->modify('+18 months'); + } else { + [$windowStart, $windowEnd] = $this->windowForView($view, $anchor); + } + $events = $this->listEvents(); + + $out = []; + foreach ($events as $event) { + $deleted = $this->deletedKeysForEvent((int) $event['id']); + $items = RecurrenceExpander::expand($event, $windowStart, $windowEnd, $deleted); + array_push($out, ...$items); + } + + usort( + $out, + static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start']) + ); + + return $out; + } + + public function listSidebarUpcoming(int $days = 14): array + { + $tz = new DateTimeZone('Europe/London'); + $start = new DateTimeImmutable('today', $tz); + $end = $start->modify('+' . max(1, $days) . ' days'); + + $events = $this->listEvents(); + $out = []; + foreach ($events as $event) { + $deleted = $this->deletedKeysForEvent((int) $event['id']); + $items = RecurrenceExpander::expand($event, $start, $end, $deleted); + array_push($out, ...$items); + } + + usort( + $out, + static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start']) + ); + + return $out; + } + + public function deleteAllEventsData(): int + { + $events = $this->listEvents(); + $count = count($events); + $this->db->query("DELETE FROM {$this->exceptionsTable}"); + $this->db->query("DELETE FROM {$this->eventsTable}"); + return $count; + } + + public function seedDefaultEvents(): int + { + $seed = [ + [ + 'uid' => 'seed-ce-001@calendar-plugin', + 'title' => 'Board Meeting', + 'description' => 'Quarterly board review.', + 'location' => 'Room A', + 'category' => 'Governance', + 'start_datetime' => '2026-04-01T10:00:00+01:00', + 'end_datetime' => '2026-04-01T11:30:00+01:00', + 'repeat_type' => 'none', + ], + [ + 'uid' => 'seed-ce-002@calendar-plugin', + 'title' => 'Office Closed', + 'description' => 'Public holiday closure.', + 'location' => 'HQ', + 'category' => 'Operations', + 'all_day_event' => true, + 'start_datetime' => '2026-05-04T00:00:00+01:00', + 'end_datetime' => '2026-05-05T00:00:00+01:00', + 'repeat_type' => 'none', + ], + [ + 'uid' => 'seed-ce-003@calendar-plugin', + 'title' => 'Daily Standup', + 'description' => '15 minute sync.', + 'location' => 'Online', + 'category' => 'Team', + 'start_datetime' => '2026-04-06T09:00:00+01:00', + 'end_datetime' => '2026-04-06T09:15:00+01:00', + 'repeat_type' => 'daily', + 'repeat_interval' => 1, + 'repeat_range_mode' => 'until', + 'repeat_until' => '2026-04-15', + ], + [ + 'uid' => 'seed-ce-004@calendar-plugin', + 'title' => 'Community Lunch', + 'description' => 'Weekly community lunch.', + 'location' => 'Cafeteria', + 'category' => 'Community', + 'start_datetime' => '2026-04-08T12:30:00+01:00', + 'end_datetime' => '2026-04-08T13:30:00+01:00', + 'repeat_type' => 'weekly', + 'repeat_interval' => 1, + 'repeat_range_mode' => 'until', + 'repeat_until' => '2026-05-06', + ], + [ + 'uid' => 'seed-ce-005@calendar-plugin', + 'title' => 'Finance Close', + 'description' => 'Month-end close process.', + 'location' => 'Finance Office', + 'category' => 'Finance', + 'start_datetime' => '2026-03-31T17:00:00+01:00', + 'end_datetime' => '2026-03-31T18:00:00+01:00', + 'repeat_type' => 'monthly', + 'repeat_interval' => 1, + 'repeat_nth_mode' => 'day_of_month', + 'repeat_nth_day' => 30, + 'repeat_range_mode' => 'until', + 'repeat_until' => '2026-06-30', + ], + ]; + + foreach ($seed as $event) { + $this->createEvent($event); + } + return count($seed); + } + + private function normalizeRow(object $row): array + { + return [ + 'id' => (int) $row->id, + 'uid' => (string) $row->uid, + 'title' => (string) $row->title, + 'description' => (string) $row->description, + 'location' => (string) $row->location, + 'category' => (string) $row->category, + 'all_day_event' => (bool) $row->all_day_event, + 'start_datetime' => (string) $row->start_datetime, + 'end_datetime' => (string) $row->end_datetime, + 'repeat_type' => (string) $row->repeat_type, + 'repeat_interval' => (int) $row->repeat_interval, + 'repeat_nth_mode' => property_exists($row, 'repeat_nth_mode') ? (string) ($row->repeat_nth_mode ?? '') : '', + 'repeat_nth_day' => property_exists($row, 'repeat_nth_day') && $row->repeat_nth_day !== null ? (int) $row->repeat_nth_day : null, + 'repeat_nth_pos' => property_exists($row, 'repeat_nth_pos') && $row->repeat_nth_pos !== null ? (int) $row->repeat_nth_pos : null, + 'repeat_nth_weekday' => property_exists($row, 'repeat_nth_weekday') && $row->repeat_nth_weekday !== null ? (int) $row->repeat_nth_weekday : null, + 'repeat_range_mode' => (string) $row->repeat_range_mode, + 'repeat_count' => is_null($row->repeat_count) ? null : (int) $row->repeat_count, + 'repeat_until' => $row->repeat_until === null ? null : (string) $row->repeat_until, + 'timezone' => (string) $row->timezone, + 'caldav_resource' => property_exists($row, 'caldav_resource') ? (string) ($row->caldav_resource ?? '') : '', + 'etag' => property_exists($row, 'etag') ? (string) ($row->etag ?? '') : '', + 'sync_version' => property_exists($row, 'sync_version') ? (int) ($row->sync_version ?? 1) : 1, + 'last_modified_by_user_id' => property_exists($row, 'last_modified_by_user_id') && $row->last_modified_by_user_id !== null + ? (int) $row->last_modified_by_user_id + : null, + 'created_at' => (string) $row->created_at, + 'updated_at' => (string) $row->updated_at, + ]; + } + + private function deletedKeysForEvent(int $eventId): array + { + $sql = $this->db->prepare( + "SELECT occurrence_key FROM {$this->exceptionsTable} WHERE event_id = %d AND exception_type = 'deleted_occurrence'", + $eventId + ); + $rows = $this->db->getResults($sql); + return array_map(static fn(object $r): string => (string) $r->occurrence_key, $rows); + } + + private function safeDate(string $dateAnchor, DateTimeZone $tz): DateTimeImmutable + { + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateAnchor)) { + return new DateTimeImmutable($dateAnchor . 'T00:00:00', $tz); + } + return new DateTimeImmutable('today', $tz); + } + + private function windowForView(string $view, DateTimeImmutable $anchor): array + { + $view = strtolower($view); + if ($view === 'day') { + $start = $anchor->setTime(0, 0, 0); + return [$start, $start->modify('+1 day')]; + } + if ($view === 'week') { + $weekday = (int) $anchor->format('w'); + $start = $anchor->modify('-' . $weekday . ' day')->setTime(0, 0, 0); + return [$start, $start->modify('+7 days')]; + } + if ($view === 'year') { + $start = $anchor->setDate((int) $anchor->format('Y'), 1, 1)->setTime(0, 0, 0); + return [$start, $start->modify('+1 year')]; + } + $monthStart = $anchor->setDate((int) $anchor->format('Y'), (int) $anchor->format('m'), 1)->setTime(0, 0, 0); + $startWeekday = (int) $monthStart->format('w'); + $gridStart = $monthStart->modify('-' . $startWeekday . ' day'); + $gridEnd = $gridStart->modify('+42 days'); + return [$gridStart, $gridEnd]; + } + + private function canonicalOccurrenceKey(string $value): ?string + { + try { + if (!str_contains($value, 'T') && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + $dt = new DateTimeImmutable($value . 'T00:00:00', new DateTimeZone('Europe/London')); + return $dt->format('c'); + } + $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London')); + return $dt->format('c'); + } catch (\Throwable) { + return null; + } + } + + private function makeEtag(string $uid, int $syncVersion, string $stamp): string + { + return '"' . substr(sha1($uid . ':' . $syncVersion . ':' . $stamp), 0, 16) . '"'; + } + + private function resourceFromUid(string $uid): string + { + $uid = trim($uid); + if ($uid === '') { + $uid = bin2hex(random_bytes(10)) . '@calendar-plugin'; + } + return $uid . '.ics'; + } + + private function toLondonDateTimeString(string $value): string + { + $value = trim($value); + if ($value === '') { + return ''; + } + try { + $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London')); + return $dt->setTimezone(new DateTimeZone('Europe/London'))->format('c'); + } catch (\Throwable) { + throw new \InvalidArgumentException('invalid datetime value'); + } + } + + private function canonicalRangeMode(string $value): string + { + $v = strtolower(trim($value)); + if ($v === 'no_end' || $v === '') { + return 'none'; + } + return in_array($v, ['none', 'count', 'until'], true) ? $v : 'none'; + } + + private function normalizeMonthlyAnchor( + string $startIso, + string $endIso, + string $repeatType, + int $repeatInterval, + string $repeatNthMode, + ?int $repeatNthDay, + ?int $repeatNthPos, + ?int $repeatNthWeekday + ): array { + if ($repeatType !== 'monthly') { + return [$startIso, $endIso]; + } + try { + $tz = new DateTimeZone('Europe/London'); + $start = new DateTimeImmutable($startIso, $tz); + $end = new DateTimeImmutable($endIso, $tz); + $duration = $start->diff($end); + $targetDay = (int) $start->format('j'); + if ($repeatNthMode === 'day_of_month' && $repeatNthDay !== null) { + $daysInMonth = (int) $start->format('t'); + $targetDay = max(1, min($repeatNthDay, $daysInMonth)); + } elseif ($repeatNthMode === 'weekday_of_month' && $repeatNthPos !== null && $repeatNthWeekday !== null) { + $nthDay = $this->nthWeekdayOfMonth((int) $start->format('Y'), (int) $start->format('n'), $repeatNthWeekday, $repeatNthPos); + if ($nthDay === null) { + $year = (int) $start->format('Y'); + $month = (int) $start->format('n'); + $step = max(1, $repeatInterval); + for ($i = 0; $i < 120; $i++) { + [$year, $month] = $this->addMonths($year, $month, $step); + $nthDay = $this->nthWeekdayOfMonth($year, $month, $repeatNthWeekday, $repeatNthPos); + if ($nthDay !== null) { + $start = $start->setDate($year, $month, $nthDay); + $targetDay = $nthDay; + break; + } + } + } else { + $targetDay = $nthDay; + } + } + $anchoredStart = $start->setDate((int) $start->format('Y'), (int) $start->format('n'), $targetDay); + $anchoredEnd = $anchoredStart->add($duration); + return [$anchoredStart->format('c'), $anchoredEnd->format('c')]; + } catch (\Throwable) { + return [$startIso, $endIso]; + } + } + + private function nthWeekdayOfMonth(int $year, int $month, int $weekday, int $pos): ?int + { + $weekday = max(0, min(6, $weekday)); + $tz = new DateTimeZone('Europe/London'); + if ($pos === -1) { + $last = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz); + $last = $last->modify('last day of this month'); + for ($day = (int) $last->format('j'); $day >= 1; $day--) { + $d = $last->setDate($year, $month, $day); + if ((int) $d->format('w') === $weekday) { + return $day; + } + } + return null; + } + + $first = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz); + $daysInMonth = (int) $first->format('t'); + $seen = 0; + for ($day = 1; $day <= $daysInMonth; $day++) { + $d = $first->setDate($year, $month, $day); + if ((int) $d->format('w') !== $weekday) { + continue; + } + $seen++; + if ($seen === $pos) { + return $day; + } + } + return null; + } + + private function addMonths(int $year, int $month, int $delta): array + { + $index = ($year * 12) + ($month - 1) + $delta; + $newYear = (int) floor($index / 12); + $newMonth = ($index % 12) + 1; + if ($newMonth <= 0) { + $newMonth += 12; + $newYear -= 1; + } + return [$newYear, $newMonth]; + } +} diff --git a/code/src/Domain/IcsService.php b/code/src/Domain/IcsService.php new file mode 100644 index 0000000..c974626 --- /dev/null +++ b/code/src/Domain/IcsService.php @@ -0,0 +1,439 @@ +escapeText($calendarName), + 'X-WR-TIMEZONE:Europe/London', + ]; + + foreach ($events as $event) { + $lines = array_merge($lines, $this->eventToLines($event, (array) $deletedKeysProvider((int) ($event['id'] ?? 0)))); + } + + $lines[] = 'END:VCALENDAR'; + + return implode("\r\n", $this->foldLines($lines)) . "\r\n"; + } + + public function parseEventFromIcs(string $ics): ?array + { + $props = $this->extractVeventProperties($ics); + if ($props === null) { + return null; + } + + $uid = (string) ($props['UID'][0] ?? ''); + $summary = (string) ($props['SUMMARY'][0] ?? 'Untitled'); + $description = (string) ($props['DESCRIPTION'][0] ?? ''); + $location = (string) ($props['LOCATION'][0] ?? ''); + $category = (string) ($props['CATEGORIES'][0] ?? ''); + $dtstartRaw = (string) ($props['DTSTART'][0] ?? ''); + $dtendRaw = (string) ($props['DTEND'][0] ?? ''); + if ($dtstartRaw === '' || $dtendRaw === '') { + return null; + } + + $allDay = str_contains((string) ($props['_DTSTART_PARAMS'][0] ?? ''), 'VALUE=DATE'); + $start = $this->parseIcsDateTime($dtstartRaw, $allDay); + $end = $this->parseIcsDateTime($dtendRaw, $allDay); + if ($start === null || $end === null) { + return null; + } + + $payload = [ + 'uid' => $uid !== '' ? $uid : bin2hex(random_bytes(10)) . '@calendar-plugin', + 'title' => $summary, + 'description' => $description, + 'location' => $location, + 'category' => $category, + 'all_day_event' => $allDay, + 'start_datetime' => $start, + 'end_datetime' => $end, + 'repeat_type' => 'none', + 'repeat_interval' => 1, + 'repeat_nth_mode' => '', + 'repeat_nth_day' => null, + 'repeat_nth_pos' => null, + 'repeat_nth_weekday' => null, + 'repeat_range_mode' => 'none', + 'repeat_count' => null, + 'repeat_until' => null, + 'timezone' => 'Europe/London', + ]; + + $rrule = (string) ($props['RRULE'][0] ?? ''); + if ($rrule !== '') { + $payload = array_merge($payload, $this->parseRrule($rrule)); + } + + $exdates = []; + foreach (($props['EXDATE'] ?? []) as $exdateRaw) { + $chunks = array_filter(array_map('trim', explode(',', (string) $exdateRaw))); + foreach ($chunks as $chunk) { + $asDate = $this->parseIcsDateTime($chunk, false); + if ($asDate !== null) { + $exdates[] = $asDate; + } + } + } + $payload['deleted_occurrence_keys'] = $exdates; + + return $payload; + } + + private function eventToLines(array $event, array $deletedKeys): array + { + $uid = (string) ($event['uid'] ?? ''); + $uid = $uid !== '' ? $uid : ('event-' . (string) ($event['id'] ?? 0) . '@calendar-plugin'); + + $start = $this->toDateTime((string) ($event['start_datetime'] ?? '')); + $end = $this->toDateTime((string) ($event['end_datetime'] ?? '')); + if ($start === null || $end === null) { + return []; + } + + $allDay = (bool) ($event['all_day_event'] ?? false); + $updated = $this->toDateTime((string) ($event['updated_at'] ?? '')) ?? new DateTimeImmutable('now', new DateTimeZone('UTC')); + + $lines = [ + 'BEGIN:VEVENT', + 'UID:' . $this->escapeText($uid), + 'SUMMARY:' . $this->escapeText((string) ($event['title'] ?? 'Untitled')), + 'DESCRIPTION:' . $this->escapeText((string) ($event['description'] ?? '')), + 'LOCATION:' . $this->escapeText((string) ($event['location'] ?? '')), + 'CATEGORIES:' . $this->escapeText((string) ($event['category'] ?? '')), + 'DTSTAMP:' . $this->toUtcIcs($updated), + 'LAST-MODIFIED:' . $this->toUtcIcs($updated), + ]; + + if ($allDay) { + $lines[] = 'DTSTART;VALUE=DATE:' . $start->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd'); + $lines[] = 'DTEND;VALUE=DATE:' . $end->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd'); + } else { + $lines[] = 'DTSTART;TZID=Europe/London:' . $start->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis'); + $lines[] = 'DTEND;TZID=Europe/London:' . $end->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis'); + } + + $rrule = $this->eventToRrule($event); + if ($rrule !== null) { + $lines[] = 'RRULE:' . $rrule; + } + + if ($deletedKeys) { + $parts = []; + foreach ($deletedKeys as $key) { + $dt = $this->toDateTime((string) $key); + if ($dt === null) { + continue; + } + $parts[] = $dt->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis'); + } + if ($parts) { + $lines[] = 'EXDATE;TZID=Europe/London:' . implode(',', $parts); + } + } + + $lines[] = 'END:VEVENT'; + return $lines; + } + + private function eventToRrule(array $event): ?string + { + $type = strtolower((string) ($event['repeat_type'] ?? 'none')); + if ($type === 'none') { + return null; + } + + $freq = match ($type) { + 'daily' => 'DAILY', + 'weekly', 'custom' => 'WEEKLY', + 'monthly' => 'MONTHLY', + 'yearly' => 'YEARLY', + default => null, + }; + if ($freq === null) { + return null; + } + + $interval = max(1, (int) ($event['repeat_interval'] ?? 1)); + $parts = ['FREQ=' . $freq, 'INTERVAL=' . $interval]; + if ($type === 'monthly') { + $nthMode = (string) ($event['repeat_nth_mode'] ?? ''); + $nthDay = isset($event['repeat_nth_day']) && $event['repeat_nth_day'] !== null ? (int) $event['repeat_nth_day'] : null; + $nthPos = isset($event['repeat_nth_pos']) && $event['repeat_nth_pos'] !== null ? (int) $event['repeat_nth_pos'] : null; + $nthWeekday = isset($event['repeat_nth_weekday']) && $event['repeat_nth_weekday'] !== null ? (int) $event['repeat_nth_weekday'] : null; + if ($nthMode === 'day_of_month' && $nthDay !== null) { + $parts[] = 'BYMONTHDAY=' . max(1, min(31, $nthDay)); + } elseif ($nthMode === 'weekday_of_month' && $nthPos !== null && $nthWeekday !== null) { + $byDay = $this->weekdayNumToToken($nthWeekday); + if ($byDay !== null) { + $parts[] = 'BYDAY=' . $byDay; + $parts[] = 'BYSETPOS=' . ($nthPos < 0 ? -1 : max(1, min(5, $nthPos))); + } + } + } + + $rangeMode = strtolower((string) ($event['repeat_range_mode'] ?? 'none')); + if ($rangeMode === 'count' && !empty($event['repeat_count'])) { + $parts[] = 'COUNT=' . max(1, (int) $event['repeat_count']); + } + if ($rangeMode === 'until' && !empty($event['repeat_until'])) { + $until = $this->toDateTime((string) $event['repeat_until'] . 'T23:59:59'); + if ($until !== null) { + $parts[] = 'UNTIL=' . $this->toUtcIcs($until); + } + } + + return implode(';', $parts); + } + + private function parseRrule(string $rrule): array + { + $parts = []; + foreach (explode(';', strtoupper(trim($rrule))) as $chunk) { + [$k, $v] = array_pad(explode('=', $chunk, 2), 2, ''); + if ($k !== '') { + $parts[$k] = $v; + } + } + + $repeatType = match ($parts['FREQ'] ?? '') { + 'DAILY' => 'daily', + 'WEEKLY' => 'weekly', + 'MONTHLY' => 'monthly', + 'YEARLY' => 'yearly', + default => 'none', + }; + + $payload = [ + 'repeat_type' => $repeatType, + 'repeat_interval' => max(1, (int) ($parts['INTERVAL'] ?? 1)), + 'repeat_nth_mode' => '', + 'repeat_nth_day' => null, + 'repeat_nth_pos' => null, + 'repeat_nth_weekday' => null, + 'repeat_range_mode' => 'none', + 'repeat_count' => null, + 'repeat_until' => null, + ]; + + if ($repeatType === 'monthly') { + if (!empty($parts['BYMONTHDAY'])) { + $raw = trim(explode(',', (string) $parts['BYMONTHDAY'])[0]); + if (preg_match('/^-?\d+$/', $raw)) { + $payload['repeat_nth_mode'] = 'day_of_month'; + $payload['repeat_nth_day'] = max(1, min(31, (int) $raw)); + } + } elseif (!empty($parts['BYDAY'])) { + $byDayRaw = trim(explode(',', (string) $parts['BYDAY'])[0]); + $pos = null; + $token = $byDayRaw; + if (preg_match('/^(-?\d+)([A-Z]{2})$/', $byDayRaw, $m)) { + $pos = (int) $m[1]; + $token = $m[2]; + } + $weekday = $this->weekdayTokenToNum($token); + if ($weekday !== null) { + $payload['repeat_nth_mode'] = 'weekday_of_month'; + $payload['repeat_nth_weekday'] = $weekday; + if (isset($parts['BYSETPOS']) && preg_match('/^-?\d+$/', (string) $parts['BYSETPOS'])) { + $pos = (int) $parts['BYSETPOS']; + } + $payload['repeat_nth_pos'] = $pos === null ? 1 : ($pos < 0 ? -1 : max(1, min(5, $pos))); + } + } + } + + if (isset($parts['COUNT'])) { + $payload['repeat_range_mode'] = 'count'; + $payload['repeat_count'] = max(1, (int) $parts['COUNT']); + } elseif (isset($parts['UNTIL'])) { + $until = $this->parseIcsDateTime($parts['UNTIL'], false); + if ($until !== null) { + $payload['repeat_range_mode'] = 'until'; + $payload['repeat_until'] = substr($until, 0, 10); + } + } + + return $payload; + } + + private function extractVeventProperties(string $ics): ?array + { + $lines = preg_split('/\r\n|\n|\r/', $ics) ?: []; + $unfolded = []; + foreach ($lines as $line) { + if ($line === '') { + continue; + } + if (($line[0] ?? '') === ' ' && $unfolded) { + $unfolded[count($unfolded) - 1] .= substr($line, 1); + continue; + } + $unfolded[] = $line; + } + + $in = false; + $props = []; + foreach ($unfolded as $line) { + $upper = strtoupper($line); + if ($upper === 'BEGIN:VEVENT') { + $in = true; + continue; + } + if ($upper === 'END:VEVENT') { + break; + } + if (!$in) { + continue; + } + [$left, $value] = array_pad(explode(':', $line, 2), 2, ''); + if ($left === '') { + continue; + } + [$name, $params] = array_pad(explode(';', $left, 2), 2, ''); + $name = strtoupper(trim($name)); + if ($name === '') { + continue; + } + $props[$name][] = $this->unescapeText(trim($value)); + if ($name === 'DTSTART') { + $props['_DTSTART_PARAMS'][] = strtoupper(trim($params)); + } + } + + return $in ? $props : null; + } + + private function parseIcsDateTime(string $value, bool $dateOnly): ?string + { + $value = trim($value); + if ($value === '') { + return null; + } + + try { + if ($dateOnly && preg_match('/^\d{8}$/', $value)) { + $dt = DateTimeImmutable::createFromFormat('Ymd H:i:s', $value . ' 00:00:00', new DateTimeZone('Europe/London')); + if ($dt instanceof DateTimeImmutable) { + return $dt->format('Y-m-d\\T00:00:00P'); + } + } + + if (preg_match('/^\d{8}T\d{6}Z$/', $value)) { + $dt = DateTimeImmutable::createFromFormat('Ymd\\THis\\Z', $value, new DateTimeZone('UTC')); + if ($dt instanceof DateTimeImmutable) { + return $dt->setTimezone(new DateTimeZone('Europe/London'))->format('c'); + } + } + + if (preg_match('/^\d{8}T\d{6}$/', $value)) { + $dt = DateTimeImmutable::createFromFormat('Ymd\\THis', $value, new DateTimeZone('Europe/London')); + if ($dt instanceof DateTimeImmutable) { + return $dt->format('c'); + } + } + + $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London')); + return $dt->format('c'); + } catch (\Throwable) { + return null; + } + } + + private function toDateTime(string $value): ?DateTimeImmutable + { + if ($value === '') { + return null; + } + try { + return new DateTimeImmutable($value, new DateTimeZone('Europe/London')); + } catch (\Throwable) { + return null; + } + } + + private function toUtcIcs(DateTimeImmutable $dt): string + { + return $dt->setTimezone(new DateTimeZone('UTC'))->format('Ymd\\THis\\Z'); + } + + private function escapeText(string $value): string + { + return str_replace( + ["\\", ";", ",", "\r\n", "\n", "\r"], + ["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"], + $value + ); + } + + private function unescapeText(string $value): string + { + return str_replace( + ["\\n", "\\N", "\\,", "\\;", "\\\\"], + ["\n", "\n", ",", ";", "\\"], + $value + ); + } + + private function weekdayNumToToken(int $weekday): ?string + { + return match ($weekday) { + 0 => 'SU', + 1 => 'MO', + 2 => 'TU', + 3 => 'WE', + 4 => 'TH', + 5 => 'FR', + 6 => 'SA', + default => null, + }; + } + + private function weekdayTokenToNum(string $token): ?int + { + return match (strtoupper(trim($token))) { + 'SU' => 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + default => null, + }; + } + + private function foldLines(array $lines): array + { + $out = []; + foreach ($lines as $line) { + if ($line === '') { + $out[] = $line; + continue; + } + while (strlen($line) > 73) { + $out[] = substr($line, 0, 73); + $line = ' ' . substr($line, 73); + } + $out[] = $line; + } + return $out; + } +} diff --git a/code/src/Domain/RecurrenceExpander.php b/code/src/Domain/RecurrenceExpander.php new file mode 100644 index 0000000..71fe24e --- /dev/null +++ b/code/src/Domain/RecurrenceExpander.php @@ -0,0 +1,193 @@ +diff($end); + $repeatType = (string) ($event['repeat_type'] ?? 'none'); + $interval = max(1, (int) ($event['repeat_interval'] ?? 1)); + $rangeMode = (string) ($event['repeat_range_mode'] ?? 'none'); + $repeatCount = isset($event['repeat_count']) ? (int) $event['repeat_count'] : null; + $repeatUntil = null; + if ($rangeMode === 'until' && !empty($event['repeat_until'])) { + $repeatUntil = self::parseDateTime((string) $event['repeat_until'] . 'T23:59:59', $tz); + } + + if ($repeatType === 'none') { + if (self::overlaps($start, $end, $windowStart, $windowEnd)) { + return [self::occurrence($event, $start, $end)]; + } + return []; + } + + $occurrences = []; + $current = $start; + $produced = 0; + + for ($i = 0; $i < self::MAX_ITERATIONS; $i++) { + if ($rangeMode === 'count' && $repeatCount !== null && $produced >= $repeatCount) { + break; + } + if ($repeatUntil && $current > $repeatUntil) { + break; + } + $currentEnd = $current->add($duration); + if (self::overlaps($current, $currentEnd, $windowStart, $windowEnd)) { + $key = $current->format('c'); + if (!isset($deletedMap[$key])) { + $occurrences[] = self::occurrence($event, $current, $currentEnd); + } + } + if ($current > $windowEnd->modify('+400 days')) { + break; + } + $produced++; + $current = self::nextStart($current, $repeatType, $interval, $event); + if (!$current) { + break; + } + } + + return $occurrences; + } + + private static function nextStart(DateTimeImmutable $current, string $repeatType, int $interval, array $event): ?DateTimeImmutable + { + return match ($repeatType) { + 'daily' => $current->add(new DateInterval('P' . $interval . 'D')), + 'weekly', 'custom' => $current->add(new DateInterval('P' . $interval . 'W')), + 'monthly' => self::nextMonthlyStart($current, $interval, $event), + 'yearly' => $current->modify('+' . $interval . ' year') ?: null, + default => null, + }; + } + + private static function nextMonthlyStart(DateTimeImmutable $current, int $interval, array $event): ?DateTimeImmutable + { + $mode = (string) ($event['repeat_nth_mode'] ?? ''); + $next = $current->modify('+' . $interval . ' month'); + if (!$next) { + return null; + } + if ($mode === 'day_of_month' && isset($event['repeat_nth_day']) && $event['repeat_nth_day'] !== null) { + $day = max(1, (int) $event['repeat_nth_day']); + $daysInMonth = (int) $next->format('t'); + return $next->setDate((int) $next->format('Y'), (int) $next->format('n'), min($day, $daysInMonth)); + } + if ($mode === 'weekday_of_month' && isset($event['repeat_nth_pos'], $event['repeat_nth_weekday']) && $event['repeat_nth_pos'] !== null && $event['repeat_nth_weekday'] !== null) { + $year = (int) $current->format('Y'); + $month = (int) $current->format('n'); + for ($i = 0; $i < 120; $i++) { + [$year, $month] = self::addMonths($year, $month, max(1, $interval)); + $day = self::nthWeekdayOfMonth($year, $month, (int) $event['repeat_nth_weekday'], (int) $event['repeat_nth_pos']); + if ($day !== null) { + return $current->setDate($year, $month, $day); + } + } + return null; + } + return $next; + } + + private static function nthWeekdayOfMonth(int $year, int $month, int $weekday, int $pos): ?int + { + $weekday = max(0, min(6, $weekday)); + $tz = new DateTimeZone('Europe/London'); + if ($pos === -1) { + $last = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz); + $last = $last->modify('last day of this month'); + for ($day = (int) $last->format('j'); $day >= 1; $day--) { + $dt = $last->setDate($year, $month, $day); + if ((int) $dt->format('w') === $weekday) { + return $day; + } + } + return null; + } + $first = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz); + $daysInMonth = (int) $first->format('t'); + $seen = 0; + for ($day = 1; $day <= $daysInMonth; $day++) { + $dt = $first->setDate($year, $month, $day); + if ((int) $dt->format('w') !== $weekday) { + continue; + } + $seen++; + if ($seen === $pos) { + return $day; + } + } + return null; + } + + private static function occurrence(array $event, DateTimeImmutable $start, DateTimeImmutable $end): array + { + return [ + 'event_id' => (int) ($event['id'] ?? 0), + 'uid' => (string) ($event['uid'] ?? ''), + 'title' => (string) ($event['title'] ?? ''), + 'description' => (string) ($event['description'] ?? ''), + 'location' => (string) ($event['location'] ?? ''), + 'category' => (string) ($event['category'] ?? ''), + 'all_day_event' => (bool) ($event['all_day_event'] ?? false), + 'occurrence_start' => $start->format('c'), + 'occurrence_end' => $end->format('c'), + 'repeat_type' => (string) ($event['repeat_type'] ?? 'none'), + ]; + } + + private static function overlaps(DateTimeImmutable $start, DateTimeImmutable $end, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd): bool + { + return $start < $windowEnd && $end > $windowStart; + } + + private static function parseDateTime(string $value, DateTimeZone $tz): ?DateTimeImmutable + { + if ($value === '') { + return null; + } + if (!str_contains($value, 'T') && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + return new DateTimeImmutable($value . 'T00:00:00', $tz); + } + try { + return new DateTimeImmutable($value, $tz); + } catch (\Throwable) { + return null; + } + } + + private static function addMonths(int $year, int $month, int $delta): array + { + $index = ($year * 12) + ($month - 1) + $delta; + $newYear = (int) floor($index / 12); + $newMonth = ($index % 12) + 1; + if ($newMonth <= 0) { + $newMonth += 12; + $newYear -= 1; + } + return [$newYear, $newMonth]; + } +} diff --git a/code/src/Domain/SettingsService.php b/code/src/Domain/SettingsService.php new file mode 100644 index 0000000..4c24285 --- /dev/null +++ b/code/src/Domain/SettingsService.php @@ -0,0 +1,91 @@ + 'Public Calendar', + 'url_slug' => '', + 'verification_page_path' => '/calendar', + 'ics_access_mode' => 'public_read', + 'diagnostics_enabled' => '1', + 'uninstall_cleanup_mode' => 'keep', + ]; + + private const OPTION_PREFIX = 'calendar_plugin_'; + + public function __construct(private readonly OptionsAdapterInterface $options) + { + } + + public function getAll(): array + { + $out = []; + foreach (self::DEFAULTS as $key => $default) { + $out[$key] = $this->get($key, $default); + } + return $out; + } + + public function get(string $key, mixed $default = null): mixed + { + $fallback = $default ?? (self::DEFAULTS[$key] ?? null); + return $this->options->get(self::OPTION_PREFIX . $key, $fallback); + } + + public function update(array $payload): array + { + $allowed = array_keys(self::DEFAULTS); + $updated = $this->getAll(); + + foreach ($allowed as $key) { + if (!array_key_exists($key, $payload)) { + continue; + } + $value = $this->sanitize($key, $payload[$key]); + $this->options->set(self::OPTION_PREFIX . $key, $value); + $updated[$key] = $value; + } + + return $updated; + } + + private function sanitize(string $key, mixed $value): mixed + { + return match ($key) { + 'caldav_calendar_name' => trim((string) $value) ?: self::DEFAULTS[$key], + 'url_slug' => trim((string) $value, " \t\n\r\0\x0B/"), + 'verification_page_path' => $this->normalizePath((string) $value), + 'ics_access_mode' => in_array((string) $value, ['public_read', 'authenticated_read'], true) + ? (string) $value + : self::DEFAULTS['ics_access_mode'], + 'diagnostics_enabled' => $this->isTruthy($value) ? '1' : '0', + 'uninstall_cleanup_mode' => in_array((string) $value, ['keep', 'remove'], true) ? (string) $value : 'keep', + default => $value, + }; + } + + private function isTruthy(mixed $value): bool + { + return in_array(strtolower(trim((string) $value)), self::TRUE_VALUES, true); + } + + private function normalizePath(string $value): string + { + $path = trim($value); + if ($path === '') { + return self::DEFAULTS['verification_page_path']; + } + if (!str_starts_with($path, '/')) { + $path = '/' . $path; + } + return '/' . trim($path, '/'); + } +} diff --git a/code/src/Domain/UserService.php b/code/src/Domain/UserService.php new file mode 100644 index 0000000..2018195 --- /dev/null +++ b/code/src/Domain/UserService.php @@ -0,0 +1,473 @@ +getPrefix(); + $stem = trim($tableStem, '_'); + $this->usersTable = $prefix . $stem . '_users'; + $this->tokensTable = $prefix . $stem . '_user_tokens'; + $this->auditTable = $prefix . $stem . '_audit_log'; + } + + public function register(string $email, string $password): array + { + $email = $this->normalizeEmail($email); + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $this->error('validation_error', 'email is required', 422); + } + if (strlen($password) < 8) { + return $this->error('validation_error', 'password must be at least 8 characters', 422); + } + if ($this->isRateLimited('register:' . $email, 10, 3600)) { + return $this->error('rate_limited', 'too many requests', 429); + } + if ($this->findUserByEmail($email)) { + return $this->error('conflict_error', 'account already exists', 409); + } + + $now = gmdate('c'); + $inserted = $this->db->insert( + $this->usersTable, + [ + 'email' => $email, + 'password_hash' => password_hash($password, PASSWORD_DEFAULT), + 'email_verified_at' => null, + 'account_status' => 'pending_approval', + 'created_at' => $now, + 'updated_at' => $now, + ] + ); + if ($inserted === false) { + return $this->error('internal_error', 'unable to create account', 500); + } + + $userId = $this->db->insertId(); + $token = $this->issueToken($userId, 'email_verify', 24 * 3600); + $this->audit('user.register', (string) $userId, 'success', ['email' => $email]); + + return [ + 'ok' => true, + 'user' => $this->publicUser((array) $this->getUserById($userId)), + 'verify_token' => $token, + 'message' => 'registration submitted', + ]; + } + + public function verifyEmail(string $token): array + { + if ($token === '') { + return $this->error('validation_error', 'token is required', 422); + } + $tok = $this->consumeToken($token, 'email_verify'); + if (!$tok) { + return $this->error('validation_error', 'invalid or expired token', 422); + } + $user = $this->getUserById((int) $tok['user_id']); + if (!$user) { + return $this->error('not_found', 'user not found', 404); + } + + $now = gmdate('c'); + $this->db->update( + $this->usersTable, + ['email_verified_at' => $now, 'updated_at' => $now], + ['id' => (int) $user['id']] + ); + $updated = $this->getUserById((int) $user['id']); + $this->audit('user.verify_email', (string) $user['id'], 'success', []); + + return [ + 'ok' => true, + 'user' => $updated ? $this->publicUser($updated) : $this->publicUser($user), + 'message' => 'An admin will review your request and notify you if approved.', + ]; + } + + public function login(string $email, string $password): array + { + $email = $this->normalizeEmail($email); + if ($this->isRateLimited('login:' . $email, 20, 3600)) { + return $this->error('rate_limited', 'too many requests', 429); + } + + $user = $this->findUserByEmail($email); + if (!$user || !password_verify($password, (string) ($user['password_hash'] ?? ''))) { + return $this->error('auth_required', 'Login failure', 401); + } + if (empty($user['email_verified_at'])) { + return $this->error('auth_required', 'Login failure', 401); + } + if ((string) ($user['account_status'] ?? '') !== 'active') { + return $this->error('auth_required', 'Login failure', 401); + } + + $this->audit('user.login', (string) $user['id'], 'success', []); + + return [ + 'ok' => true, + 'user' => $this->publicUser($user), + ]; + } + + public function authenticateActiveUserCredentials(string $email, string $password): ?array + { + $email = $this->normalizeEmail($email); + if ($email === '' || $password === '') { + return null; + } + $user = $this->findUserByEmail($email); + if (!$user) { + return null; + } + if (!password_verify($password, (string) ($user['password_hash'] ?? ''))) { + return null; + } + if (empty($user['email_verified_at'])) { + return null; + } + if ((string) ($user['account_status'] ?? '') !== 'active') { + return null; + } + return $this->publicUser($user); + } + + public function issueSessionToken(int $userId, int $ttlSeconds = 2592000): string + { + if ($userId <= 0) { + return ''; + } + return $this->issueToken($userId, 'session', max(300, $ttlSeconds)); + } + + public function authenticateSessionToken(string $token): ?array + { + $row = $this->findValidToken($token, 'session'); + if ($row === null) { + return null; + } + + $user = $this->getUserById((int) ($row['user_id'] ?? 0)); + if (!$user) { + return null; + } + if (empty($user['email_verified_at'])) { + return null; + } + if ((string) ($user['account_status'] ?? '') !== 'active') { + return null; + } + + return $this->publicUser($user); + } + + public function revokeSessionToken(string $token): void + { + $row = $this->findValidToken($token, 'session'); + if ($row === null) { + return; + } + $this->db->update( + $this->tokensTable, + ['used_at' => gmdate('c')], + ['id' => (int) $row['id']] + ); + } + + public function requestPasswordReset(string $email): array + { + $email = $this->normalizeEmail($email); + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $this->error('validation_error', 'email is required', 422); + } + if ($this->isRateLimited('reset:' . $email, 10, 3600)) { + return $this->error('rate_limited', 'too many requests', 429); + } + + $user = $this->findUserByEmail($email); + if (!$user) { + return ['ok' => true, 'message' => 'if account exists, reset email will be sent']; + } + + $token = $this->issueToken((int) $user['id'], 'password_reset', 30 * 60); + $this->audit('user.password_reset.request', (string) $user['id'], 'success', []); + + return [ + 'ok' => true, + 'reset_token' => $token, + 'message' => 'password reset requested', + ]; + } + + public function resetPassword(string $token, string $newPassword): array + { + if (strlen($newPassword) < 8) { + return $this->error('validation_error', 'password must be at least 8 characters', 422); + } + + $tok = $this->consumeToken($token, 'password_reset'); + if (!$tok) { + return $this->error('validation_error', 'invalid or expired token', 422); + } + + $user = $this->getUserById((int) $tok['user_id']); + if (!$user) { + return $this->error('not_found', 'user not found', 404); + } + + $this->db->update( + $this->usersTable, + ['password_hash' => password_hash($newPassword, PASSWORD_DEFAULT), 'updated_at' => gmdate('c')], + ['id' => (int) $user['id']] + ); + // Invalidate persistent web sessions after password reset. + $this->db->delete($this->tokensTable, ['user_id' => (int) $user['id'], 'token_type' => 'session']); + $this->audit('user.password_reset.complete', (string) $user['id'], 'success', []); + + return ['ok' => true, 'message' => 'password updated']; + } + + public function listUsers(): array + { + $rows = $this->db->getResults("SELECT * FROM {$this->usersTable} ORDER BY id ASC"); + return array_map(fn(object $r): array => $this->publicUser((array) $r), $rows); + } + + public function approveUser(int $id): ?array + { + $user = $this->getUserById($id); + if (!$user) { + return null; + } + if (empty($user['email_verified_at'])) { + return null; + } + $this->db->update( + $this->usersTable, + ['account_status' => 'active', 'updated_at' => gmdate('c')], + ['id' => $id] + ); + $updated = $this->getUserById($id); + $this->audit('user.approve', (string) $id, 'success', []); + return $updated ? $this->publicUser($updated) : null; + } + + public function removeUser(int $id): bool + { + $deleted = $this->db->delete($this->usersTable, ['id' => $id]); + $this->db->delete($this->tokensTable, ['user_id' => $id]); + if ($deleted !== false) { + $this->audit('user.remove', (string) $id, 'success', []); + return true; + } + return false; + } + + private function findUserByEmail(string $email): ?array + { + foreach ($this->listRawUsers() as $user) { + if (strtolower((string) ($user['email'] ?? '')) === strtolower($email)) { + return $user; + } + } + return null; + } + + private function getUserById(int $id): ?array + { + $sql = $this->db->prepare("SELECT * FROM {$this->usersTable} WHERE id = %d", $id); + $row = $this->db->getRow($sql); + return $row ? (array) $row : null; + } + + private function listRawUsers(): array + { + $rows = $this->db->getResults("SELECT * FROM {$this->usersTable} ORDER BY id ASC"); + return array_map(static fn(object $r): array => (array) $r, $rows); + } + + private function issueToken(int $userId, string $type, int $ttlSeconds): string + { + $token = bin2hex(random_bytes(16)); + $hash = hash('sha256', $token); + $expiresAt = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->modify('+' . $ttlSeconds . ' seconds')->format('c'); + + $this->db->insert( + $this->tokensTable, + [ + 'user_id' => $userId, + 'token_type' => $type, + 'token_hash' => $hash, + 'expires_at' => $expiresAt, + 'used_at' => null, + 'created_at' => gmdate('c'), + ] + ); + + return $token; + } + + private function consumeToken(string $token, string $type): ?array + { + $row = $this->findValidToken($token, $type); + if ($row === null) { + return null; + } + $this->db->update( + $this->tokensTable, + ['used_at' => gmdate('c')], + ['id' => (int) $row['id']] + ); + return $row; + } + + private function findValidToken(string $token, string $type): ?array + { + $raw = trim($token); + if ($raw === '') { + return null; + } + $hash = hash('sha256', $raw); + $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); + + $rows = $this->db->getResults("SELECT * FROM {$this->tokensTable} ORDER BY id ASC"); + foreach ($rows as $rowObj) { + $row = (array) $rowObj; + if ((string) ($row['token_type'] ?? '') !== $type) { + continue; + } + if ((string) ($row['token_hash'] ?? '') !== $hash) { + continue; + } + if (!empty($row['used_at'])) { + return null; + } + try { + $expires = new DateTimeImmutable((string) $row['expires_at'], new DateTimeZone('UTC')); + } catch (\Throwable) { + return null; + } + if ($expires < $now) { + return null; + } + return $row; + } + return null; + } + + private function publicUser(array $user): array + { + return [ + 'id' => (int) ($user['id'] ?? 0), + 'email' => (string) ($user['email'] ?? ''), + 'email_verified_at' => $user['email_verified_at'] ?? null, + 'account_status' => (string) ($user['account_status'] ?? 'pending_approval'), + 'created_at' => (string) ($user['created_at'] ?? ''), + 'updated_at' => (string) ($user['updated_at'] ?? ''), + ]; + } + + private function error(string $code, string $message, int $status): array + { + return ['error' => ['code' => $code, 'message' => $message, 'status' => $status]]; + } + + private function normalizeEmail(string $email): string + { + return strtolower(trim($email)); + } + + private function audit(string $action, string $target, string $result, array $context): void + { + if (!$this->isDiagnosticsEnabled()) { + return; + } + $this->db->insert( + $this->auditTable, + [ + 'actor' => 'plugin', + 'action' => $action, + 'target' => $target, + 'result' => $result, + 'created_at' => gmdate('c'), + 'context_json' => json_encode($context), + ] + ); + } + + private function isDiagnosticsEnabled(): bool + { + $optionsTable = $this->db->getPrefix() . 'options'; + $sql = $this->db->prepare( + "SELECT option_value FROM {$optionsTable} WHERE option_name = %s LIMIT 1", + 'calendar_plugin_diagnostics_enabled' + ); + $row = $this->db->getRow($sql); + if (!$row || !property_exists($row, 'option_value')) { + return true; + } + return in_array(strtolower(trim((string) $row->option_value)), ['1', 'true', 'yes', 'on'], true); + } + + private function isRateLimited(string $bucket, int $limit, int $windowSeconds): bool + { + $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); + $type = 'rate:' . substr(hash('sha256', $bucket), 0, 32); + $sql = $this->db->prepare("SELECT * FROM {$this->tokensTable} WHERE token_type = %s ORDER BY id ASC", $type); + $rows = $this->db->getResults($sql); + + $activeCount = 0; + foreach ($rows as $rowObj) { + $row = (array) $rowObj; + $id = (int) ($row['id'] ?? 0); + $usedAt = (string) ($row['used_at'] ?? ''); + $expiresAtRaw = (string) ($row['expires_at'] ?? ''); + $expired = true; + try { + $expiresAt = new DateTimeImmutable($expiresAtRaw, new DateTimeZone('UTC')); + $expired = $expiresAt < $now; + } catch (\Throwable) { + $expired = true; + } + + if ($id > 0 && ($usedAt !== '' || $expired)) { + $this->db->delete($this->tokensTable, ['id' => $id]); + continue; + } + + if (!$expired && $usedAt === '') { + $activeCount++; + } + } + + if ($activeCount >= $limit) { + return true; + } + + $this->db->insert( + $this->tokensTable, + [ + 'user_id' => 0, + 'token_type' => $type, + 'token_hash' => hash('sha256', bin2hex(random_bytes(16))), + 'expires_at' => $now->modify('+' . max(1, $windowSeconds) . ' seconds')->format('c'), + 'used_at' => null, + 'created_at' => gmdate('c'), + ] + ); + return false; + } +} diff --git a/code/src/Infrastructure/ServiceContainer.php b/code/src/Infrastructure/ServiceContainer.php new file mode 100644 index 0000000..874b495 --- /dev/null +++ b/code/src/Infrastructure/ServiceContainer.php @@ -0,0 +1,25 @@ + */ + private array $services = []; + + public function set(string $id, object $service): void + { + $this->services[$id] = $service; + } + + public function get(string $id): object + { + if (!isset($this->services[$id])) { + throw new \RuntimeException(sprintf('Service not found: %s', $id)); + } + + return $this->services[$id]; + } +} diff --git a/code/src/Infrastructure/WordPress/MigrationManager.php b/code/src/Infrastructure/WordPress/MigrationManager.php new file mode 100644 index 0000000..cb2281c --- /dev/null +++ b/code/src/Infrastructure/WordPress/MigrationManager.php @@ -0,0 +1,222 @@ +get_charset_collate(); + $prefix = $this->db->getPrefix(); + $stem = trim($this->tableStem, '_'); + + $events = $prefix . $stem . '_events'; + $exceptions = $prefix . $stem . '_recurrence_exceptions'; + $users = $prefix . $stem . '_users'; + $tokens = $prefix . $stem . '_user_tokens'; + $audit = $prefix . $stem . '_audit_log'; + + $sqlEvents = "CREATE TABLE {$events} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + uid VARCHAR(191) NOT NULL, + title TEXT NOT NULL, + description LONGTEXT NOT NULL, + location TEXT NOT NULL, + category TEXT NOT NULL, + all_day_event TINYINT(1) NOT NULL DEFAULT 0, + start_datetime VARCHAR(64) NOT NULL, + end_datetime VARCHAR(64) NOT NULL, + repeat_type VARCHAR(24) NOT NULL DEFAULT 'none', + repeat_interval INT NOT NULL DEFAULT 1, + repeat_nth_mode VARCHAR(32) NOT NULL DEFAULT '', + repeat_nth_day INT NULL, + repeat_nth_pos INT NULL, + repeat_nth_weekday INT NULL, + repeat_range_mode VARCHAR(24) NOT NULL DEFAULT 'none', + repeat_count INT NULL, + repeat_until VARCHAR(16) NULL, + timezone VARCHAR(64) NOT NULL DEFAULT 'Europe/London', + caldav_resource VARCHAR(191) NULL, + etag VARCHAR(64) NULL, + sync_version INT NOT NULL DEFAULT 1, + last_modified_by_user_id BIGINT UNSIGNED NULL, + created_at VARCHAR(32) NOT NULL, + updated_at VARCHAR(32) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uid (uid(191)), + UNIQUE KEY caldav_resource (caldav_resource), + KEY start_datetime (start_datetime(32)), + KEY end_datetime (end_datetime(32)) + ) {$charsetCollate};"; + + $sqlExceptions = "CREATE TABLE {$exceptions} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + event_id BIGINT UNSIGNED NOT NULL, + occurrence_key VARCHAR(64) NOT NULL, + exception_type VARCHAR(32) NOT NULL, + created_at VARCHAR(32) NOT NULL, + updated_at VARCHAR(32) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY event_occurrence (event_id, occurrence_key), + KEY event_id (event_id) + ) {$charsetCollate};"; + + $sqlUsers = "CREATE TABLE {$users} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + email VARCHAR(191) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + email_verified_at VARCHAR(32) NULL, + account_status VARCHAR(32) NOT NULL DEFAULT 'pending_approval', + created_at VARCHAR(32) NOT NULL, + updated_at VARCHAR(32) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY email (email) + ) {$charsetCollate};"; + + $sqlTokens = "CREATE TABLE {$tokens} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + token_type VARCHAR(32) NOT NULL, + token_hash VARCHAR(255) NOT NULL, + expires_at VARCHAR(32) NOT NULL, + used_at VARCHAR(32) NULL, + created_at VARCHAR(32) NOT NULL, + PRIMARY KEY (id), + KEY user_id (user_id), + KEY token_type (token_type) + ) {$charsetCollate};"; + + $sqlAudit = "CREATE TABLE {$audit} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + actor VARCHAR(191) NOT NULL, + action VARCHAR(191) NOT NULL, + target VARCHAR(191) NOT NULL, + result VARCHAR(32) NOT NULL, + created_at VARCHAR(32) NOT NULL, + context_json LONGTEXT NULL, + PRIMARY KEY (id), + KEY action (action), + KEY created_at (created_at) + ) {$charsetCollate};"; + + dbDelta($sqlEvents); + dbDelta($sqlExceptions); + dbDelta($sqlUsers); + dbDelta($sqlTokens); + dbDelta($sqlAudit); + + // Ensure every event has a stable CalDAV object resource name. + $this->db->query( + "UPDATE {$events} + SET caldav_resource = CONCAT(uid, '.ics') + WHERE (caldav_resource IS NULL OR caldav_resource = '') + AND uid IS NOT NULL + AND uid <> ''" + ); + $this->normalizeEventDateTimesToLondon($events); + + update_option(self::STEM_OPTION, $stem); + update_option('calendar_plugin_schema_version', self::SCHEMA_VERSION); + } + + public function assertActivationSafe(): void + { + $stem = trim($this->tableStem, '_'); + $ownedStem = trim((string) get_option(self::STEM_OPTION, ''), '_'); + if ($ownedStem !== '' && $ownedStem !== $stem) { + throw new \RuntimeException( + sprintf( + 'Calendar Plugin is already initialized with table stem "%s". Requested stem "%s" is different.', + $ownedStem, + $stem + ) + ); + } + if ($ownedStem !== $stem && $this->anyTargetTablesExist($stem)) { + $legacySchemaVersion = trim((string) get_option('calendar_plugin_schema_version', '')); + if ($legacySchemaVersion === '') { + throw new \RuntimeException( + sprintf( + 'Calendar Plugin activation blocked: target tables for stem "%s" already exist. Choose another table stem via CALENDAR_PLUGIN_TABLE_STEM.', + $stem + ) + ); + } + } + } + + private function anyTargetTablesExist(string $stem): bool + { + $prefix = $this->db->getPrefix(); + $tables = [ + $prefix . $stem . '_events', + $prefix . $stem . '_recurrence_exceptions', + $prefix . $stem . '_users', + $prefix . $stem . '_user_tokens', + $prefix . $stem . '_audit_log', + ]; + foreach ($tables as $table) { + $sql = $this->db->prepare('SHOW TABLES LIKE %s', $table); + if (count($this->db->getResults($sql)) > 0) { + return true; + } + } + return false; + } + + private function normalizeEventDateTimesToLondon(string $eventsTable): void + { + $rows = $this->db->getResults( + "SELECT id, start_datetime, end_datetime FROM {$eventsTable}" + ); + $tz = new DateTimeZone('Europe/London'); + foreach ($rows as $row) { + $id = (int) ($row->id ?? 0); + if ($id <= 0) { + continue; + } + $start = $this->normalizeDateTimeString((string) ($row->start_datetime ?? ''), $tz); + $end = $this->normalizeDateTimeString((string) ($row->end_datetime ?? ''), $tz); + if ($start === null || $end === null) { + continue; + } + $sql = $this->db->prepare( + "UPDATE {$eventsTable} SET start_datetime = %s, end_datetime = %s WHERE id = %d", + $start, + $end, + $id + ); + $this->db->query($sql); + } + } + + private function normalizeDateTimeString(string $value, DateTimeZone $tz): ?string + { + $value = trim($value); + if ($value === '') { + return null; + } + try { + $dt = new DateTimeImmutable($value, $tz); + return $dt->setTimezone($tz)->format('c'); + } catch (\Throwable) { + return null; + } + } +} diff --git a/code/src/Infrastructure/WordPress/WordPressAuthAdapter.php b/code/src/Infrastructure/WordPress/WordPressAuthAdapter.php new file mode 100644 index 0000000..b1f988f --- /dev/null +++ b/code/src/Infrastructure/WordPress/WordPressAuthAdapter.php @@ -0,0 +1,31 @@ +user_email ?? '') : ''; + } +} diff --git a/code/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php b/code/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php new file mode 100644 index 0000000..4fdc04e --- /dev/null +++ b/code/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php @@ -0,0 +1,60 @@ +wpdb->prefix; + } + + public function prepare(string $query, mixed ...$args): string + { + return (string) $this->wpdb->prepare($query, ...$args); + } + + public function query(string $query): int|false + { + return $this->wpdb->query($query); + } + + public function getResults(string $query): array + { + return $this->wpdb->get_results($query) ?: []; + } + + public function getRow(string $query): ?object + { + $row = $this->wpdb->get_row($query); + return is_object($row) ? $row : null; + } + + public function insert(string $table, array $data, array $formats = []): int|false + { + return $this->wpdb->insert($table, $data, $formats); + } + + public function update(string $table, array $data, array $where, array $formats = [], array $whereFormats = []): int|false + { + return $this->wpdb->update($table, $data, $where, $formats, $whereFormats); + } + + public function delete(string $table, array $where, array $whereFormats = []): int|false + { + return $this->wpdb->delete($table, $where, $whereFormats); + } + + public function insertId(): int + { + return (int) $this->wpdb->insert_id; + } +} diff --git a/code/src/Infrastructure/WordPress/WordPressHttpAdapter.php b/code/src/Infrastructure/WordPress/WordPressHttpAdapter.php new file mode 100644 index 0000000..1157fd7 --- /dev/null +++ b/code/src/Infrastructure/WordPress/WordPressHttpAdapter.php @@ -0,0 +1,25 @@ +tableStem = self::resolveTableStem(); + $this->settingsService = new SettingsService($this->options); + $this->eventService = new EventService($this->db, $this->tableStem); + $this->icsService = new IcsService(); + $this->calDavService = new CalDavService($this->eventService, $this->icsService); + $this->userService = new UserService($this->db, $this->tableStem); + } + + public static function boot(string $pluginFile): void + { + global $wpdb; + + $db = new WordPressDatabaseAdapter($wpdb); + $options = new WordPressOptionsAdapter(); + $auth = new WordPressAuthAdapter(); + $http = new WordPressHttpAdapter(); + + register_activation_hook($pluginFile, [self::class, 'activate']); + register_deactivation_hook($pluginFile, [self::class, 'deactivate']); + + $plugin = new self($db, $options, $auth, $http); + $plugin->register(); + } + + public static function activate(): void + { + try { + global $wpdb; + $db = new WordPressDatabaseAdapter($wpdb); + $tableStem = self::resolveTableStem(); + $migration = new MigrationManager($db, $tableStem); + $migration->assertActivationSafe(); + $migration->migrate(); + } catch (\Throwable $e) { + if (function_exists('wp_die')) { + wp_die( + esc_html($e->getMessage()), + 'Calendar Plugin Activation Blocked', + ['response' => 500, 'back_link' => true] + ); + } + throw $e; + } + } + + public static function deactivate(): void + { + // Intentionally no destructive behavior on deactivate. + } + + public function register(): void + { + $this->http->addAction('init', [$this, 'ensureSchemaCurrent']); + $this->http->addAction('init', [$this, 'onInit']); + $this->http->addAction('rest_api_init', [$this, 'registerRoutes']); + $this->http->addAction('admin_menu', [$this, 'registerAdminMenu']); + $this->http->addAction('admin_post_calendar_plugin_download_diagnostics', [$this, 'handleDiagnosticsDownload']); + $this->http->addAction('template_redirect', [$this, 'maybeServeSpecialEndpoints']); + } + + public function onInit(): void + { + $this->http->addShortcode('calendar', [$this, 'renderCalendarShortcode']); + $this->http->addShortcode('calendar_sidebar_upcoming', [$this, 'renderSidebarShortcode']); + } + + public function ensureSchemaCurrent(): void + { + $version = trim((string) get_option('calendar_plugin_schema_version', '')); + if ($version === '3') { + return; + } + try { + self::activate(); + } catch (\Throwable) { + // Avoid hard-failing page loads; diagnostics/setup will still surface issues. + } + } + + public function registerRoutes(): void + { + $this->registerHealthRoute(); + $this->registerSettingsRoutes(); + $this->registerEventRoutes(); + $this->registerPublicRoutes(); + $this->registerUserRoutes(); + $this->registerIcsRoutes(); + $this->registerCalDavRoutes(); + } + + public function renderCalendarShortcode(): string + { + $caldavRoot = $this->caldavRootPath(); + $icsUrl = $this->icsPath(); + $ns = self::API_NAMESPACE; + $caldavRootEsc = esc_html($caldavRoot); + $icsUrlEsc = esc_html($icsUrl); + $nsEsc = esc_html($ns); + $todayEsc = esc_html(gmdate('Y-m-d')); + + return strtr(<<<'HTML' +
+
+
+ Not logged in + + + CalDAV + ICS +
+
+ +
+ + + + + + + + +
+ + +
+
+ +

+

Events

+
+ + + + + + + + + +
+HTML + , [ + '__CALDAV_ROOT__' => $caldavRootEsc, + '__ICS_URL__' => $icsUrlEsc, + '__TODAY__' => $todayEsc, + '__NS__' => $nsEsc, + ]); + } + + public function renderSidebarShortcode(): string + { + $items = $this->eventService->listSidebarUpcoming(14); + if (!$items) { + return '

No upcoming events.

'; + } + + $rows = []; + foreach ($items as $item) { + $start = (string) ($item['occurrence_start'] ?? ''); + $end = (string) ($item['occurrence_end'] ?? ''); + $startTs = strtotime($start); + $endTs = strtotime($end); + $dateLabel = $startTs !== false ? date('j F Y', $startTs) : substr($start, 0, 10); + $timeLabel = ''; + if ($startTs !== false && $endTs !== false) { + $startTime = date('H:i', $startTs); + $endTime = date('H:i', $endTs); + if ($startTime !== '00:00' || $endTime !== '00:00') { + $timeLabel = $this->formatSidebarTimeRange($startTs, $endTs); + } + } + $title = trim((string) ($item['title'] ?? '')); + $description = trim((string) ($item['description'] ?? '')); + $headline = $dateLabel; + if ($timeLabel !== '') { + $headline .= ', ' . $timeLabel; + } + $headline .= ', ' . ($title !== '' ? $title : $description); + $rows[] = sprintf( + '

%s%s

', + esc_html($headline), + $description !== '' ? '
' . esc_html($description) : '' + ); + } + + return '
' . implode('', $rows) . '
'; + } + + private function formatSidebarTimeRange(int $startTs, int $endTs): string + { + $startMeridiem = strtolower(date('a', $startTs)); + $endMeridiem = strtolower(date('a', $endTs)); + $startLabel = $this->formatSidebarTimeValue($startTs); + $endLabel = $this->formatSidebarTimeValue($endTs); + if ($startMeridiem === $endMeridiem) { + $startLabel = preg_replace('/(am|pm)$/', '', $startLabel) ?: $startLabel; + return $startLabel . '–' . $endLabel; + } + return $startLabel . '–' . $endLabel; + } + + private function formatSidebarTimeValue(int $ts): string + { + $hour = (int) date('G', $ts); + $minute = (int) date('i', $ts); + $meridiem = strtolower(date('a', $ts)); + $hour12 = $hour % 12; + if ($hour12 === 0) { + $hour12 = 12; + } + if ($minute === 0) { + return $hour12 . $meridiem; + } + return $hour12 . '.' . str_pad((string) $minute, 2, '0', STR_PAD_LEFT) . $meridiem; + } + + public function registerAdminMenu(): void + { + if (!function_exists('add_menu_page')) { + return; + } + + add_menu_page( + 'Calendar Plugin', + 'Calendar Plugin', + 'manage_options', + 'calendar-plugin', + [$this, 'renderAdminSetupPage'] + ); + + add_submenu_page( + 'calendar-plugin', + 'Users', + 'Users', + 'manage_options', + 'calendar-plugin-users', + [$this, 'renderAdminUsersPage'] + ); + + add_submenu_page( + 'calendar-plugin', + 'Setup', + 'Setup', + 'manage_options', + 'calendar-plugin-setup', + [$this, 'renderAdminSetupPage'] + ); + + add_submenu_page( + 'calendar-plugin', + 'Diagnostics', + 'Diagnostics', + 'manage_options', + 'calendar-plugin-diagnostics', + [$this, 'renderAdminDiagnosticsPage'] + ); + + if (function_exists('remove_submenu_page')) { + remove_submenu_page('calendar-plugin', 'calendar-plugin'); + } + } + + public function renderAdminSetupPage(): void + { + $settings = $this->settingsService->getAll(); + $currentStem = trim((string) get_option('calendar_plugin_table_stem', $this->tableStem), '_'); + if ($currentStem === '') { + $currentStem = $this->tableStem; + } + $dbPrefix = (string) $this->db->getPrefix(); + $currentTablePrefix = $dbPrefix . $currentStem; + $message = ''; + $error = ''; + + if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { + $nonce = (string) ($_POST['_calendar_setup_nonce'] ?? ''); + $nonceSupported = function_exists('wp_nonce_field') && function_exists('wp_verify_nonce'); + if ($nonceSupported && ($nonce === '' || !$this->auth->verifyNonce($nonce, 'calendar_setup_update'))) { + $error = 'Invalid setup form submission.'; + } else { + $action = (string) ($_POST['setup_action'] ?? 'save_settings'); + if ($action === 'delete_all_events') { + $deleted = $this->eventService->deleteAllEventsData(); + $message = 'Deleted ' . $deleted . ' events and all recurrence exceptions.'; + } elseif ($action === 'seed_events') { + try { + $seeded = $this->eventService->seedDefaultEvents(); + $message = 'Seeded ' . $seeded . ' default events.'; + } catch (\Throwable $e) { + $error = 'Seed failed: ' . $e->getMessage(); + } + } else { + $payload = [ + 'caldav_calendar_name' => (string) ($_POST['caldav_calendar_name'] ?? ''), + 'url_slug' => (string) ($_POST['url_slug'] ?? ''), + 'verification_page_path' => (string) ($_POST['verification_page_path'] ?? ''), + 'ics_access_mode' => (string) ($_POST['ics_access_mode'] ?? ''), + 'diagnostics_enabled' => isset($_POST['diagnostics_enabled']) ? '1' : '0', + 'uninstall_cleanup_mode' => (string) ($_POST['uninstall_cleanup_mode'] ?? 'keep'), + ]; + $settings = $this->settingsService->update($payload); + $requestedPrefixRaw = strtolower(trim((string) ($_POST['table_prefix'] ?? $currentTablePrefix))); + $requestedPrefix = preg_replace('/[^a-z0-9_]/', '', $requestedPrefixRaw) ?: $currentTablePrefix; + $requestedPrefix = trim($requestedPrefix, '_'); + if ($requestedPrefix === '') { + $requestedPrefix = $currentTablePrefix; + } + if ($requestedPrefix !== $currentTablePrefix) { + $prefixResult = $this->updatePluginTablePrefix($currentStem, $requestedPrefix); + if ($prefixResult['ok']) { + $currentStem = (string) $prefixResult['stem']; + $currentTablePrefix = $dbPrefix . $currentStem; + $message = 'Settings saved. Table prefix updated to ' . $currentTablePrefix . '.'; + } else { + $error = (string) $prefixResult['message']; + } + } else { + $message = 'Settings saved.'; + } + } + } + } + + $nonceField = ''; + if (function_exists('wp_nonce_field')) { + ob_start(); + wp_nonce_field('calendar_setup_update', '_calendar_setup_nonce'); + $nonceField = (string) ob_get_clean(); + } else { + $nonceField = ''; + } + + $saveNote = ''; + if ($error !== '') { + $saveNote .= '

' . esc_html($error) . '

'; + } + if ($message !== '') { + $saveNote .= '

' . esc_html($message) . '

'; + } + + $icsMode = (string) ($settings['ics_access_mode'] ?? 'public_read'); + $icsModePublicSel = $icsMode === 'public_read' ? 'selected' : ''; + $icsModeAuthSel = $icsMode === 'authenticated_read' ? 'selected' : ''; + $diagnosticsChecked = ((string) ($settings['diagnostics_enabled'] ?? '1')) === '1' ? 'checked' : ''; + $cleanupMode = (string) ($settings['uninstall_cleanup_mode'] ?? 'keep'); + $cleanupKeepSel = $cleanupMode === 'keep' ? 'selected' : ''; + $cleanupRemoveSel = $cleanupMode === 'remove' ? 'selected' : ''; + + $form = '
' + . $nonceField + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '

' + . ' ' + . '

' + . '
'; + + echo $this->adminPageShell( + 'Setup', + $saveNote + . '

Configure presentation and endpoint behavior below or via API: /wp-json/' . self::API_NAMESPACE . '/settings.

' + . $form + . '' + ); + } + + public function renderAdminUsersPage(): void + { + $message = ''; + $error = ''; + + if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { + $nonce = (string) ($_POST['_calendar_users_nonce'] ?? ''); + $nonceSupported = function_exists('wp_nonce_field') && function_exists('wp_verify_nonce'); + if ($nonceSupported && ($nonce === '' || !$this->auth->verifyNonce($nonce, 'calendar_users_action'))) { + $error = 'Invalid users action submission.'; + } else { + $action = (string) ($_POST['users_action'] ?? ''); + $userId = (int) ($_POST['user_id'] ?? 0); + if ($userId <= 0) { + $error = 'Invalid user selection.'; + } elseif ($action === 'approve') { + $user = $this->userService->approveUser($userId); + if ($user === null) { + $error = 'User not found.'; + } else { + $message = 'User approved.'; + } + } elseif ($action === 'remove') { + $ok = $this->userService->removeUser($userId); + if (!$ok) { + $error = 'User not found.'; + } else { + $message = 'User removed.'; + } + } else { + $error = 'Unsupported users action.'; + } + } + } + + $nonceField = ''; + if (function_exists('wp_nonce_field')) { + ob_start(); + wp_nonce_field('calendar_users_action', '_calendar_users_nonce'); + $nonceField = (string) ob_get_clean(); + } else { + $nonceField = ''; + } + + $users = $this->userService->listUsers(); + $rows = ''; + foreach ($users as $u) { + $userId = (int) ($u['id'] ?? 0); + $status = (string) ($u['account_status'] ?? ''); + $isApproved = $status === 'active'; + $isVerified = !empty((string) ($u['email_verified_at'] ?? '')); + $approveButton = $isApproved + ? '' + : ($isVerified + ? '' + : ''); + $removeButton = ''; + $actions = '
' + . $nonceField + . '' + . $approveButton + . $removeButton + . '
'; + $rows .= '' + . '' . $userId . '' + . '' . esc_html((string) $u['email']) . '' + . '' . esc_html($status) . '' + . '' . esc_html((string) ($u['email_verified_at'] ?? '')) . '' + . '' . $actions . '' + . ''; + } + $feedback = ''; + if ($error !== '') { + $feedback .= '

' . esc_html($error) . '

'; + } + if ($message !== '') { + $feedback .= '

' . esc_html($message) . '

'; + } + + $body = $feedback + . '

Approve pending users or remove defunct users.

' + . '' + . ($rows !== '' ? $rows : '') + . '
IDEmailStatusEmail VerifiedActions
No users
'; + echo $this->adminPageShell('Users', $body); + } + + public function renderAdminDiagnosticsPage(): void + { + $enabled = $this->isDiagnosticsEnabled(); + $diag = $this->collectDiagnosticsSnapshot($enabled); + $downloadUrl = admin_url('admin-post.php?action=calendar_plugin_download_diagnostics&_wpnonce=' . wp_create_nonce('calendar_diagnostics_download')); + $downloadButton = $enabled + ? '

Download Diagnostics

' + : '

Enable diagnostics in Setup to collect and download diagnostics.

'; + echo $this->adminPageShell( + 'Diagnostics', + $downloadButton + . '

Runtime diagnostics snapshot:

' . esc_html(json_encode($diag, JSON_PRETTY_PRINT)) . '
' + ); + } + + public function handleDiagnosticsDownload(): void + { + if (!$this->auth->currentUserCan('manage_options')) { + wp_die('Not authorized.', 'Forbidden', ['response' => 403]); + } + $nonce = (string) ($_GET['_wpnonce'] ?? ''); + if (!$this->auth->verifyNonce($nonce, 'calendar_diagnostics_download')) { + wp_die('Invalid diagnostics download request.'); + } + if (!$this->isDiagnosticsEnabled()) { + wp_die('Diagnostics are disabled in Setup.'); + } + + $diag = $this->collectDiagnosticsSnapshot(true); + nocache_headers(); + header('Content-Type: application/json; charset=utf-8'); + header('Content-Disposition: attachment; filename="calendar-diagnostics-' . gmdate('Ymd-His') . '.json"'); + header('X-Content-Type-Options: nosniff'); + echo json_encode($diag, JSON_PRETTY_PRINT); + exit; + } + + private function isDiagnosticsEnabled(): bool + { + return ((string) ($this->settingsService->get('diagnostics_enabled', '1') ?? '1')) === '1'; + } + + public function maybeServeSpecialEndpoints(): void + { + $uri = (string) ($_SERVER['REQUEST_URI'] ?? ''); + $path = parse_url($uri, PHP_URL_PATH); + if (!is_string($path)) { + return; + } + + if ($path === $this->icsPath()) { + $this->serveIcsResponse(); + exit; + } + + if ($path === '/.well-known/caldav') { + wp_redirect($this->caldavRootPath(), 301); + exit; + } + + if ($path === rtrim($this->caldavRootPath(), '/') . '/') { + $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); + if ($method === 'OPTIONS') { + header('Allow: OPTIONS, PROPFIND, REPORT'); + header('DAV: 1, calendar-access'); + http_response_code(200); + exit; + } + } + + if (str_starts_with($path, rtrim($this->caldavRootPath(), '/'))) { + $this->serveCalDavPath($path, strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'))); + exit; + } + } + + private function registerHealthRoute(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/health', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + return [ + 'status' => 'ok', + 'plugin' => 'calendar-plugin', + 'version' => '0.1.15', + 'db_prefix' => $this->db->getPrefix(), + ]; + }, + ] + ); + } + + private function registerSettingsRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/settings', + [ + 'methods' => 'GET', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => fn(): array => ['data' => $this->settingsService->getAll()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/settings', + [ + 'methods' => 'PATCH', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + return ['data' => $this->settingsService->update($payload)]; + }, + ] + ); + } + + private function registerEventRoutes(): void + { + $canWrite = fn($request = null): bool => $this->canWriteCalendar($request); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events', + [ + 'methods' => 'GET', + 'permission_callback' => $canWrite, + 'callback' => fn(): array => ['data' => $this->eventService->listEvents()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events', + [ + 'methods' => 'POST', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + try { + $deletedKeys = isset($payload['deleted_occurrence_keys']) && is_array($payload['deleted_occurrence_keys']) + ? array_values($payload['deleted_occurrence_keys']) + : []; + unset($payload['deleted_occurrence_keys']); + $created = $this->eventService->createEvent($payload); + if ($deletedKeys !== []) { + $this->eventService->syncDeletedOccurrenceKeys((int) ($created['id'] ?? 0), $deletedKeys, true); + $created = $this->eventService->getEvent((int) ($created['id'] ?? 0)) ?: $created; + } + return ['data' => $created]; + } catch (\InvalidArgumentException $e) { + return $this->error('validation_error', $e->getMessage(), 422); + } catch (\RuntimeException $e) { + return $this->error('internal_error', $e->getMessage(), 500); + } + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)', + [ + 'methods' => 'GET', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $event = $this->eventService->getEvent($id); + if (!$event) { + return $this->error('not_found', 'event not found', 404); + } + return ['data' => $event]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)', + [ + 'methods' => 'PATCH', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + try { + $deletedKeysProvided = isset($payload['deleted_occurrence_keys']) && is_array($payload['deleted_occurrence_keys']); + $deletedKeys = $deletedKeysProvided ? array_values((array) $payload['deleted_occurrence_keys']) : []; + unset($payload['deleted_occurrence_keys']); + $event = $this->eventService->updateEvent($id, $payload); + } catch (\InvalidArgumentException $e) { + return $this->error('validation_error', $e->getMessage(), 422); + } + if (!$event) { + return $this->error('not_found', 'event not found', 404); + } + if ($deletedKeysProvided) { + $this->eventService->syncDeletedOccurrenceKeys($id, $deletedKeys, true); + $event = $this->eventService->getEvent($id) ?: $event; + } + return ['data' => $event]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)', + [ + 'methods' => 'DELETE', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $ok = $this->eventService->deleteEvent($id); + if (!$ok) { + return $this->error('not_found', 'event not found', 404); + } + return ['data' => ['deleted' => true]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)/occurrences/(?P[^/]+)', + [ + 'methods' => 'DELETE', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $key = urldecode((string) $request->get_param('occurrence_key')); + $ok = $this->eventService->deleteOccurrence($id, $key); + if (!$ok) { + return $this->error('validation_error', 'invalid occurrence or event', 422); + } + return ['data' => ['deleted' => true]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)/occurrences', + [ + 'methods' => 'GET', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $from = (string) ($request->get_param('from') ?: gmdate('Y-m-d')); + $months = (int) ($request->get_param('months') ?: 3); + $items = $this->eventService->listEventOccurrences($id, $from, $months); + if ($items === null) { + return $this->error('not_found', 'event not found', 404); + } + return ['data' => $items, 'meta' => ['count' => count($items), 'from' => $from, 'months' => max(1, min($months, 24))]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/preview-occurrences', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null || !is_array($payload)) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $event = isset($payload['event']) && is_array($payload['event']) ? (array) $payload['event'] : []; + $from = (string) ($payload['from'] ?? gmdate('Y-m-d')); + $months = (int) ($payload['months'] ?? 3); + try { + $items = $this->eventService->previewOccurrences($event, $from, $months); + } catch (\InvalidArgumentException $e) { + return $this->error('validation_error', $e->getMessage(), 422); + } + return ['data' => $items, 'meta' => ['count' => count($items), 'from' => $from, 'months' => max(1, min($months, 24))]]; + }, + ] + ); + } + + private function registerPublicRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/public/events', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array { + $view = (string) ($request->get_param('view') ?: 'month'); + $date = (string) ($request->get_param('date') ?: gmdate('Y-m-d')); + $futureOnlyRaw = (string) ($request->get_param('future_only') ?? ''); + $futureOnly = in_array(strtolower($futureOnlyRaw), ['1', 'true', 'yes', 'on'], true); + $items = $this->eventService->listPublicOccurrences($view, $date, $futureOnly); + return [ + 'data' => $items, + 'meta' => ['count' => count($items), 'view' => $view, 'future_only' => $futureOnly], + ]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/public/sidebar-events', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + $items = $this->eventService->listSidebarUpcoming(14); + return [ + 'data' => $items, + 'meta' => ['count' => count($items), 'window_days' => 14], + ]; + }, + ] + ); + } + + private function registerUserRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/register', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->register( + (string) ($payload['email'] ?? ''), + (string) ($payload['password'] ?? '') + ); + if (isset($result['error'])) { + return $this->unwrapServiceResult($result); + } + + $debugTokens = $this->allowDebugTokens() && !empty($payload['debug_tokens']); + $verifyToken = (string) ($result['verify_token'] ?? ''); + $email = (string) ($result['user']['email'] ?? ''); + if ($verifyToken !== '' && $email !== '') { + $this->sendUserEmailVerification($email, $verifyToken); + } + + if (!$debugTokens) { + unset($result['verify_token']); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/verify', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->verifyEmail((string) ($payload['token'] ?? '')); + if (!isset($result['error'])) { + $verifiedEmail = (string) ($result['user']['email'] ?? ''); + if ($verifiedEmail !== '') { + $this->sendAdminApprovalRequestEmail($verifiedEmail); + } + } + return $this->unwrapServiceResult($result); + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/login', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->login( + (string) ($payload['email'] ?? ''), + (string) ($payload['password'] ?? '') + ); + if (isset($result['error'])) { + return $this->unwrapServiceResult($result); + } + $sessionToken = $this->userService->issueSessionToken((int) ($result['user']['id'] ?? 0), self::SESSION_TTL_SECONDS); + if ($sessionToken !== '') { + $this->setCalendarSessionCookie($sessionToken); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/logout', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + $token = $this->readCalendarSessionTokenFromRequest(null); + if ($token !== '') { + $this->userService->revokeSessionToken($token); + } + $this->clearCalendarSessionCookie(); + return ['data' => ['logged_out' => true]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/me', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $user = $this->resolveCalDavUserForRequest($request); + if ($user === null) { + return $this->error('auth_required', 'Login failure', 401); + } + return ['data' => $user]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/password/request', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->requestPasswordReset((string) ($payload['email'] ?? '')); + if (isset($result['error'])) { + return $this->unwrapServiceResult($result); + } + + $debugTokens = $this->allowDebugTokens() && !empty($payload['debug_tokens']); + $email = (string) ($payload['email'] ?? ''); + $token = (string) ($result['reset_token'] ?? ''); + if ($email !== '' && $token !== '') { + $this->sendPasswordResetEmail($email, $token); + } + if (!$debugTokens) { + unset($result['reset_token']); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/password/reset', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->resetPassword( + (string) ($payload['token'] ?? ''), + (string) ($payload['new_password'] ?? '') + ); + return $this->unwrapServiceResult($result); + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/admin/users', + [ + 'methods' => 'GET', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => fn(): array => ['data' => $this->userService->listUsers()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/admin/users/(?P\d+)/approve', + [ + 'methods' => 'PATCH', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $user = $this->userService->approveUser($id); + if (!$user) { + return $this->error('not_found', 'user not found', 404); + } + return ['data' => $user]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/admin/users/(?P\d+)', + [ + 'methods' => 'DELETE', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $ok = $this->userService->removeUser($id); + if (!$ok) { + return $this->error('not_found', 'user not found', 404); + } + return ['data' => ['deleted' => true]]; + }, + ] + ); + } + + private function registerIcsRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/public/ics', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + $settings = $this->settingsService->getAll(); + $calendarName = (string) ($settings['caldav_calendar_name'] ?? 'Public Calendar'); + $ics = $this->icsService->buildCalendar( + $this->eventService->listEvents(), + fn(int $eventId): array => $this->eventService->getDeletedOccurrenceKeys($eventId), + $calendarName + ); + return ['data' => $ics]; + }, + ] + ); + } + + private function registerCalDavRoutes(): void + { + $canRead = fn($request = null): bool => $this->resolveCalDavUserForRequest($request) !== null; + $canWrite = fn($request = null): bool => $this->canWriteCalendar($request); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/resources', + [ + 'methods' => 'GET', + 'permission_callback' => $canRead, + 'callback' => fn(): array => ['data' => $this->calDavService->listResources()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/object/(?P[^/]+\.ics)', + [ + 'methods' => 'GET', + 'permission_callback' => $canRead, + 'callback' => function ($request): array|\WP_Error { + $resource = urldecode((string) $request->get_param('resource')); + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + return $this->error('not_found', 'resource not found', 404); + } + return ['data' => $obj]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/object/(?P[^/]+\.ics)', + [ + 'methods' => 'PUT', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $resource = urldecode((string) $request->get_param('resource')); + $payload = $this->jsonPayload($request); + if ($payload === null || !isset($payload['ics']) || !is_string($payload['ics'])) { + return $this->error('validation_error', 'ics payload is required', 422); + } + + $ifMatch = $this->requestHeader($request, 'if-match'); + $ifNoneMatch = $this->requestHeader($request, 'if-none-match'); + $result = $this->calDavService->putObject( + $resource, + $payload['ics'], + $ifMatch !== '' ? $ifMatch : null, + $ifNoneMatch !== '' ? $ifNoneMatch : null, + (int) (($this->resolveCalDavUserForRequest($request)['id'] ?? 0)) + ); + if (isset($result['error'])) { + $error = (array) $result['error']; + return $this->error( + (string) ($error['code'] ?? 'caldav_error'), + (string) ($error['message'] ?? 'caldav operation failed'), + (int) ($error['status'] ?? 500) + ); + } + + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/object/(?P[^/]+\.ics)', + [ + 'methods' => 'DELETE', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $resource = urldecode((string) $request->get_param('resource')); + $result = $this->calDavService->deleteObject($resource); + if (isset($result['error'])) { + $error = (array) $result['error']; + return $this->error( + (string) ($error['code'] ?? 'caldav_error'), + (string) ($error['message'] ?? 'caldav operation failed'), + (int) ($error['status'] ?? 500) + ); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/multiget', + [ + 'methods' => 'POST', + 'permission_callback' => $canRead, + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null || !isset($payload['resources']) || !is_array($payload['resources'])) { + return $this->error('validation_error', 'resources array is required', 422); + } + return ['data' => $this->calDavService->multiget($payload['resources'])]; + }, + ] + ); + } + + private function serveIcsResponse(): void + { + $settings = $this->settingsService->getAll(); + if ( + (string) ($settings['ics_access_mode'] ?? 'public_read') === 'authenticated_read' + && $this->resolveCalDavUserForRequest(null) === null + ) { + http_response_code(401); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['error' => ['code' => 'auth_required', 'message' => 'authentication required']]); + return; + } + + $calendarName = (string) ($settings['caldav_calendar_name'] ?? 'Public Calendar'); + $ics = $this->icsService->buildCalendar( + $this->eventService->listEvents(), + fn(int $eventId): array => $this->eventService->getDeletedOccurrenceKeys($eventId), + $calendarName + ); + $etag = '"' . substr(sha1($ics), 0, 16) . '"'; + $lastModified = gmdate('D, d M Y H:i:s') . ' GMT'; + + http_response_code(200); + header('Content-Type: text/calendar; charset=utf-8'); + header('ETag: ' . $etag); + header('Last-Modified: ' . $lastModified); + header('Cache-Control: public, max-age=120'); + echo $ics; + } + + private function serveCalDavPath(string $path, string $method): void + { + $caldavUser = $this->resolveCalDavUserForRequest(null); + if ($caldavUser === null) { + http_response_code(401); + header('WWW-Authenticate: Basic realm="Calendar CalDAV"'); + header('Content-Type: application/xml; charset=utf-8'); + echo 'auth required'; + return; + } + + $root = rtrim($this->caldavRootPath(), '/'); + $principalCollection = $root . '/principals/'; + $principal = $principalCollection . (int) ($caldavUser['id'] ?? 0) . '/'; + $calendarsRoot = $root . '/calendars/'; + $collection = $root . '/calendars/public/'; + $resourcePrefix = $collection; + + if ($method === 'HEAD') { + if ($path === $root || $path === $root . '/' || $path === $calendarsRoot || $path === rtrim($calendarsRoot, '/') || $path === $collection || $path === rtrim($collection, '/')) { + header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD'); + header('DAV: 1, calendar-access'); + http_response_code(200); + return; + } + if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) { + $resource = basename($path); + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + http_response_code(404); + return; + } + header('Content-Type: text/calendar; charset=utf-8'); + header('ETag: ' . (string) ($obj['etag'] ?? '')); + http_response_code(200); + return; + } + } + + if ($method === 'OPTIONS') { + header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD'); + header('DAV: 1, calendar-access'); + http_response_code(200); + return; + } + + if ($method === 'PROPFIND') { + header('Content-Type: application/xml; charset=utf-8'); + http_response_code(207); + if ($path === $root || $path === $root . '/') { + echo $this->caldavPropfindRootXml($root, $principal, $calendarsRoot, $collection); + return; + } + if ($path === $principalCollection || $path === rtrim($principalCollection, '/')) { + echo $this->caldavPropfindPrincipalCollectionXml($principalCollection, $principal); + return; + } + if ($path === $principal || $path === rtrim($principal, '/')) { + echo $this->caldavPropfindPrincipalXml($principal, $calendarsRoot); + return; + } + if ($path === $calendarsRoot || $path === rtrim($calendarsRoot, '/')) { + echo $this->caldavPropfindCalendarsRootXml($calendarsRoot, $collection); + return; + } + if ($path === rtrim($collection, '/')) { + $path = $collection; + } + if ($path === $collection) { + echo $this->caldavPropfindCollectionXml($collection, $this->caldavSyncToken(), true); + return; + } + if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) { + $resource = urldecode(basename($path)); + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + http_response_code(404); + echo 'not found'; + return; + } + echo $this->caldavPropfindObjectXml($collection . $resource, (string) ($obj['etag'] ?? '')); + return; + } + http_response_code(404); + echo 'not found'; + return; + } + + if ($method === 'REPORT' && $path === $collection) { + $body = (string) file_get_contents('php://input'); + header('Content-Type: application/xml; charset=utf-8'); + http_response_code(207); + echo $this->caldavReportXml($collection, $body, $this->caldavSyncToken()); + return; + } + + if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) { + $resource = urldecode(basename($path)); + if ($method === 'GET') { + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + http_response_code(404); + return; + } + header('Content-Type: text/calendar; charset=utf-8'); + header('ETag: ' . (string) ($obj['etag'] ?? '')); + http_response_code(200); + echo (string) ($obj['ics'] ?? ''); + return; + } + if ($method === 'PUT') { + $raw = (string) file_get_contents('php://input'); + $ifMatch = trim((string) ($_SERVER['HTTP_IF_MATCH'] ?? '')); + $ifNoneMatch = trim((string) ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')); + $result = $this->calDavService->putObject( + $resource, + $raw, + $ifMatch !== '' ? $ifMatch : null, + $ifNoneMatch !== '' ? $ifNoneMatch : null, + (int) ($caldavUser['id'] ?? 0) + ); + if (isset($result['error'])) { + $error = (array) $result['error']; + http_response_code((int) ($error['status'] ?? 500)); + header('Content-Type: application/xml; charset=utf-8'); + echo '' . esc_html((string) ($error['message'] ?? 'error')) . ''; + return; + } + $status = (int) ($result['status'] ?? 204); + $event = (array) ($result['event'] ?? []); + if (!empty($event['etag'])) { + header('ETag: ' . (string) $event['etag']); + } + http_response_code($status); + return; + } + if ($method === 'DELETE') { + $result = $this->calDavService->deleteObject($resource); + if (isset($result['error'])) { + $error = (array) $result['error']; + http_response_code((int) ($error['status'] ?? 500)); + return; + } + http_response_code(204); + return; + } + } + + http_response_code(405); + header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD'); + } + + private function caldavPropfindRootXml(string $root, string $principal, string $calendarsRoot, string $collection): string + { + return '' + . '' + . '' . htmlspecialchars($root . '/', ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'CalDAV Root' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($calendarsRoot, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'Calendar Home Set' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($collection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars((string) $this->settingsService->get('caldav_calendar_name', 'Public Calendar'), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindPrincipalCollectionXml(string $principalCollection, string $principal): string + { + return '' + . '' + . '' . htmlspecialchars($principalCollection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'Principals' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindPrincipalXml(string $principal, string $calendarsRoot): string + { + return '' + . '' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars($calendarsRoot, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindCalendarsRootXml(string $calendarsRoot, string $collection): string + { + return '' + . '' + . '' . htmlspecialchars($calendarsRoot, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'Calendar Home Set' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($collection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars((string) $this->settingsService->get('caldav_calendar_name', 'Public Calendar'), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars(sha1($this->caldavSyncToken()), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . $this->caldavSupportedReportSetXml() + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindCollectionXml(string $collection, string $syncToken, bool $includeMembers = false): string + { + $xml = '' + . '' + . '' . htmlspecialchars($collection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars((string) $this->settingsService->get('caldav_calendar_name', 'Public Calendar'), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars(sha1($syncToken), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars(sha1($syncToken), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars($syncToken, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . $this->caldavSupportedReportSetXml() + . 'HTTP/1.1 200 OK'; + + if ($includeMembers) { + $resources = $this->calDavService->listResources(); + foreach ($resources as $r) { + $resource = (string) ($r['resource'] ?? ''); + if ($resource === '') { + continue; + } + $etag = (string) ($r['etag'] ?? ''); + $href = $collection . $resource; + $xml .= '' . htmlspecialchars($href, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars($etag, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'text/calendar; charset=utf-8' + . 'HTTP/1.1 200 OK'; + } + } + + return $xml . ''; + } + + private function caldavPropfindObjectXml(string $href, string $etag): string + { + return '' + . '' + . '' . htmlspecialchars($href, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars($etag, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'text/calendar; charset=utf-8' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavSupportedReportSetXml(): string + { + return '' + . '' + . '' + . '' + . ''; + } + + private function caldavReportXml(string $collection, string $xmlBody, string $syncToken): string + { + $bodyLower = strtolower($xmlBody); + $resources = array_map(static fn(array $r): string => (string) ($r['resource'] ?? ''), $this->calDavService->listResources()); + $items = []; + + if (str_contains($bodyLower, 'sync-collection')) { + $items = $this->calDavService->multiget($resources); + } elseif (str_contains($bodyLower, 'calendar-query')) { + $items = $this->calDavService->multiget($resources); + if (preg_match('/start\\s*=\\s*"([0-9TzZ]+)"/i', $xmlBody, $s) && preg_match('/end\\s*=\\s*"([0-9TzZ]+)"/i', $xmlBody, $e)) { + $start = $this->icalToIso($s[1]); + $end = $this->icalToIso($e[1]); + $items = array_values(array_filter($items, function (array $it) use ($start, $end): bool { + if ((int) ($it['status'] ?? 404) !== 200) { + return false; + } + $ics = (string) ($it['ics'] ?? ''); + $dt = $this->extractFirstDtStartIso($ics); + if ($dt === null) { + return false; + } + return $dt >= $start && $dt <= $end; + })); + } + } else { + preg_match_all('#<[^>]*href[^>]*>([^<]+)]*href>#i', $xmlBody, $matches); + $requested = array_values(array_filter(array_map(static function (string $href): string { + $trimmed = trim($href); + $path = parse_url($trimmed, PHP_URL_PATH); + $candidate = is_string($path) && $path !== '' ? $path : $trimmed; + return urldecode(basename($candidate)); + }, (array) ($matches[1] ?? [])))); + $items = $requested ? $this->calDavService->multiget($requested) : $this->calDavService->multiget($resources); + } + + $responses = ''; + foreach ($items as $item) { + $status = (int) ($item['status'] ?? 404); + $resource = (string) ($item['resource'] ?? ''); + $responses .= '' . htmlspecialchars($collection . $resource, ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''; + if ($status === 200) { + $responses .= '' . htmlspecialchars((string) ($item['etag'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars((string) ($item['ics'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''; + } + $responses .= 'HTTP/1.1 ' . $status . ($status === 200 ? ' OK' : ' Not Found') + . ''; + } + return '' + . htmlspecialchars($syncToken, ENT_XML1 | ENT_QUOTES, 'UTF-8') + . '' + . $responses + . ''; + } + + private function caldavSyncToken(): string + { + $rows = $this->calDavService->listResources(); + $seed = ''; + foreach ($rows as $row) { + $seed .= (string) ($row['resource'] ?? '') . '|' . (string) ($row['etag'] ?? '') . ';'; + } + return 'urn:calendar-plugin:sync:' . sha1($seed); + } + + private function icalToIso(string $value): string + { + $value = trim($value); + if (preg_match('/^\\d{8}T\\d{6}Z$/', $value)) { + $dt = \DateTimeImmutable::createFromFormat('Ymd\\THis\\Z', $value, new \DateTimeZone('UTC')); + if ($dt instanceof \DateTimeImmutable) { + return $dt->setTimezone(new \DateTimeZone('Europe/London'))->format('c'); + } + } + if (preg_match('/^\\d{8}T\\d{6}$/', $value)) { + $dt = \DateTimeImmutable::createFromFormat('Ymd\\THis', $value, new \DateTimeZone('Europe/London')); + if ($dt instanceof \DateTimeImmutable) { + return $dt->format('c'); + } + } + return '1970-01-01T00:00:00+00:00'; + } + + private function extractFirstDtStartIso(string $ics): ?string + { + if (!preg_match('/^DTSTART(?:;[^:]+)?:([0-9TzZ]+)$/mi', $ics, $m)) { + return null; + } + return $this->icalToIso((string) $m[1]); + } + + private function unwrapServiceResult(array $result): array|\WP_Error + { + if (isset($result['error']) && is_array($result['error'])) { + $error = (array) $result['error']; + return $this->error( + (string) ($error['code'] ?? 'service_error'), + (string) ($error['message'] ?? 'request failed'), + (int) ($error['status'] ?? 500) + ); + } + return ['data' => $result]; + } + + private function collectDiagnosticsSnapshot(bool $enabled): array + { + $diag = [ + 'diagnostics_enabled' => $enabled, + 'generated_at_utc' => gmdate('c'), + 'db_prefix' => $this->db->getPrefix(), + 'table_stem' => $this->tableStem, + 'ics_path' => $this->icsPath(), + 'caldav_root' => $this->caldavRootPath(), + 'current_user_id' => $this->auth->currentUserId(), + 'current_user_email' => $this->auth->currentUserEmail(), + ]; + if (!$enabled) { + $diag['note'] = 'Diagnostics collection is disabled in Setup.'; + return $diag; + } + + $auditTable = $this->db->getPrefix() . trim($this->tableStem, '_') . '_audit_log'; + $sql = $this->db->prepare( + "SELECT actor, action, target, result, created_at, context_json FROM {$auditTable} ORDER BY id DESC LIMIT %d", + 200 + ); + $rows = $this->db->getResults($sql); + $diag['audit_log_recent'] = array_map( + static function (object $row): array { + return [ + 'actor' => (string) ($row->actor ?? ''), + 'action' => (string) ($row->action ?? ''), + 'target' => (string) ($row->target ?? ''), + 'result' => (string) ($row->result ?? ''), + 'created_at' => (string) ($row->created_at ?? ''), + 'context_json' => (string) ($row->context_json ?? ''), + ]; + }, + $rows + ); + $diag['audit_log_count'] = count($diag['audit_log_recent']); + return $diag; + } + + private function updatePluginTablePrefix(string $currentStem, string $requestedPrefix): array + { + $dbPrefix = (string) $this->db->getPrefix(); + if (!str_starts_with($requestedPrefix, $dbPrefix)) { + return ['ok' => false, 'message' => 'Table prefix must start with WordPress DB prefix: ' . $dbPrefix]; + } + $newStem = trim(substr($requestedPrefix, strlen($dbPrefix)), '_'); + if ($newStem === '') { + return ['ok' => false, 'message' => 'Invalid table prefix.']; + } + if ($newStem === $currentStem) { + return ['ok' => true, 'stem' => $currentStem]; + } + + $suffixes = ['events', 'recurrence_exceptions', 'users', 'user_tokens', 'audit_log']; + foreach ($suffixes as $suffix) { + $from = $dbPrefix . $currentStem . '_' . $suffix; + $to = $dbPrefix . $newStem . '_' . $suffix; + $existsFrom = $this->db->getResults($this->db->prepare('SHOW TABLES LIKE %s', $from)); + if (count($existsFrom) === 0) { + continue; + } + $existsTo = $this->db->getResults($this->db->prepare('SHOW TABLES LIKE %s', $to)); + if (count($existsTo) > 0) { + return ['ok' => false, 'message' => 'Target table already exists: ' . $to]; + } + $this->db->query(sprintf('RENAME TABLE `%s` TO `%s`', $from, $to)); + } + update_option('calendar_plugin_table_stem', $newStem); + return ['ok' => true, 'stem' => $newStem]; + } + + private function adminPageShell(string $title, string $bodyHtml): string + { + $nav = '

Calendar Plugin

'; + return '

' . esc_html($title) . '

' + . '
' . $nav + . '
' . $bodyHtml . '
'; + } + + private function slugPrefix(): string + { + $slug = trim((string) ($this->settingsService->get('url_slug', '') ?? ''), '/'); + return $slug === '' ? '' : ('/' . $slug); + } + + private function icsPath(): string + { + return $this->slugPrefix() . '/calendar.ics'; + } + + private function caldavRootPath(): string + { + return $this->slugPrefix() . '/caldav/'; + } + + private function canWriteCalendar(mixed $request = null): bool + { + if ($this->auth->currentUserCan('edit_posts')) { + return true; + } + return $this->resolveCalDavUserForRequest($request) !== null; + } + + private static function resolveTableStem(): string + { + $stem = 'cs_calendar'; + $saved = ''; + if (function_exists('get_option')) { + $saved = (string) get_option('calendar_plugin_table_stem', ''); + if ($saved !== '') { + $stem = $saved; + } + } + if (defined('CALENDAR_PLUGIN_TABLE_STEM')) { + $stem = (string) CALENDAR_PLUGIN_TABLE_STEM; + } + if (function_exists('apply_filters')) { + /** @var mixed $filtered */ + $filtered = apply_filters('calendar_plugin_table_stem', $stem); + if (is_string($filtered)) { + $stem = $filtered; + } + } + $stem = strtolower(trim($stem)); + $stem = preg_replace('/[^a-z0-9_]/', '', $stem) ?: 'calendar'; + $stem = trim($stem, '_') ?: 'calendar'; + if ($saved === '') { + $detected = self::detectExistingStem(); + if ($detected !== '') { + return $detected; + } + } + return $stem; + } + + private static function detectExistingStem(): string + { + global $wpdb; + if (!isset($wpdb) || !is_object($wpdb) || !property_exists($wpdb, 'prefix')) { + return ''; + } + $prefix = (string) $wpdb->prefix; + $candidates = ['cs_calendar', 'calendar']; + foreach ($candidates as $candidate) { + $table = $prefix . $candidate . '_events'; + $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); + if (is_string($exists) && $exists !== '') { + return $candidate; + } + } + return ''; + } + + private function resolveCalDavUserForRequest(mixed $request = null): ?array + { + if ($this->auth->currentUserId() > 0) { + return [ + 'id' => $this->auth->currentUserId(), + 'email' => $this->auth->currentUserEmail(), + 'source' => 'wp', + ]; + } + + $sessionToken = $this->readCalendarSessionTokenFromRequest($request); + if ($sessionToken !== '') { + $sessionUser = $this->userService->authenticateSessionToken($sessionToken); + if ($sessionUser !== null) { + $sessionUser['source'] = 'calendar_session'; + return $sessionUser; + } + } + + [$email, $password] = $this->readBasicAuthCredentialsFromRequest($request); + if ($email === '' || $password === '') { + return null; + } + $user = $this->userService->authenticateActiveUserCredentials($email, $password); + if (!$user) { + return null; + } + $user['source'] = 'calendar_user'; + return $user; + } + + private function readBasicAuthCredentialsFromRequest(mixed $request = null): array + { + $header = ''; + if (is_object($request) && method_exists($request, 'get_header')) { + $header = (string) $request->get_header('authorization'); + } + if ($header === '') { + $header = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? ''); + } + if ($header === '' && isset($_SERVER['PHP_AUTH_USER'])) { + return [(string) ($_SERVER['PHP_AUTH_USER'] ?? ''), (string) ($_SERVER['PHP_AUTH_PW'] ?? '')]; + } + if ($header === '' || !str_starts_with(strtolower($header), 'basic ')) { + return ['', '']; + } + $decoded = base64_decode(substr($header, 6), true); + if (!is_string($decoded) || !str_contains($decoded, ':')) { + return ['', '']; + } + [$u, $p] = explode(':', $decoded, 2); + return [(string) $u, (string) $p]; + } + + private function readCalendarSessionTokenFromRequest(mixed $request = null): string + { + $name = self::SESSION_COOKIE; + if (isset($_COOKIE[$name]) && is_string($_COOKIE[$name])) { + return trim((string) $_COOKIE[$name]); + } + + $cookieHeader = ''; + if (is_object($request) && method_exists($request, 'get_header')) { + $cookieHeader = (string) $request->get_header('cookie'); + } + if ($cookieHeader === '') { + $cookieHeader = (string) ($_SERVER['HTTP_COOKIE'] ?? ''); + } + if ($cookieHeader === '') { + return ''; + } + + foreach (explode(';', $cookieHeader) as $pair) { + $parts = explode('=', trim($pair), 2); + if (count($parts) !== 2) { + continue; + } + if (trim((string) $parts[0]) !== $name) { + continue; + } + return trim((string) $parts[1]); + } + + return ''; + } + + private function setCalendarSessionCookie(string $token): void + { + if ($token === '') { + return; + } + $secure = $this->isHttpsRequest(); + setcookie(self::SESSION_COOKIE, $token, [ + 'expires' => time() + self::SESSION_TTL_SECONDS, + 'path' => '/', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } + + private function clearCalendarSessionCookie(): void + { + $secure = $this->isHttpsRequest(); + setcookie(self::SESSION_COOKIE, '', [ + 'expires' => time() - 3600, + 'path' => '/', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } + + private function isHttpsRequest(): bool + { + if (!empty($_SERVER['HTTPS']) && (string) $_SERVER['HTTPS'] !== 'off') { + return true; + } + $forwardedProto = strtolower(trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))); + if ($forwardedProto !== '') { + foreach (explode(',', $forwardedProto) as $proto) { + if (trim($proto) === 'https') { + return true; + } + } + } + if (function_exists('home_url')) { + $home = (string) home_url('/'); + if (str_starts_with(strtolower($home), 'https://')) { + return true; + } + } + return false; + } + + private function allowDebugTokens(): bool + { + return defined('CALENDAR_PLUGIN_ALLOW_DEBUG_TOKENS') && CALENDAR_PLUGIN_ALLOW_DEBUG_TOKENS === true; + } + + private function sendUserEmailVerification(string $email, string $token): void + { + if (!function_exists('wp_mail')) { + return; + } + $verifyUrl = (string) home_url($this->verificationPagePath() . '?calendar_verify_token=' . rawurlencode($token)); + @wp_mail( + $email, + 'Verify your calendar account', + "Please verify your calendar account using this link:\n\n{$verifyUrl}\n\nVerification token (for copy/paste):\n{$token}\n" + ); + } + + private function verificationPagePath(): string + { + $path = trim((string) ($this->settingsService->get('verification_page_path', '/calendar') ?? '/calendar')); + if ($path === '') { + $path = '/calendar'; + } + if (!str_starts_with($path, '/')) { + $path = '/' . $path; + } + return '/' . trim($path, '/'); + } + + private function sendPasswordResetEmail(string $email, string $token): void + { + if (!function_exists('wp_mail')) { + return; + } + $resetUrl = (string) home_url('/?calendar_reset_token=' . rawurlencode($token)); + @wp_mail( + $email, + 'Calendar password reset', + "A password reset was requested for your calendar account.\n\nReset link:\n{$resetUrl}\n" + ); + } + + private function sendAdminApprovalRequestEmail(string $email): void + { + if (!function_exists('wp_mail') || !function_exists('get_option')) { + return; + } + $adminEmail = (string) get_option('admin_email', ''); + if ($adminEmail === '') { + return; + } + $url = (string) admin_url('admin.php?page=calendar-plugin-users'); + @wp_mail( + $adminEmail, + 'Calendar user pending approval', + "A new user is pending approval: {$email}\nReview: {$url}\n" + ); + } + + private function jsonPayload($request): ?array + { + if (!is_object($request) || !method_exists($request, 'get_json_params')) { + return null; + } + $payload = $request->get_json_params(); + return is_array($payload) ? $payload : []; + } + + private function requestHeader(object $request, string $name): string + { + if (method_exists($request, 'get_header')) { + $value = $request->get_header($name); + return is_string($value) ? trim($value) : ''; + } + return ''; + } + + private function error(string $code, string $message, int $status): \WP_Error + { + return new \WP_Error($code, $message, ['status' => $status]); + } +} diff --git a/code/src/bootstrap.php b/code/src/bootstrap.php new file mode 100644 index 0000000..9fa19ca --- /dev/null +++ b/code/src/bootstrap.php @@ -0,0 +1,18 @@ +prefix; + +$tables = [ + $prefix . $stem . '_events', + $prefix . $stem . '_recurrence_exceptions', + $prefix . $stem . '_users', + $prefix . $stem . '_user_tokens', + $prefix . $stem . '_audit_log', +]; + +foreach ($tables as $table) { + $wpdb->query("DROP TABLE IF EXISTS `{$table}`"); +} + +$options = [ + 'calendar_plugin_caldav_calendar_name', + 'calendar_plugin_url_slug', + 'calendar_plugin_ics_access_mode', + 'calendar_plugin_diagnostics_enabled', + 'calendar_plugin_uninstall_cleanup_mode', + 'calendar_plugin_table_stem', + 'calendar_plugin_schema_version', +]; +foreach ($options as $optionName) { + delete_option($optionName); +} diff --git a/compatibility-layer/README.md b/compatibility-layer/README.md new file mode 100644 index 0000000..faaa2fa --- /dev/null +++ b/compatibility-layer/README.md @@ -0,0 +1,26 @@ +# Compatibility Layer + +This directory is the canonical local WordPress emulation entrypoint. + +Purpose: +- run the stand-alone harness used for local deterministic testing +- keep emulation tooling out of deployable plugin code (`code/`) + +Current implementation delegates to the existing harness implementation under `fixture/`. + +Primary commands from repository root: + +```bash +./compatibility-layer/reset.sh +./compatibility-layer/seed.sh +./compatibility-layer/run.sh +``` + +Optional archived local checks (fixture-era): + +```bash +./compatibility-layer/smoke.sh +./compatibility-layer/security_smoke.sh +``` + +These delegate to scripts under `fixture-tests/`. Active development validation now targets the remote server first. diff --git a/compatibility-layer/__pycache__/server.cpython-313.pyc b/compatibility-layer/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000..97aa4c3 Binary files /dev/null and b/compatibility-layer/__pycache__/server.cpython-313.pyc differ diff --git a/compatibility-layer/caldav_client_compat_smoke.sh b/compatibility-layer/caldav_client_compat_smoke.sh new file mode 100755 index 0000000..f1ac725 --- /dev/null +++ b/compatibility-layer/caldav_client_compat_smoke.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +"$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/../fixture-tests/fixture_caldav_client_compat_smoke.sh" "$@" diff --git a/compatibility-layer/e2e_wp_emulation.php b/compatibility-layer/e2e_wp_emulation.php new file mode 100755 index 0000000..471c044 --- /dev/null +++ b/compatibility-layer/e2e_wp_emulation.php @@ -0,0 +1,346 @@ +#!/usr/bin/env php +data['status'] ?? 500; + fwrite(STDERR, "[FAIL] {$context}: {$res->code} {$res->message} ({$status})\n"); + exit(1); + } + return $res; +} + +require __DIR__ . '/wp_emulation.php'; + +$health = unwrap(request('GET', '/calendar/v1/health'), 'health'); +assert_true(($health['status'] ?? '') === 'ok', 'health status should be ok'); + +$GLOBALS['wp_user'] = [ + 'id' => 0, + 'email' => '', + 'caps' => [], +]; +$unauthCreate = request('POST', '/calendar/v1/events', [ + 'title' => 'No Auth Event', + 'start_datetime' => '2026-05-01T09:00:00+01:00', + 'end_datetime' => '2026-05-01T10:00:00+01:00', +]); +assert_true($unauthCreate instanceof WP_Error, 'unauthenticated event create should be denied'); +assert_true(($unauthCreate->data['status'] ?? 0) === 403, 'unauthenticated event create should return 403'); + +$GLOBALS['wp_user'] = [ + 'id' => 1, + 'email' => 'admin@example.test', + 'caps' => ['manage_options', 'edit_posts'], +]; + +$settingsPatch = unwrap( + request('PATCH', '/calendar/v1/settings', ['caldav_calendar_name' => 'Calendar E2E']), + 'patch settings' +); +assert_true(($settingsPatch['data']['caldav_calendar_name'] ?? '') === 'Calendar E2E', 'settings patch must persist'); + +$settingsGet = unwrap(request('GET', '/calendar/v1/settings'), 'get settings'); +assert_true(($settingsGet['data']['caldav_calendar_name'] ?? '') === 'Calendar E2E', 'settings get should reflect patch'); + +$register = unwrap( + request('POST', '/calendar/v1/users/register', ['email' => 'demo@example.test', 'password' => 'pass12345', 'debug_tokens' => true]), + 'register user' +); +$verifyToken = (string) ($register['data']['verify_token'] ?? ''); +assert_true($verifyToken !== '', 'register should issue verify token'); +$userId = (int) (($register['data']['user']['id'] ?? 0)); +assert_true($userId > 0, 'register should create user id'); + +$loginPending = request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'pass12345']); +assert_true($loginPending instanceof WP_Error, 'pending user login should fail'); +assert_true(($loginPending->data['status'] ?? 0) === 401, 'pending user login should return 401'); + +$verified = unwrap(request('POST', '/calendar/v1/users/verify', ['token' => $verifyToken]), 'verify email'); +assert_true(str_contains((string) ($verified['data']['message'] ?? ''), 'admin will review'), 'verify should return approval message'); + +$approved = unwrap(request('PATCH', '/calendar/v1/admin/users/' . $userId . '/approve', []), 'approve user'); +assert_true((string) ($approved['data']['account_status'] ?? '') === 'active', 'approved user status should be active'); + +$loginActive = unwrap( + request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'pass12345']), + 'active user login' +); +assert_true((bool) (($loginActive['data']['ok'] ?? false) === true), 'active user login should succeed'); + +$resetReq = unwrap( + request('POST', '/calendar/v1/users/password/request', ['email' => 'demo@example.test', 'debug_tokens' => true]), + 'request reset' +); +$resetToken = (string) ($resetReq['data']['reset_token'] ?? ''); +assert_true($resetToken !== '', 'password reset request should return reset token'); + +$resetDone = unwrap( + request('POST', '/calendar/v1/users/password/reset', ['token' => $resetToken, 'new_password' => 'newpass123']), + 'reset password' +); +assert_true((bool) (($resetDone['data']['ok'] ?? false) === true), 'password reset should succeed'); + +$loginNewPass = unwrap( + request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'newpass123']), + 'login with new password' +); +assert_true((bool) (($loginNewPass['data']['ok'] ?? false) === true), 'login should work after password reset'); + +$resetReuse = request('POST', '/calendar/v1/users/password/reset', ['token' => $resetToken, 'new_password' => 'anotherpass123']); +assert_true($resetReuse instanceof WP_Error, 'password reset token should be single-use'); +assert_true(($resetReuse->data['status'] ?? 0) === 422, 'reused reset token should return 422'); + +$usersList = unwrap(request('GET', '/calendar/v1/admin/users'), 'list users'); +assert_true(count((array) ($usersList['data'] ?? [])) >= 1, 'admin users list should return at least one row'); + +$removeUser = unwrap(request('DELETE', '/calendar/v1/admin/users/' . $userId), 'remove user'); +assert_true((bool) (($removeUser['data']['deleted'] ?? false) === true), 'admin remove user should succeed'); +$loginRemoved = request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'newpass123']); +assert_true($loginRemoved instanceof WP_Error, 'removed user login should fail'); +assert_true(($loginRemoved->data['status'] ?? 0) === 401, 'removed user login should return 401'); + +$created = unwrap( + request('POST', '/calendar/v1/events', [ + 'title' => 'Emulated Recurring Event', + 'description' => 'e2e', + 'location' => 'Room 1', + 'category' => 'Test', + 'all_day_event' => false, + 'start_datetime' => '2026-04-01T10:00:00+01:00', + 'end_datetime' => '2026-04-01T11:00:00+01:00', + 'repeat_type' => 'daily', + 'repeat_interval' => 1, + 'repeat_range_mode' => 'count', + 'repeat_count' => 3, + ]), + 'create event' +); +$eventId = (int) ($created['data']['id'] ?? 0); +assert_true($eventId > 0, 'event id should be generated'); + +$invalidEvent = request('POST', '/calendar/v1/events', [ + 'title' => 'Bad Range', + 'start_datetime' => '2026-04-12T12:00:00+01:00', + 'end_datetime' => '2026-04-12T11:00:00+01:00', +]); +assert_true($invalidEvent instanceof WP_Error, 'invalid event with end before start should fail'); +assert_true(($invalidEvent->data['status'] ?? 0) === 422, 'invalid event should return 422'); + +$public = unwrap(request('GET', '/calendar/v1/public/events', ['view' => 'month', 'date' => '2026-04-01']), 'public events'); +$countBefore = (int) ($public['meta']['count'] ?? 0); +assert_true($countBefore >= 3, 'public month view should include recurrence occurrences'); + +$preview = unwrap( + request('POST', '/calendar/v1/events/preview-occurrences', [ + 'event' => [ + 'title' => 'Preview Event', + 'start_datetime' => '2026-04-05T08:00:00+01:00', + 'end_datetime' => '2026-04-05T09:00:00+01:00', + 'repeat_type' => 'weekly', + 'repeat_interval' => 1, + 'repeat_range_mode' => 'count', + 'repeat_count' => 4, + ], + 'from' => '2026-04-01', + 'months' => 1, + ]), + 'preview occurrences' +); +assert_true((int) ($preview['meta']['count'] ?? 0) >= 1, 'preview occurrences should return generated items'); + +$previewBad = request('POST', '/calendar/v1/events/preview-occurrences', [ + 'event' => [ + 'title' => 'Bad Preview', + 'start_datetime' => '2026-04-05T10:00:00+01:00', + 'end_datetime' => '2026-04-05T09:00:00+01:00', + 'repeat_type' => 'weekly', + ], + 'from' => '2026-04-01', + 'months' => 1, +]); +assert_true($previewBad instanceof WP_Error, 'preview occurrences should validate invalid event range'); +assert_true(($previewBad->data['status'] ?? 0) === 422, 'invalid preview payload should return 422'); + +$deleteOne = unwrap( + request('DELETE', '/calendar/v1/events/' . $eventId . '/occurrences/' . rawurlencode('2026-04-02T10:00:00+01:00')), + 'delete occurrence' +); +assert_true(($deleteOne['data']['deleted'] ?? false) === true, 'delete occurrence should succeed'); + +$deleteSameAgain = unwrap( + request('DELETE', '/calendar/v1/events/' . $eventId . '/occurrences/' . rawurlencode('2026-04-02T10:00:00+01:00')), + 'delete same occurrence again' +); +assert_true(($deleteSameAgain['data']['deleted'] ?? false) === true, 'repeat delete occurrence should be idempotent success'); + +$occurrences = unwrap( + request('GET', '/calendar/v1/events/' . $eventId . '/occurrences', ['from' => '2026-04-01', 'months' => 1]), + 'event occurrences' +); +assert_true((int) ($occurrences['meta']['count'] ?? 0) >= 2, 'event occurrences endpoint should return remaining recurrence instances'); + +$publicAfter = unwrap(request('GET', '/calendar/v1/public/events', ['view' => 'month', 'date' => '2026-04-01']), 'public events after delete'); +$days = []; +foreach (($publicAfter['data'] ?? []) as $item) { + $days[] = substr((string) ($item['occurrence_start'] ?? ''), 0, 10); +} +assert_true(!in_array('2026-04-02', $days, true), 'deleted single occurrence should be excluded'); + +$ics = unwrap(request('GET', '/calendar/v1/public/ics'), 'public ics'); +$icsBody = (string) ($ics['data'] ?? ''); +assert_true(str_contains($icsBody, 'BEGIN:VCALENDAR'), 'ics response should include vcalendar envelope'); +assert_true(str_contains($icsBody, 'RRULE:'), 'ics response should include recurrence rule'); +assert_true(str_contains($icsBody, 'EXDATE'), 'ics response should include deleted occurrence as exdate'); + +$monthlyOrdinal = unwrap( + request('POST', '/calendar/v1/events', [ + 'title' => 'Monthly Ordinal', + 'description' => 'monthly weekday parity', + 'location' => 'Room 2', + 'category' => 'Monthly', + 'all_day_event' => false, + 'start_datetime' => '2026-04-05T09:30:00+01:00', + 'end_datetime' => '2026-04-05T10:30:00+01:00', + 'repeat_type' => 'monthly', + 'repeat_interval' => 1, + 'repeat_nth_mode' => 'weekday_of_month', + 'repeat_nth_pos' => -1, + 'repeat_nth_weekday' => 0, + 'repeat_range_mode' => 'count', + 'repeat_count' => 3, + ]), + 'create monthly ordinal event' +); +$monthlyEventId = (int) ($monthlyOrdinal['data']['id'] ?? 0); +assert_true($monthlyEventId > 0, 'monthly ordinal event id should be generated'); +$monthlyOcc = unwrap( + request('GET', '/calendar/v1/events/' . $monthlyEventId . '/occurrences', ['from' => '2026-04-01', 'months' => 3]), + 'list monthly ordinal occurrences' +); +$monthlyDays = array_map( + static fn(array $row): string => substr((string) ($row['occurrence_start'] ?? ''), 0, 10), + (array) ($monthlyOcc['data'] ?? []) +); +assert_true(in_array('2026-04-26', $monthlyDays, true), 'monthly ordinal should include last Sunday of April 2026'); +assert_true(in_array('2026-05-31', $monthlyDays, true), 'monthly ordinal should include last Sunday of May 2026'); + +$resources = unwrap(request('GET', '/calendar/v1/caldav/resources'), 'list caldav resources'); +$resourceList = (array) ($resources['data'] ?? []); +assert_true(count($resourceList) >= 1, 'caldav resources should list at least one object'); +$firstResource = (string) ($resourceList[0]['resource'] ?? ''); +assert_true($firstResource !== '', 'caldav resource should have object name'); + +$getObject = unwrap(request('GET', '/calendar/v1/caldav/object/' . rawurlencode($firstResource)), 'get caldav object'); +$firstEtag = (string) ($getObject['data']['etag'] ?? ''); +assert_true($firstEtag !== '', 'caldav object should include etag'); + +$newResource = 'new-fixture-event.ics'; +$newIcs = implode("\r\n", [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Calendar Plugin E2E//EN', + 'BEGIN:VEVENT', + 'UID:new-fixture-event@calendar-plugin', + 'SUMMARY:Fixture PUT Event', + 'DESCRIPTION:Created via CalDAV PUT', + 'DTSTART;TZID=Europe/London:20260410T093000', + 'DTEND;TZID=Europe/London:20260410T103000', + 'END:VEVENT', + 'END:VCALENDAR', + '', +]); +$createdViaPut = unwrap( + request('PUT', '/calendar/v1/caldav/object/' . $newResource, ['ics' => $newIcs], ['If-None-Match' => '*']), + 'caldav put create' +); +assert_true((bool) (($createdViaPut['data']['created'] ?? false) === true), 'caldav put should create object with if-none-match'); + +$duplicateBlocked = request( + 'PUT', + '/calendar/v1/caldav/object/' . $newResource, + ['ics' => $newIcs], + ['If-None-Match' => '*'] +); +assert_true($duplicateBlocked instanceof WP_Error, 'duplicate create should fail on if-none-match precondition'); +assert_true(($duplicateBlocked->data['status'] ?? 0) === 412, 'duplicate create should return 412'); + +$updatedIcs = implode("\r\n", [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Calendar Plugin E2E//EN', + 'BEGIN:VEVENT', + 'UID:new-fixture-event@calendar-plugin', + 'SUMMARY:Fixture PUT Event Updated', + 'DESCRIPTION:Updated via CalDAV PUT', + 'DTSTART;TZID=Europe/London:20260410T100000', + 'DTEND;TZID=Europe/London:20260410T110000', + 'END:VEVENT', + 'END:VCALENDAR', + '', +]); +$staleUpdate = request( + 'PUT', + '/calendar/v1/caldav/object/' . $newResource, + ['ics' => $updatedIcs], + ['If-Match' => '"stale-etag"'] +); +assert_true($staleUpdate instanceof WP_Error, 'stale etag update should fail with precondition error'); +assert_true(($staleUpdate->data['status'] ?? 0) === 412, 'stale etag update should return 412'); + +$currentObj = unwrap(request('GET', '/calendar/v1/caldav/object/' . $newResource), 'get new resource object'); +$currentEtag = (string) ($currentObj['data']['etag'] ?? ''); +assert_true($currentEtag !== '', 'newly created object should have etag'); + +$updatedOk = unwrap( + request('PUT', '/calendar/v1/caldav/object/' . $newResource, ['ics' => $updatedIcs], ['If-Match' => $currentEtag]), + 'caldav put update with etag' +); +assert_true((bool) (($updatedOk['data']['created'] ?? true) === false), 'caldav put with if-match should update existing object'); + +$multi = unwrap( + request('POST', '/calendar/v1/caldav/multiget', ['resources' => [$newResource, 'missing-event.ics']]), + 'caldav multiget' +); +$multiData = (array) ($multi['data'] ?? []); +assert_true(count($multiData) === 2, 'multiget should return one row per requested resource'); +assert_true((int) ($multiData[0]['status'] ?? 0) === 200, 'multiget should return 200 for existing resource'); +assert_true((int) ($multiData[1]['status'] ?? 0) === 404, 'multiget should return 404 for missing resource'); + +$deletedObj = unwrap(request('DELETE', '/calendar/v1/caldav/object/' . $newResource), 'caldav delete object'); +assert_true((bool) (($deletedObj['data']['deleted'] ?? false) === true), 'caldav delete should remove object'); + +$deletedNotFound = request('GET', '/calendar/v1/caldav/object/' . $newResource); +assert_true($deletedNotFound instanceof WP_Error, 'deleted caldav object should no longer resolve'); +assert_true(($deletedNotFound->data['status'] ?? 0) === 404, 'deleted caldav object should return 404'); + +$sluggedSettings = unwrap( + request('PATCH', '/calendar/v1/settings', ['url_slug' => 'events']), + 'patch slug setting' +); +assert_true(($sluggedSettings['data']['url_slug'] ?? '') === 'events', 'url_slug setting should persist'); +$sluggedCalendarHtml = do_shortcode('[calendar]'); +assert_true(str_contains($sluggedCalendarHtml, 'href="/events/calendar.ics"'), 'calendar shortcode should use slugged ICS path'); +assert_true(str_contains($sluggedCalendarHtml, 'href="/events/caldav/"'), 'calendar shortcode should use slugged CalDAV path'); + +$sidebarHtml = do_shortcode('[calendar_sidebar_upcoming]'); +assert_true(str_contains($sidebarHtml, '
data['status'] ?? 500; + fwrite(STDERR, "[FAIL] {$context}: {$res->code} {$res->message} ({$status})\n"); + exit(1); + } + return $res; +} + +require __DIR__ . '/wp_emulation.php'; + +$html = do_shortcode('[calendar]'); +assert_true(str_contains($html, 'cp-login-btn'), 'calendar shortcode should render login button'); +assert_true(str_contains($html, 'cp-create-btn'), 'calendar shortcode should render create button'); +assert_true(str_contains($html, 'cp-public-list'), 'calendar shortcode should render public list'); +assert_true(str_contains($html, 'cp-verify-btn'), 'calendar shortcode should render verify action'); +assert_true(str_contains($html, 'cp-reset-request-btn'), 'calendar shortcode should render reset action'); +assert_true(str_contains($html, 'cp-future-only'), 'calendar shortcode should render future-only control'); +assert_true(str_contains($html, 'cp-theme'), 'calendar shortcode should render theme selector'); +assert_true(str_contains($html, 'cp-delete-occ-confirm-btn'), 'calendar shortcode should render delete occurrence control'); + +$reg = unwrap(request('POST', '/calendar/v1/users/register', [ + 'email' => 'ui-emu@example.test', + 'password' => 'pass12345', + 'debug_tokens' => true, +]), 'register'); +$verify = (string) ($reg['data']['verify_token'] ?? ''); +$userId = (int) ($reg['data']['user']['id'] ?? 0); +assert_true($verify !== '' && $userId > 0, 'registration should provide token/id in debug mode'); + +unwrap(request('POST', '/calendar/v1/users/verify', ['token' => $verify]), 'verify'); +unwrap(request('PATCH', '/calendar/v1/admin/users/' . $userId . '/approve', []), 'approve'); + +$GLOBALS['wp_user'] = [ + 'id' => 0, + 'email' => '', + 'caps' => [], +]; + +$authHeader = 'Basic ' . base64_encode('ui-emu@example.test:pass12345'); +$me = unwrap(request('GET', '/calendar/v1/users/me', [], ['Authorization' => $authHeader]), 'users me'); +assert_true((string) ($me['data']['email'] ?? '') === 'ui-emu@example.test', 'me endpoint should resolve basic user'); + +$created = unwrap(request('POST', '/calendar/v1/events', [ + 'title' => 'UI Emu Event', + 'description' => 'ui flow', + 'location' => 'Desk', + 'category' => 'Test', + 'all_day_event' => false, + 'start_datetime' => '2026-06-01T10:00:00+01:00', + 'end_datetime' => '2026-06-01T11:00:00+01:00', + 'repeat_type' => 'daily', + 'repeat_interval' => 1, + 'repeat_range_mode' => 'count', + 'repeat_count' => 3, +], ['Authorization' => $authHeader]), 'create event'); +$eventId = (int) ($created['data']['id'] ?? 0); +assert_true($eventId > 0, 'basic-auth user should be able to create event'); + +unwrap(request('DELETE', '/calendar/v1/events/' . $eventId . '/occurrences/' . rawurlencode('2026-06-02T10:00:00+01:00'), [], ['Authorization' => $authHeader]), 'delete occurrence'); +$occ = unwrap(request('GET', '/calendar/v1/events/' . $eventId . '/occurrences', ['from' => '2026-06-01', 'months' => 1], ['Authorization' => $authHeader]), 'list occurrences'); +$days = []; +foreach ((array) ($occ['data'] ?? []) as $row) { + $days[] = substr((string) ($row['occurrence_start'] ?? ''), 0, 10); +} +assert_true(!in_array('2026-06-02', $days, true), 'deleted occurrence should not remain'); + +unwrap(request('DELETE', '/calendar/v1/events/' . $eventId, [], ['Authorization' => $authHeader]), 'delete event'); +$deletedEvent = request('GET', '/calendar/v1/events/' . $eventId, [], ['Authorization' => $authHeader]); +assert_true($deletedEvent instanceof WP_Error, 'deleted event should not resolve'); +assert_true(($deletedEvent->data['status'] ?? 0) === 404, 'deleted event fetch should return 404'); + +$GLOBALS['wp_user'] = [ + 'id' => 1, + 'email' => 'admin@example.test', + 'caps' => ['manage_options', 'edit_posts'], +]; +unwrap(request('DELETE', '/calendar/v1/admin/users/' . $userId, []), 'remove user'); +$GLOBALS['wp_user'] = [ + 'id' => 0, + 'email' => '', + 'caps' => [], +]; +$meAfterRemove = request('GET', '/calendar/v1/users/me', [], ['Authorization' => $authHeader]); +assert_true($meAfterRemove instanceof WP_Error, 'removed user should not authenticate'); +assert_true(($meAfterRemove->data['status'] ?? 0) === 401, 'removed user me endpoint should return 401'); + +echo "[PASS] ui-e2e wp emulation\n"; diff --git a/compatibility-layer/wp-emu-root/wp-admin/includes/upgrade.php b/compatibility-layer/wp-emu-root/wp-admin/includes/upgrade.php new file mode 100644 index 0000000..7f37a7d --- /dev/null +++ b/compatibility-layer/wp-emu-root/wp-admin/includes/upgrade.php @@ -0,0 +1,2 @@ +params = $jsonParams; + foreach ($headers as $k => $v) { + $this->headers[strtolower((string) $k)] = (string) $v; + } + } + + public function get_method(): string + { + return $this->method; + } + + public function get_route(): string + { + return $this->route; + } + + public function get_json_params(): array + { + return $this->jsonParams; + } + + public function set_param(string $key, mixed $value): void + { + $this->params[$key] = $value; + } + + public function get_param(string $key): mixed + { + return $this->params[$key] ?? null; + } + + public function get_header(string $name): string + { + return $this->headers[strtolower($name)] ?? ''; + } +} + +final class WPDB_Compat +{ + public string $prefix = 'wp_'; + public int $insert_id = 0; + + /** @var array>> */ + private array $tables = []; + + /** @var array */ + private array $autoIds = []; + + public function __construct(string $dbPath) + { + // In-memory emulation; path retained for compatibility. + } + + public function get_charset_collate(): string + { + return ''; + } + + public function prepare(string $query, mixed ...$args): string + { + $out = $query; + foreach ($args as $arg) { + $replacement = is_int($arg) ? (string) $arg : ("'" . str_replace("'", "''", (string) $arg) . "'"); + $out = preg_replace('/%[dsf]/', $replacement, $out, 1) ?? $out; + } + return $out; + } + + public function query(string $query): int|false + { + $trimmed = trim($query); + if (preg_match('/^CREATE TABLE IF NOT EXISTS\s+([a-zA-Z0-9_]+)/i', $trimmed, $m)) { + $table = $m[1]; + $this->tables[$table] = $this->tables[$table] ?? []; + $this->autoIds[$table] = $this->autoIds[$table] ?? 0; + return 0; + } + return 0; + } + + public function get_results(string $query): array + { + $q = trim($query); + + if (preg_match('/^SELECT \* FROM\s+([a-zA-Z0-9_]+)\s+ORDER BY\s+id\s+ASC$/i', $q, $m)) { + $rows = $this->tables[$m[1]] ?? []; + usort($rows, static fn(array $a, array $b): int => ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0))); + return array_map(static fn(array $r): object => (object) $r, $rows); + } + + if (preg_match('/^SELECT\s+\*\s+FROM\s+([a-zA-Z0-9_]+)\s+WHERE\s+id\s*=\s*(\d+)$/i', $q, $m)) { + $row = $this->findById($m[1], (int) $m[2]); + return $row ? [(object) $row] : []; + } + + if (preg_match('/^SELECT\s+\*\s+FROM\s+([a-zA-Z0-9_]+)\s+WHERE\s+caldav_resource\s*=\s*\'([^\']*)\'$/i', $q, $m)) { + $table = $m[1]; + $needle = str_replace("''", "'", $m[2]); + $rows = $this->tables[$table] ?? []; + foreach ($rows as $row) { + if ((string) ($row['caldav_resource'] ?? '') === $needle) { + return [(object) $row]; + } + } + return []; + } + + if (preg_match('/^SELECT\s+occurrence_key\s+FROM\s+([a-zA-Z0-9_]+)\s+WHERE\s+event_id\s*=\s*(\d+)\s+AND\s+exception_type\s*=\s*\'([^\']+)\'$/i', $q, $m)) { + $table = $m[1]; + $eventId = (int) $m[2]; + $type = $m[3]; + $rows = $this->tables[$table] ?? []; + $filtered = array_values(array_filter( + $rows, + static fn(array $r): bool => ((int) ($r['event_id'] ?? 0) === $eventId) && ((string) ($r['exception_type'] ?? '') === $type) + )); + return array_map( + static fn(array $r): object => (object) ['occurrence_key' => (string) ($r['occurrence_key'] ?? '')], + $filtered + ); + } + + return []; + } + + public function get_row(string $query): ?object + { + $rows = $this->get_results($query); + return $rows[0] ?? null; + } + + public function get_var(string $query): mixed + { + $q = trim($query); + if (preg_match('/^SHOW TABLES LIKE\s*\'([^\']+)\'$/i', $q, $m)) { + $table = str_replace("''", "'", $m[1]); + return array_key_exists($table, $this->tables) ? $table : null; + } + $row = $this->get_row($query); + if (!$row) { + return null; + } + $vars = get_object_vars($row); + foreach ($vars as $value) { + return $value; + } + return null; + } + + public function insert(string $table, array $data, array $formats = []): int|false + { + $this->tables[$table] = $this->tables[$table] ?? []; + $this->autoIds[$table] = $this->autoIds[$table] ?? 0; + + $id = ++$this->autoIds[$table]; + $row = ['id' => $id] + $data; + $this->tables[$table][] = $row; + $this->insert_id = $id; + + return 1; + } + + public function update(string $table, array $data, array $where, array $formats = [], array $whereFormats = []): int|false + { + $rows = $this->tables[$table] ?? []; + $updated = 0; + foreach ($rows as $i => $row) { + if (!$this->matchesWhere($row, $where)) { + continue; + } + $rows[$i] = array_merge($row, $data); + $updated++; + } + $this->tables[$table] = $rows; + return $updated; + } + + public function delete(string $table, array $where, array $whereFormats = []): int|false + { + $rows = $this->tables[$table] ?? []; + $kept = []; + $deleted = 0; + foreach ($rows as $row) { + if ($this->matchesWhere($row, $where)) { + $deleted++; + continue; + } + $kept[] = $row; + } + $this->tables[$table] = $kept; + return $deleted; + } + + private function findById(string $table, int $id): ?array + { + $rows = $this->tables[$table] ?? []; + foreach ($rows as $row) { + if ((int) ($row['id'] ?? 0) === $id) { + return $row; + } + } + return null; + } + + private function matchesWhere(array $row, array $where): bool + { + foreach ($where as $k => $v) { + if (!array_key_exists($k, $row)) { + return false; + } + if ((string) $row[$k] !== (string) $v) { + return false; + } + } + return true; + } +} + +$GLOBALS['wp_actions'] = []; +$GLOBALS['wp_shortcodes'] = []; +$GLOBALS['wp_routes'] = []; +$GLOBALS['wp_options'] = []; +$GLOBALS['wp_mail_outbox'] = []; +$GLOBALS['wp_user'] = [ + 'id' => 1, + 'email' => 'admin@example.test', + 'caps' => ['manage_options', 'edit_posts'], +]; +$GLOBALS['wp_activation_hooks'] = []; +$GLOBALS['wp_admin_menu'] = []; +$GLOBALS['wp_options']['admin_email'] = 'admin@example.test'; + +function add_action(string $hook, callable $callback, int $priority = 10, int $acceptedArgs = 1): void +{ + $GLOBALS['wp_actions'][$hook][] = $callback; +} + +function do_action(string $hook, mixed ...$args): void +{ + foreach (($GLOBALS['wp_actions'][$hook] ?? []) as $cb) { + $cb(...$args); + } +} + +function add_shortcode(string $tag, callable $callback): void +{ + $GLOBALS['wp_shortcodes'][$tag] = $callback; +} + +function do_shortcode(string $content): string +{ + if (preg_match('/\[([a-z0-9_\-]+)\]/i', $content, $m)) { + $tag = $m[1]; + if (isset($GLOBALS['wp_shortcodes'][$tag])) { + return (string) call_user_func($GLOBALS['wp_shortcodes'][$tag]); + } + } + return $content; +} + +function register_rest_route(string $namespace, string $route, array $args): void +{ + $GLOBALS['wp_routes'][] = [ + 'namespace' => '/' . trim($namespace, '/'), + 'route' => $route, + 'args' => $args, + ]; +} + +function rest_do_request(WP_REST_Request $request): array|WP_Error +{ + $method = strtoupper($request->get_method()); + $path = '/' . trim($request->get_route(), '/'); + + foreach ($GLOBALS['wp_routes'] as $entry) { + $namespace = $entry['namespace']; + $routePattern = $entry['route']; + $args = $entry['args']; + + $registeredMethods = strtoupper((string) ($args['methods'] ?? 'GET')); + if ($registeredMethods !== $method) { + continue; + } + + $regex = '#^' . preg_quote($namespace, '#') . preg_replace('#/#', '\\/', $routePattern) . '$#'; + if (!preg_match($regex, $path, $matches)) { + continue; + } + + foreach ($matches as $k => $v) { + if (is_string($k)) { + $request->set_param($k, $v); + } + } + + $perm = $args['permission_callback'] ?? '__return_true'; + $allowed = is_callable($perm) ? (bool) call_user_func($perm, $request) : false; + if (!$allowed) { + return new WP_Error('forbidden', 'Forbidden', ['status' => 403]); + } + + $cb = $args['callback'] ?? null; + if (!is_callable($cb)) { + return new WP_Error('server_error', 'Route callback missing', ['status' => 500]); + } + + return call_user_func($cb, $request); + } + + return new WP_Error('not_found', 'Route not found', ['status' => 404]); +} + +function register_activation_hook(string $pluginFile, callable $callback): void +{ + $GLOBALS['wp_activation_hooks'][$pluginFile] = $callback; +} + +function register_deactivation_hook(string $pluginFile, callable $callback): void +{ + $GLOBALS['wp_activation_hooks'][$pluginFile . ':deactivate'] = $callback; +} + +function get_option(string $key, mixed $default = false): mixed +{ + return array_key_exists($key, $GLOBALS['wp_options']) ? $GLOBALS['wp_options'][$key] : $default; +} + +function update_option(string $key, mixed $value, bool $autoload = true): bool +{ + $GLOBALS['wp_options'][$key] = $value; + return true; +} + +function delete_option(string $key): bool +{ + unset($GLOBALS['wp_options'][$key]); + return true; +} + +function get_current_user_id(): int +{ + return (int) ($GLOBALS['wp_user']['id'] ?? 0); +} + +function current_user_can(string $capability): bool +{ + return in_array($capability, (array) ($GLOBALS['wp_user']['caps'] ?? []), true); +} + +function wp_verify_nonce(string $nonce, string $action): bool +{ + return $nonce === 'ok:' . $action; +} + +function wp_get_current_user(): object +{ + return (object) ['user_email' => (string) ($GLOBALS['wp_user']['email'] ?? '')]; +} + +function esc_html(string $value): string +{ + return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} + +function __return_true(): bool +{ + return true; +} + +if (!function_exists('dbDelta')) { + function dbDelta(string $sql): array + { + return []; + } +} + +function add_menu_page(string $pageTitle, string $menuTitle, string $capability, string $menuSlug, callable $callback): void +{ + $GLOBALS['wp_admin_menu'][$menuSlug] = ['title' => $menuTitle, 'callback' => $callback]; +} + +function add_submenu_page(string $parentSlug, string $pageTitle, string $menuTitle, string $capability, string $menuSlug, callable $callback): void +{ + $GLOBALS['wp_admin_menu'][$menuSlug] = ['title' => $menuTitle, 'parent' => $parentSlug, 'callback' => $callback]; +} + +function admin_url(string $path = ''): string +{ + return '/wp-admin/' . ltrim($path, '/'); +} + +function home_url(string $path = ''): string +{ + return 'http://localhost' . (str_starts_with($path, '/') ? $path : ('/' . $path)); +} + +function wp_mail(string|array $to, string $subject, string $message): bool +{ + $GLOBALS['wp_mail_outbox'][] = ['to' => $to, 'subject' => $subject, 'message' => $message]; + return true; +} + +function wp_generate_password(int $length = 12, bool $specialChars = true): string +{ + $bytes = random_bytes(max(1, intdiv($length + 1, 2))); + return substr(bin2hex($bytes), 0, $length); +} + +if (!defined('ABSPATH')) { +define('ABSPATH', __DIR__ . '/wp-emu-root/'); +define('CALENDAR_PLUGIN_ALLOW_DEBUG_TOKENS', true); +} + +$upgradePath = ABSPATH . 'wp-admin/includes/upgrade.php'; +@mkdir(dirname($upgradePath), 0775, true); +file_put_contents( + $upgradePath, + "query('CREATE TABLE IF NOT EXISTS wp_calendar_events (id INTEGER PRIMARY KEY AUTOINCREMENT)'); +$wpdb->query('CREATE TABLE IF NOT EXISTS wp_calendar_recurrence_exceptions (id INTEGER PRIMARY KEY AUTOINCREMENT)'); + +require_once dirname(__DIR__) . '/code/calendar-plugin.php'; + +do_action('init'); +do_action('rest_api_init'); + +if (PHP_SAPI === 'cli' && basename((string) ($_SERVER['SCRIPT_FILENAME'] ?? '')) === 'wp_emulation.php') { + echo "wp-emulation booted\n"; + echo 'routes=' . count($GLOBALS['wp_routes']) . "\n"; +} diff --git a/credentials/.env b/credentials/.env new file mode 100644 index 0000000..c302f82 --- /dev/null +++ b/credentials/.env @@ -0,0 +1,48 @@ +# Dummy remote testing server environment settings +# Replace all placeholder values before real use. + +# Remote host access +REMOTE_HOST=cs.chezstephens.org.uk +REMOTE_PORT=22 +REMOTE_USER=root +REMOTE_SSH_KEY_PATH=credentials/id_rsa +REMOTE_APP_DIR=/var/www/wordpress/wp-content/plugins/calendar-plugin + +# Remote runtime +REMOTE_ENV=testing +REMOTE_TIMEZONE=Europe/London +REMOTE_PHP_BIN=/usr/bin/php +REMOTE_WP_CLI=/usr/local/bin/wp + +# WordPress context (testing only) +WP_PATH=/var/www/html +WP_URL=https://chezstephens.org.uk +WP_PLUGIN_SLUG=calendar-plugin + +# Database placeholders (testing only) +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_NAME=wordpress_test +DB_USER=wp_test_user +DB_PASSWORD=change_me + +# Deployment/package settings +PACKAGE_DIR=/home/wp_tester/releases +PACKAGE_NAME=calendar-plugin-0.0.0.zip +KEEP_DATA_ON_UNINSTALL=true + +# Local testing database (Debian 13 / MariaDB) +LOCAL_DB_HOST=localhost +LOCAL_DB_PORT=3306 +LOCAL_DB_NAME=calendar_plugin_test +LOCAL_DB_USER=calendar_plugin_test_user +LOCAL_DB_PASSWORD=38Kgw7WY5w9xJpwyFcnK + +# SMTP settings for fixture email workflows (register/verify/reset/request-write) +SMTP_HOST=mail.chezstephens.org.uk +SMTP_PORT=587 +SMTP_USE_TLS=true +SMTP_USERNAME=adrians@chezstephens.org.uk +SMTP_PASSWORD=buck..it +SMTP_FROM=adrians@chezstephens.org.uk +SMTP_ADMIN_TO=adrians@chezstephens.org.uk diff --git a/credentials/id_rsa b/credentials/id_rsa new file mode 100644 index 0000000..30a58de --- /dev/null +++ b/credentials/id_rsa @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEAxWDcob7BWPrw/bMOCfm0QGj6gdKNxuatgjbX4uJLk1f3f74erHGq +610ebDUqdrmcVjeG70eIJKJEp+xxMtmyr+vRIQXBTzuAkAunXQigFKe985VPVHsH8DU+mY +sOH+W+Rnsh0at8yExyqKImH5P980ZuxQJDoM5DJk96Mn3YstUySlTXOZ+X3lBbYf93pYZ/ +cyrBzwkakjluL9oEwqSPqjjM8EbnJi4JHNsxtAZPxIluGbsdlIKDSzePXl67aA7PLzQunO +L2MN7yUrymSALVGqLHZE8M4FMJByyjmRKSnWswhtsXuGc6jkMMv1idg8VwypCGLvTOjibG +p4fGgXJLd8jYaq2QrXy5OXyZqoAtlZo7FCEBKhMG3VRlJ5JEeBamkCstyKuy4qqYF2uwe9 +pnt9wbmFAIBVNRK/GR3J6ki50YWtfAMpYlfaLdfTuliceQf4Jsc+Bj3K+xtZyz9J1Xjcvt +dQk9rqX/NZ7VbOpprzky8sCp9mEjVqvhaopVN7mTAAAFgLYbU1+2G1NfAAAAB3NzaC1yc2 +EAAAGBAMVg3KG+wVj68P2zDgn5tEBo+oHSjcbmrYI21+LiS5NX93++HqxxqutdHmw1Kna5 +nFY3hu9HiCSiRKfscTLZsq/r0SEFwU87gJALp10IoBSnvfOVT1R7B/A1PpmLDh/lvkZ7Id +GrfMhMcqiiJh+T/fNGbsUCQ6DOQyZPejJ92LLVMkpU1zmfl95QW2H/d6WGf3Mqwc8JGpI5 +bi/aBMKkj6o4zPBG5yYuCRzbMbQGT8SJbhm7HZSCg0s3j15eu2gOzy80Lpzi9jDe8lK8pk +gC1Rqix2RPDOBTCQcso5kSkp1rMIbbF7hnOo5DDL9YnYPFcMqQhi70zo4mxqeHxoFyS3fI +2GqtkK18uTl8maqALZWaOxQhASoTBt1UZSeSRHgWppArLcirsuKqmBdrsHvaZ7fcG5hQCA +VTUSvxkdyepIudGFrXwDKWJX2i3X07pYnHkH+CbHPgY9yvsbWcs/SdV43L7XUJPa6l/zWe +1Wzqaa85MvLAqfZhI1ar4WqKVTe5kwAAAAMBAAEAAAGAWs/jE0QZ31+ty3w7hFlwFoZ2Y4 +7FjnMJ97RWBdyKWyOJCywlHsA5nIq+eZjIjdF+Xai0m5j0ya4jGoPN3VCORySflqr4MwU0 +dJH4EfTq+jXnTpAu7Laig2FsCOcSu5hPwEvc1oQpKFsMEgxwr+y+VdTdGCWfiff8qz68AU +knj7hJqCt6ztdf33hnYyJQIUdNkmZkv2X35Lkpujh8IjXmp7H0kMR+i3F43d738lVJFCsL +DimqRW77C3tnqkq5vPm6iI6UW+rKuUyXRnGX4VFM9PYvDnGpznHRJe43aQepgDmvBJQaVU +u67lDfpmGnaj/8UBIjC2sWlaqST/TltmcfmabjW9RE8pu1r+mI9b3Q/J2viDbhqqR8Jr3e +XFRqE7ndH+pGycwIWSmumAUo7Dyf86d398ZjpaJRzoMhD3HsBvZP3VEb7/gfaGv2hnFobc +VUtXP9SEB51hUwuvevKb6rmHrhlIXtVs5b4GEyv7tTNdzVIa+SaD+AlohjIlAo6AE5AAAA +wQC97JCbpX8BUb1lfps7CuKoMrnV7eV2o5hBzRmDLz/nSgrnQ22D4QR1Rmrk2/zWWZ3/VH +wB75WO5lWe6ww832Q3h5qbx9R14DtOLUXkZRw6veEa6cnJnGYFJVemOqKsMYmKIxMkd6wq +ynPVR+oseXQ7XFDwUGWi7x2q4EAaMQUqIAeylWeOVYDZUQdn/tCnYz1M56NADGEKo3o13R +E+GWN/xO2D90EXpVuJdSMvs5FWF5wpSioBf7NiaEqoW1JfI2YAAADBAOhTviR6/GgVmd/y +f48HOlhOv8yax9cLJ1uw8IoMu5VABTiSdVL2gdpBdsjNG1tzgccjmxzFWaDR3OLw4Ho1w9 +ke/AvZ7ysqoNRzRTFMuYshXg1K2GeTTBXCj4QApFZdT2bz+KbpWIO9k2eU5Y2i5AhyZq7E +NcTBeDQzYE+3VHTWfAHoTaTXCBewc1V49NTM2DOMRwKXb9Dm84m12gI3gUtjb/z3W2pZA+ +0o3M3CYtKjQTd2Fnr9MtqIA/QZACiplQAAAMEA2X167zBeebDQG00MQF0GfKY64lElelZX +8CxH4N7QtTGnSsJkBD7xR8S2aOlgP8ZNQJV3NhjbMzqTxsCsHX1+185IcUUrhitphSktW4 +WQDenScMNhWZG/SS2o5RAZHRA+6CcypvPOZNoFCZkHNwOlrUuarzvMlKfBrlax9z3L/CpJ +AytTVc4haM4PkC4U8FOmIUAufagxmj8nqtUut1nc9O676OgWrZRnBhf1voJphdNg0b3RrE +szaVxAD1qt0hyHAAAACGNzYmFja3VwAQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/docs/fixture.md b/docs/fixture.md new file mode 100644 index 0000000..7f6a9aa --- /dev/null +++ b/docs/fixture.md @@ -0,0 +1,118 @@ +# Local Compatibility Harness + +## Purpose +Run a local, deterministic compatibility harness that exercises the current requirements for: + +- Admin navigation pages (`Edit Calendar`, `Users`, `Setup`) +- API CRUD and user lifecycle endpoints +- Shared-calendar CalDAV read/write flows +- ICS export endpoint + +Seed data includes canonical test fixtures `CE-001` through `CE-010` from `tests/calendar_entries.md`. + +## Quick Start +From repository root: + +```bash +./compatibility-layer/reset.sh +./compatibility-layer/seed.sh +./compatibility-layer/run.sh +``` + +Server default: + +- `http://127.0.0.1:8080` + +## WP Emulation Check +Run the stand-alone local WordPress emulation check (loads plugin code from `code/` and runs a minimal E2E flow): + +```bash +./compatibility-layer/e2e_wp_emulation.php +``` + +## Harness Authentication Model +This harness uses simple test auth headers/credentials to emulate behavior. + +### Admin/API (WordPress-style harness auth) +- Header: `X-WP-User: admin` (full admin) +- Header: `X-WP-User: editor` (editor-limited) + +### CalDAV (plugin user basic auth over local HTTP harness) +Seeded users: + +- `rw_user@example.test` / `rwpass123456` (`write`) +- `ro_user@example.test` / `ropass123456` (`read_only`) + +## Key Endpoints + +### Admin Pages +- `/wp-admin/admin.php?page=calendar-edit&as=admin` +- `/wp-admin/admin.php?page=calendar-users&as=admin` +- `/wp-admin/admin.php?page=calendar-setup&as=admin` + +Interactive pages available: + +- Public calendar UI: `/calendar` +- User self-service portal: `/users` + +If setup `url_slug` is configured (for example `demo`), canonical URLs move under that prefix (for example `/demo/calendar`, `/demo/users`, `/demo/wp-json/calendar/v1/...`, `/demo/calendar.ics`, `/demo/caldav/...`). + +### API +- `/wp-json/calendar/v1/events` +- `/wp-json/calendar/v1/events/{id}` +- `/wp-json/calendar/v1/events/{id}/occurrences/{occurrence_key}` +- `/wp-json/calendar/v1/users/register` +- `/wp-json/calendar/v1/users/verify` +- `/wp-json/calendar/v1/users/forgot-password` +- `/wp-json/calendar/v1/users/reset-password` +- `/wp-json/calendar/v1/users/{id}/request-write` +- `/wp-json/calendar/v1/admin/users` +- `/wp-json/calendar/v1/admin/users/{id}` +- `/wp-json/calendar/v1/admin/setup` +- `/wp-json/calendar/v1/public/events` +- `/wp-json/calendar/v1/users/me` + +### ICS +- `/calendar.ics` + +### CalDAV (single shared public calendar) +- `/caldav/` +- `/caldav/calendars/` +- `/caldav/calendars/public/` +- `/caldav/calendars/public/{object_id}.ics` + +## Smoke Commands + +List events: + +```bash +curl -sS -H 'X-WP-User: admin' http://127.0.0.1:8080/wp-json/calendar/v1/events +``` + +Delete one recurring occurrence as exception: + +```bash +curl -sS -X DELETE \ + -H 'X-WP-User: admin' \ + "http://127.0.0.1:8080/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00" \ + -i +``` + +Fetch ICS: + +```bash +curl -sS http://127.0.0.1:8080/calendar.ics +``` + +CalDAV read as read-only user: + +```bash +curl -sS -u ro_user@example.test:ropass123456 \ + http://127.0.0.1:8080/caldav/calendars/public/1.ics -i +``` + +## Notes +- This compatibility harness is intentionally minimal and deterministic for local development. +- It is not a production security implementation. +- It enforces the shared-calendar model with per-user read/write permissions. +- Current implementation delegates to `fixture/` internals via wrapper scripts in `compatibility-layer/`. diff --git a/fixture-tests/README.md b/fixture-tests/README.md new file mode 100644 index 0000000..2cec17d --- /dev/null +++ b/fixture-tests/README.md @@ -0,0 +1,9 @@ +# Fixture Tests (Archived) + +These tests are retained from the local fixture/harness phase. + +- `fixture_smoke.sh` +- `fixture_security_smoke.sh` +- `fixture_caldav_client_compat_smoke.sh` + +Current development validation is remote-server-first. Keep these scripts for legacy comparison/debug only. diff --git a/fixture-tests/fixture_caldav_client_compat_smoke.sh b/fixture-tests/fixture_caldav_client_compat_smoke.sh new file mode 100755 index 0000000..8a8b43d --- /dev/null +++ b/fixture-tests/fixture_caldav_client_compat_smoke.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:8080}" +CALDAV_USER="${CALDAV_USER:-rw_user@example.test}" +CALDAV_PASSWORD="${CALDAV_PASSWORD:-rwpass123456}" + +require_contains() { + local haystack="$1" + local needle="$2" + local msg="$3" + if ! printf '%s' "$haystack" | grep -q "$needle"; then + echo "[caldav-compat] FAIL: $msg" + exit 1 + fi +} + +require_contains_ci() { + local haystack="$1" + local needle="$2" + local msg="$3" + if ! printf '%s' "$haystack" | grep -qi "$needle"; then + echo "[caldav-compat] FAIL: $msg" + exit 1 + fi +} + +echo "[caldav-compat] checking unauthenticated challenge" +unauth_headers="$(curl -sS -i "$BASE_URL/caldav/" | tr -d '\r')" +require_contains "$unauth_headers" ' 401 ' "expected 401 from /caldav/" +require_contains_ci "$unauth_headers" 'www-authenticate: basic' "missing WWW-Authenticate Basic challenge" + +echo "[caldav-compat] checking OPTIONS capability advertisement" +opt_headers="$(curl -sS -i -u "$CALDAV_USER:$CALDAV_PASSWORD" -X OPTIONS "$BASE_URL/caldav/" | tr -d '\r')" +require_contains_ci "$opt_headers" 'dav:' "missing DAV header" +require_contains_ci "$opt_headers" 'calendar-access' "missing DAV calendar-access advertisement" +require_contains_ci "$opt_headers" 'allow: ' "missing Allow header for CalDAV OPTIONS" +require_contains_ci "$opt_headers" 'options' "Allow header missing OPTIONS" +require_contains_ci "$opt_headers" 'propfind' "Allow header missing PROPFIND" +require_contains_ci "$opt_headers" 'report' "Allow header missing REPORT" + +echo "[caldav-compat] checking root PROPFIND discovery" +root_xml="$(curl -fsS -u "$CALDAV_USER:$CALDAV_PASSWORD" -X PROPFIND \ + -H 'Depth: 1' \ + -H 'Content-Type: application/xml' \ + --data '' \ + "$BASE_URL/caldav/")" +require_contains "$root_xml" '' "root discovery missing current-user-principal" +require_contains "$root_xml" '/caldav/calendars/public/' "root discovery missing public calendar href" +require_contains "$root_xml" '' "root discovery missing calendar collection marker" + +principal_href="$(printf '%s' "$root_xml" | grep -oE '/caldav/principals/[^<"]+/' | head -n 1 || true)" +if [ -z "$principal_href" ]; then + echo "[caldav-compat] FAIL: unable to locate principal href in root PROPFIND" + exit 1 +fi + +echo "[caldav-compat] checking principal calendar-home-set" +principal_xml="$(curl -fsS -u "$CALDAV_USER:$CALDAV_PASSWORD" -X PROPFIND \ + -H 'Depth: 0' \ + -H 'Content-Type: application/xml' \ + --data '' \ + "$BASE_URL$principal_href")" +require_contains "$principal_xml" '' "principal missing calendar-home-set" +require_contains "$principal_xml" '/caldav/calendars/' "calendar-home-set does not point to /caldav/calendars/" + +echo "[caldav-compat] checking calendars home-set collection listing" +home_xml="$(curl -fsS -u "$CALDAV_USER:$CALDAV_PASSWORD" -X PROPFIND \ + -H 'Depth: 1' \ + -H 'Content-Type: application/xml' \ + --data '' \ + "$BASE_URL/caldav/calendars/")" +require_contains "$home_xml" '/caldav/calendars/public/' "calendar home-set listing missing public calendar href" +require_contains "$home_xml" '' "calendar home-set listing missing calendar marker" +require_contains "$home_xml" '' "calendar home-set listing missing supported component set" + +echo "[caldav-compat] all checks passed" diff --git a/fixture-tests/fixture_security_smoke.sh b/fixture-tests/fixture_security_smoke.sh new file mode 100755 index 0000000..6b3e8c4 --- /dev/null +++ b/fixture-tests/fixture_security_smoke.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:8080}" +DB_PATH="${DB_PATH:-fixture/fixture.db}" +RUN_ID="${RUN_ID:-$(date +%s)}" + +echo "[security-smoke] checking service readiness" +curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" >/dev/null + +echo "[security-smoke] checking register token non-disclosure" +register_email="sec-nodisclose-${RUN_ID}@example.test" +reg_code="$(curl -sS -o /tmp/sec_register.json -w '%{http_code}' -H 'Content-Type: application/json' \ + -d "{\"email\":\"${register_email}\",\"password\":\"strongpass123\"}" \ + "$BASE_URL/wp-json/calendar/v1/users/register")" +if [ "$reg_code" != "200" ] && [ "$reg_code" != "201" ] && [ "$reg_code" != "409" ]; then + echo "[security-smoke] FAIL: unexpected register status $reg_code" + exit 1 +fi +reg_payload="$(cat /tmp/sec_register.json)" +if echo "$reg_payload" | grep -q 'verification_token_fixture'; then + echo "[security-smoke] FAIL: register response leaked verification token" + exit 1 +fi + +echo "[security-smoke] checking forgot-password token non-disclosure" +forgot_payload="$(curl -fsS -H 'Content-Type: application/json' \ + -d '{"email":"adrians@chezstephens.org.uk"}' \ + "$BASE_URL/wp-json/calendar/v1/users/forgot-password")" +if echo "$forgot_payload" | grep -q 'reset_token_fixture'; then + echo "[security-smoke] FAIL: forgot-password response leaked reset token" + exit 1 +fi + +echo "[security-smoke] checking register rate limiting" +seen_429=0 +rate_prefix="sec-rate-${RUN_ID}" +for i in $(seq 1 10); do + code="$(curl -sS -o /tmp/sec_reg_$i.json -w '%{http_code}' \ + -H 'Content-Type: application/json' \ + -d "{\"email\":\"${rate_prefix}-$i@example.test\",\"password\":\"strongpass123\"}" \ + "$BASE_URL/wp-json/calendar/v1/users/register")" + if [ "$code" = "429" ]; then + seen_429=1 + break + fi +done +if [ "$seen_429" -ne 1 ]; then + echo "[security-smoke] FAIL: expected 429 from register rate limiter" + exit 1 +fi + +echo "[security-smoke] checking password hash format in database" +python3 - "$DB_PATH" <<'PY' +import sqlite3 +import sys + +db_path = sys.argv[1] +conn = sqlite3.connect(db_path) +cur = conn.cursor() +rows = cur.execute("SELECT email, password_hash FROM caldav_users").fetchall() +conn.close() +if not rows: + print("[security-smoke] FAIL: no users found") + raise SystemExit(1) +for email, pw_hash in rows: + if not isinstance(pw_hash, str) or not pw_hash.startswith("pbkdf2_sha256$"): + print(f"[security-smoke] FAIL: non-pbkdf2 hash for {email}: {pw_hash!r}") + raise SystemExit(1) +print("[security-smoke] password hash format OK") +PY + +echo "[security-smoke] checking strict CalDAV resource filename semantics" +resource="sec-$(date +%s).ics" +cat > /tmp/sec_caldav.ics <<'ICS' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Security Smoke//EN +BEGIN:VEVENT +UID:sec-smoke-uid@example.test +SUMMARY:Security Smoke Event +DTSTART:20260415T100000 +DTEND:20260415T110000 +END:VEVENT +END:VCALENDAR +ICS +put_code="$(curl -sS -o /tmp/sec_caldav_put.json -w '%{http_code}' \ + -u adrians@chezstephens.org.uk:brillig1 \ + -X PUT --data-binary @/tmp/sec_caldav.ics \ + "$BASE_URL/caldav/calendars/public/$resource")" +if [ "$put_code" != "201" ] && [ "$put_code" != "200" ]; then + echo "[security-smoke] FAIL: expected 200/201 for CalDAV PUT, got $put_code" + exit 1 +fi +get_code="$(curl -sS -o /tmp/sec_caldav_get.ics -w '%{http_code}' \ + -u adrians@chezstephens.org.uk:brillig1 \ + "$BASE_URL/caldav/calendars/public/$resource")" +if [ "$get_code" != "200" ]; then + echo "[security-smoke] FAIL: expected 200 for CalDAV GET on same resource, got $get_code" + exit 1 +fi +missing_code="$(curl -sS -o /tmp/sec_caldav_missing.out -w '%{http_code}' \ + -u adrians@chezstephens.org.uk:brillig1 \ + "$BASE_URL/caldav/calendars/public/999999.ics")" +if [ "$missing_code" != "404" ]; then + echo "[security-smoke] FAIL: expected 404 for unknown numeric resource, got $missing_code" + exit 1 +fi + +echo "[security-smoke] all checks passed" diff --git a/fixture-tests/fixture_smoke.sh b/fixture-tests/fixture_smoke.sh new file mode 100755 index 0000000..b7c0bde --- /dev/null +++ b/fixture-tests/fixture_smoke.sh @@ -0,0 +1,330 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:8080}" + +echo "[smoke] checking api list" +curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" >/dev/null + +echo "[smoke] checking ics" +curl -fsS "$BASE_URL/calendar.ics" | grep -q 'BEGIN:VCALENDAR' + +echo "[smoke] checking caldav read" +curl -fsS -u rw_user@example.test:rwpass123456 "$BASE_URL/caldav/calendars/public/1.ics" >/dev/null + +echo "[smoke] checking caldav multiget href filtering" +TMP_XML="$(mktemp)" +cleanup_tmp_xml() { rm -f "$TMP_XML"; } +trap cleanup_tmp_xml EXIT +cat > "$TMP_XML" <<'XML' + + + + /caldav/calendars/public/4.ics + +XML +REPORT_XML="$(curl -fsS -u rw_user@example.test:rwpass123456 -X REPORT \ + "$BASE_URL/caldav/calendars/public/" \ + -H 'Content-Type: text/xml; charset=utf-8' \ + -H 'Depth: 1' \ + --data-binary @"$TMP_XML")" +echo "$REPORT_XML" | grep -q '/caldav/calendars/public/4.ics' +if echo "$REPORT_XML" | grep -q '/caldav/calendars/public/1.ics'; then + echo "[smoke] multiget returned unrelated resources" + exit 1 +fi +trap - EXIT +cleanup_tmp_xml + +echo "[smoke] checking recurrence exception delete" +curl -fsS -X DELETE -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00" \ + -o /dev/null -w '%{http_code}' | grep -q '^204$' + +echo "[smoke] checking event occurrences endpoint + idempotent delete" +CREATE_JSON='{"title":"Smoke Occurrences API","description":"occ-endpoint","location":"","category":"","all_day_event":false,"start_datetime":"2026-04-01T10:00:00+01:00","end_datetime":"2026-04-01T11:00:00+01:00","repeat_type":"daily","repeat_interval":1,"repeat_range_mode":"count","repeat_count":3}' +CREATE_RESP="$(curl -fsS -H 'Content-Type: application/json' -H 'X-WP-User: admin' \ + -d "$CREATE_JSON" "$BASE_URL/wp-json/calendar/v1/events")" +EVENT_ID="$(python3 - <<'PY' "$CREATE_RESP" +import json,sys +print(json.loads(sys.argv[1])["data"]["id"]) +PY +)" +OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-04-01&months=1")" +python3 - <<'PY' "$OCC_RESP" +import json,sys +data=json.loads(sys.argv[1]) +days=[x["occurrence_start"][:10] for x in data["data"]] +assert len(data["data"]) >= 3 +assert "2026-04-02" in days +PY +curl -fsS -X DELETE -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-04-02T10:00:00+01:00" \ + -o /dev/null -w '%{http_code}' | grep -q '^204$' +curl -fsS -X DELETE -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-04-02T10:00:00+01:00" \ + -o /dev/null -w '%{http_code}' | grep -q '^204$' +OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-04-01&months=1")" +python3 - <<'PY' "$OCC_RESP" +import json,sys +days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] +assert "2026-04-02" not in days +PY + +echo "[smoke] checking caldav vtimezone parsing regression" +TMP_ICS="$(mktemp)" +cleanup_tmp_ics() { rm -f "$TMP_ICS"; } +trap cleanup_tmp_ics EXIT +cat > "$TMP_ICS" <<'ICS' +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +BEGIN:STANDARD +DTSTART:18471201T000000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9 +TZOFFSETFROM:+0115 +TZOFFSETTO:+0000 +TZNAME:GMT +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:smoke-vtimezone-parser-001 +SUMMARY:Smoke VTIMEZONE Parse +DTSTART;TZID=Europe/London:20260423T150000 +DTEND;TZID=Europe/London:20260423T160000 +END:VEVENT +END:VCALENDAR +ICS +curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ + -H 'Content-Type: text/calendar; charset=utf-8' \ + --data-binary @"$TMP_ICS" \ + "$BASE_URL/caldav/calendars/public/smoke-vtimezone.ics" >/dev/null +CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ + "$BASE_URL/caldav/calendars/public/smoke-vtimezone.ics")" +echo "$CALDAV_ICS" | grep -q 'DTSTART[^[:space:]]*:20260423T150000' +if echo "$CALDAV_ICS" | grep -q 'RRULE'; then + echo "[smoke] unexpected RRULE found in VEVENT-only upload" + exit 1 +fi +trap - EXIT +cleanup_tmp_ics + +echo "[smoke] checking caldav monthly nth-weekday roundtrip" +TMP_ICS="$(mktemp)" +cleanup_tmp_ics() { rm -f "$TMP_ICS"; } +trap cleanup_tmp_ics EXIT +cat > "$TMP_ICS" <<'ICS' +BEGIN:VCALENDAR +PRODID:-//Smoke//EN +VERSION:2.0 +BEGIN:VEVENT +UID:smoke-monthly-nth-001 +SUMMARY:Smoke Monthly Nth +DTSTART;TZID=Europe/London:20260402T150000 +DTEND;TZID=Europe/London:20260402T160000 +RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4 +END:VEVENT +END:VCALENDAR +ICS +curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ + -H 'Content-Type: text/calendar; charset=utf-8' \ + --data-binary @"$TMP_ICS" \ + "$BASE_URL/caldav/calendars/public/smoke-monthly-nth.ics" >/dev/null +CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ + "$BASE_URL/caldav/calendars/public/smoke-monthly-nth.ics")" +echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4' +trap - EXIT +cleanup_tmp_ics + +echo "[smoke] checking caldav monthly BYDAY ordinal import" +TMP_ICS="$(mktemp)" +cleanup_tmp_ics() { rm -f "$TMP_ICS"; } +trap cleanup_tmp_ics EXIT +cat > "$TMP_ICS" <<'ICS' +BEGIN:VCALENDAR +PRODID:-//Smoke//EN +VERSION:2.0 +BEGIN:VEVENT +UID:smoke-monthly-ordinal-001 +SUMMARY:Smoke Monthly Ordinal +DTSTART;TZID=Europe/London:20260411T150000 +DTEND;TZID=Europe/London:20260411T160000 +RRULE:FREQ=MONTHLY;BYDAY=2SA +END:VEVENT +END:VCALENDAR +ICS +curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ + -H 'Content-Type: text/calendar; charset=utf-8' \ + --data-binary @"$TMP_ICS" \ + "$BASE_URL/caldav/calendars/public/smoke-monthly-ordinal.ics" >/dev/null +CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ + "$BASE_URL/caldav/calendars/public/smoke-monthly-ordinal.ics")" +echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2' +trap - EXIT +cleanup_tmp_ics + +echo "[smoke] checking caldav monthly last-weekday roundtrip" +TMP_ICS="$(mktemp)" +cleanup_tmp_ics() { rm -f "$TMP_ICS"; } +trap cleanup_tmp_ics EXIT +cat > "$TMP_ICS" <<'ICS' +BEGIN:VCALENDAR +PRODID:-//Smoke//EN +VERSION:2.0 +BEGIN:VEVENT +UID:smoke-monthly-last-001 +SUMMARY:Smoke Monthly Last +DTSTART;TZID=Europe/London:20260425T150000 +DTEND;TZID=Europe/London:20260425T160000 +RRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1 +END:VEVENT +END:VCALENDAR +ICS +curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ + -H 'Content-Type: text/calendar; charset=utf-8' \ + --data-binary @"$TMP_ICS" \ + "$BASE_URL/caldav/calendars/public/smoke-monthly-last.ics" >/dev/null +CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ + "$BASE_URL/caldav/calendars/public/smoke-monthly-last.ics")" +echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1' +trap - EXIT +cleanup_tmp_ics + +echo "[smoke] checking monthly nth-weekday delete-exception preserves rule" +CREATE_JSON='{"title":"Smoke 4th Sunday Anchor","description":"anchor-normalization","location":"","category":"","all_day_event":false,"start_datetime":"2026-05-19T15:00:00+01:00","end_datetime":"2026-05-19T16:00:00+01:00","repeat_type":"monthly","repeat_interval":1,"repeat_nth_mode":"weekday_of_month","repeat_nth_pos":4,"repeat_nth_weekday":0,"repeat_range_mode":"no_end"}' +CREATE_RESP="$(curl -fsS -H 'Content-Type: application/json' -H 'X-WP-User: admin' \ + -d "$CREATE_JSON" "$BASE_URL/wp-json/calendar/v1/events")" +python3 - <<'PY' "$CREATE_RESP" +import json,sys +data=json.loads(sys.argv[1])["data"] +assert data["start_datetime"] == "2026-05-24T15:00:00+01:00" +PY +EVENT_ID="$(python3 - <<'PY' "$CREATE_RESP" +import json,sys +print(json.loads(sys.argv[1])["data"]["id"]) +PY +)" +curl -fsS -X DELETE -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-05-24T15:00:00+01:00" \ + -o /dev/null -w '%{http_code}' | grep -q '^204$' +CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ + "$BASE_URL/caldav/calendars/public/${EVENT_ID}.ics")" +echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4' +echo "$CALDAV_ICS" | grep -q 'EXDATE;TZID=Europe/London:20260524T150000' + +echo "[smoke] checking delete-exception canonical key matching" +CREATE_JSON='{"title":"Smoke Daily Exception TZ","description":"tz-key","location":"","category":"","all_day_event":false,"start_datetime":"2026-03-02T10:00:00+00:00","end_datetime":"2026-03-02T11:00:00+00:00","repeat_type":"daily","repeat_interval":1,"repeat_range_mode":"until","repeat_until":"2026-03-19"}' +CREATE_RESP="$(curl -fsS -H 'Content-Type: application/json' -H 'X-WP-User: admin' \ + -d "$CREATE_JSON" "$BASE_URL/wp-json/calendar/v1/events")" +EVENT_ID="$(python3 - <<'PY' "$CREATE_RESP" +import json,sys +print(json.loads(sys.argv[1])["data"]["id"]) +PY +)" +curl -fsS -X DELETE -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-03-11T11:00:00+01:00" \ + -o /dev/null -w '%{http_code}' | grep -q '^204$' +OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-03-01&months=1")" +python3 - <<'PY' "$OCC_RESP" +import json,sys +days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] +assert "2026-03-11" not in days +PY +curl -fsS -X DELETE -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences/2026-03-10" \ + -o /dev/null -w '%{http_code}' | grep -q '^204$' +OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-03-01&months=1")" +python3 - <<'PY' "$OCC_RESP" +import json,sys +days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] +assert "2026-03-10" not in days +PY + +echo "[smoke] checking caldav EXDATE import to recurrence exceptions" +TMP_ICS="$(mktemp)" +cleanup_tmp_ics() { rm -f "$TMP_ICS"; } +trap cleanup_tmp_ics EXIT +cat > "$TMP_ICS" <<'ICS' +BEGIN:VCALENDAR +PRODID:-//Smoke//EN +VERSION:2.0 +BEGIN:VEVENT +UID:smoke-exdate-import-001 +SUMMARY:Smoke EXDATE Import +DTSTART;TZID=Europe/London:20260302T100000 +DTEND;TZID=Europe/London:20260302T110000 +RRULE:FREQ=DAILY;UNTIL=20260319T235959 +EXDATE;TZID=Europe/London:20260310T100000,20260311T100000 +END:VEVENT +END:VCALENDAR +ICS +curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ + -H 'Content-Type: text/calendar; charset=utf-8' \ + --data-binary @"$TMP_ICS" \ + "$BASE_URL/caldav/calendars/public/smoke-exdate-import.ics" >/dev/null +EVENT_ID="$(curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" | python3 -c "import json,sys; data=json.load(sys.stdin)['data']; print([e for e in data if e.get('uid')=='smoke-exdate-import-001'][0]['id'])")" +OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-03-01&months=1")" +python3 - <<'PY' "$OCC_RESP" +import json,sys +days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] +assert "2026-03-10" not in days +assert "2026-03-11" not in days +PY +trap - EXIT +cleanup_tmp_ics + +echo "[smoke] checking caldav cancelled-occurrence component handling" +TMP_ICS="$(mktemp)" +cleanup_tmp_ics() { rm -f "$TMP_ICS"; } +trap cleanup_tmp_ics EXIT +cat > "$TMP_ICS" <<'ICS' +BEGIN:VCALENDAR +PRODID:-//Smoke//EN +VERSION:2.0 +BEGIN:VEVENT +UID:smoke-cancelled-occurrence-001 +SUMMARY:Smoke Cancelled Occurrence +DTSTART;TZID=Europe/London:20260408T123000 +DTEND;TZID=Europe/London:20260408T133000 +RRULE:FREQ=WEEKLY +END:VEVENT +BEGIN:VEVENT +UID:smoke-cancelled-occurrence-001 +RECURRENCE-ID;TZID=Europe/London:20260506T123000 +DTSTART;TZID=Europe/London:20260506T123000 +DTEND;TZID=Europe/London:20260506T133000 +STATUS:CANCELLED +END:VEVENT +END:VCALENDAR +ICS +curl -fsS -u rw_user@example.test:rwpass123456 -X PUT \ + -H 'Content-Type: text/calendar; charset=utf-8' \ + --data-binary @"$TMP_ICS" \ + "$BASE_URL/caldav/calendars/public/smoke-cancelled-occurrence.ics" >/dev/null +CALDAV_ICS="$(curl -fsS -u rw_user@example.test:rwpass123456 \ + "$BASE_URL/caldav/calendars/public/smoke-cancelled-occurrence.ics")" +echo "$CALDAV_ICS" | grep -q 'RRULE:FREQ=WEEKLY' +echo "$CALDAV_ICS" | grep -q 'EXDATE;TZID=Europe/London:20260506T123000' +EVENT_ID="$(curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" | python3 -c "import json,sys; data=json.load(sys.stdin)['data']; print([e for e in data if e.get('uid')=='smoke-cancelled-occurrence-001'][0]['id'])")" +OCC_RESP="$(curl -fsS -H 'X-WP-User: admin' \ + "$BASE_URL/wp-json/calendar/v1/events/${EVENT_ID}/occurrences?from=2026-05-01&months=1")" +python3 - <<'PY' "$OCC_RESP" +import json,sys +days=[x["occurrence_start"][:10] for x in json.loads(sys.argv[1])["data"]] +assert "2026-05-06" not in days +assert "2026-05-13" in days +PY +trap - EXIT +cleanup_tmp_ics + +echo "[smoke] checking future-only default" +curl -fsS "$BASE_URL/calendar" | grep -q 'id="futureOnly" type="checkbox" checked' + +echo "[smoke] all checks passed" diff --git a/fixture/__pycache__/server.cpython-313.pyc b/fixture/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000..d4f0cde Binary files /dev/null and b/fixture/__pycache__/server.cpython-313.pyc differ diff --git a/fixture/fixture.db b/fixture/fixture.db new file mode 100644 index 0000000..086c404 Binary files /dev/null and b/fixture/fixture.db differ diff --git a/fixture/http_trace.log b/fixture/http_trace.log new file mode 100644 index 0000000..bbcb132 --- /dev/null +++ b/fixture/http_trace.log @@ -0,0 +1,1164 @@ +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 43446\r\n\r\n\n\n\n \n \n Calendar\n \n\n\n\n
Calendar Fixture Public Calendar UI
\n
\n
\n
\n
\n
\n \n
\n
\n
\n \n \n \n \n \n
\n
\n ICS Link\n CalDAV\n
\n
\n \n
\n
\n
\n
\n
\n
\n

Account Login

\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n

Register

\n
\n
\n
\n
\n
\n
\n
\n

Password Recovery

\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n

Event Details

\n
\n
\n
\n
\n
\n
\n
\n
\n

Event

\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n \n \n \n
\n
\n

Delete Single Occurrence (Exception)

\n
\n \n \n
\n
\n \n
\n \n
\n
\n
\n
\n\n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 6585\r\n\r\n{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"b2246c0ba36be704\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"97ffb830a549901c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"start_datetime\": \"2026-04-06T09:00:00+01:00\", \"end_datetime\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 10, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0f947e3952fcf681\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"start_datetime\": \"2026-04-08T12:30:00+01:00\", \"end_datetime\": \"2026-04-08T13:30:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4f06e2ef4d6d8e38\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-30T17:00:00+01:00\", \"end_datetime\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-08-31\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"927953eb5c8dd26c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 6, \"uid\": \"fixture-ce-006@calendar-wp-plugin\", \"title\": \"Annual Conference\", \"description\": \"Annual community conference.\", \"location\": \"Main Hall\", \"category\": \"Events\", \"all_day_event\": false, \"start_datetime\": \"2026-06-15T10:00:00+01:00\", \"end_datetime\": \"2026-06-15T17:00:00+01:00\", \"repeat_type\": \"yearly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"93cf1bad962c75f4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"start_datetime\": \"2026-04-07T15:00:00+01:00\", \"end_datetime\": \"2026-04-07T16:00:00+01:00\", \"repeat_type\": \"custom\", \"repeat_interval\": 2, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-07-31\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7201c1345f1b2573\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 8, \"uid\": \"fixture-ce-008@calendar-wp-plugin\", \"title\": \"DST Validation Event\", \"description\": \"Validates DST transition rendering.\", \"location\": \"Lab\", \"category\": \"QA\", \"all_day_event\": false, \"start_datetime\": \"2026-10-25T00:30:00+01:00\", \"end_datetime\": \"2026-10-25T02:30:00+00:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a81452cded01c6a1\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 9, \"uid\": \"fixture-ce-009@calendar-wp-plugin\", \"title\": \"Leap Day Marker\", \"description\": \"Leap day recurrence behavior.\", \"location\": \"Calendar\", \"category\": \"QA\", \"all_day_event\": false, \"start_datetime\": \"2028-02-29T09:00:00+00:00\", \"end_datetime\": \"2028-02-29T10:00:00+00:00\", \"repeat_type\": \"yearly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"978f53232a5bb40f\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}, {\"id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"start_datetime\": \"2026-04-03T14:00:00+01:00\", \"end_datetime\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 8, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"aff80ba990836966\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:53+00:00\"}], \"meta\": {\"count\": 10}}", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c070ea0768ce7e3b\"", "Content-Length": "3132"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"c070ea0768ce7e3b\"\r\nContent-Length: 3132\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b2246c0ba36be704\"", "Content-Length": "364"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"b2246c0ba36be704\"\r\nContent-Length: 364\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "ETag": "\"864b421e254e30ac\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nETag: \"864b421e254e30ac\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"864b421e254e30ac\"", "Content-Length": "288"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"864b421e254e30ac\"\r\nContent-Length: 288\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "ETag": "\"44d2d2c64b2e5412\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nETag: \"44d2d2c64b2e5412\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"44d2d2c64b2e5412\"", "Content-Length": "319"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"44d2d2c64b2e5412\"\r\nContent-Length: 319\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "ETag": "\"fa7dfc8cbf381fc0\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nETag: \"fa7dfc8cbf381fc0\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"fa7dfc8cbf381fc0\"", "Content-Length": "327"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"fa7dfc8cbf381fc0\"\r\nContent-Length: 327\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "ETag": "\"3f65af9c5527a154\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nETag: \"3f65af9c5527a154\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3f65af9c5527a154\"", "Content-Length": "322"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"3f65af9c5527a154\"\r\nContent-Length: 322\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 673\r\n\r\n{\"data\": {\"id\": 15, \"uid\": \"98fd8f149a14e2dddeb7@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"530723cfffec98c3\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:54+00:00\"}}", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"006c790da9929803\"", "Content-Length": "420"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"006c790da9929803\"\r\nContent-Length: 420\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:98fd8f149a14e2dddeb7@calendar-wp-plugin\r\nDTSTAMP:20260330T140854Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 655\r\n\r\n{\"data\": {\"id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"5eeeb56da7d6ae79\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:08:54+00:00\"}}", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 5212\r\n\r\n{\"data\": [{\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-06T10:00:00+00:00\", \"occurrence_end\": \"2026-03-06T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-07T10:00:00+00:00\", \"occurrence_end\": \"2026-03-07T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-08T10:00:00+00:00\", \"occurrence_end\": \"2026-03-08T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-09T10:00:00+00:00\", \"occurrence_end\": \"2026-03-09T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-10T10:00:00+00:00\", \"occurrence_end\": \"2026-03-10T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-12T10:00:00+00:00\", \"occurrence_end\": \"2026-03-12T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-13T10:00:00+00:00\", \"occurrence_end\": \"2026-03-13T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-14T10:00:00+00:00\", \"occurrence_end\": \"2026-03-14T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-15T10:00:00+00:00\", \"occurrence_end\": \"2026-03-15T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-16T10:00:00+00:00\", \"occurrence_end\": \"2026-03-16T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-17T10:00:00+00:00\", \"occurrence_end\": \"2026-03-17T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-18T10:00:00+00:00\", \"occurrence_end\": \"2026-03-18T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-19T10:00:00+00:00\", \"occurrence_end\": \"2026-03-19T11:00:00+00:00\", \"repeat_type\": \"daily\"}]}", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 4906\r\n\r\n{\"data\": [{\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-06T10:00:00+00:00\", \"occurrence_end\": \"2026-03-06T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-07T10:00:00+00:00\", \"occurrence_end\": \"2026-03-07T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-08T10:00:00+00:00\", \"occurrence_end\": \"2026-03-08T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-09T10:00:00+00:00\", \"occurrence_end\": \"2026-03-09T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-12T10:00:00+00:00\", \"occurrence_end\": \"2026-03-12T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-13T10:00:00+00:00\", \"occurrence_end\": \"2026-03-13T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-14T10:00:00+00:00\", \"occurrence_end\": \"2026-03-14T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-15T10:00:00+00:00\", \"occurrence_end\": \"2026-03-15T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-16T10:00:00+00:00\", \"occurrence_end\": \"2026-03-16T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-17T10:00:00+00:00\", \"occurrence_end\": \"2026-03-17T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-18T10:00:00+00:00\", \"occurrence_end\": \"2026-03-18T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ddec804fa982f5df9ed5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-19T10:00:00+00:00\", \"occurrence_end\": \"2026-03-19T11:00:00+00:00\", \"repeat_type\": \"daily\"}]}", "body_truncated": false}} +{"ts": "2026-03-30T14:08:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:08:54 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:08:54 GMT\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 43446\r\n\r\n\n\n\n \n \n Calendar\n \n\n\n\n
Calendar Fixture Public Calendar UI
\n
\n
\n
\n
\n
\n \n
\n
\n
\n \n \n \n \n \n
\n
\n ICS Link\n CalDAV\n
\n
\n \n
\n
\n
\n
\n
\n
\n

Account Login

\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n

Register

\n
\n
\n
\n
\n
\n
\n
\n

Password Recovery

\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n

Event Details

\n
\n
\n
\n
\n
\n
\n
\n
\n

Event

\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n \n \n \n
\n
\n

Delete Single Occurrence (Exception)

\n
\n \n \n
\n
\n \n
\n \n
\n
\n
\n
\n\n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:09:43+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14085", "If-Match": "\"b448b4adc3c325bc\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19190330T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19201025T030000\r\nRDATE:19201025T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19210403T020000\r\nRDATE:19210403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19211003T030000\r\nRDATE:19211003T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19220326T020000\r\nRDATE:19220326T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19221008T030000\r\nRDATE:19221008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19230422T020000\r\nRDATE:19230422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19240413T020000\r\nRDATE:19240413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19230916T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19250419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19270410T020000\r\nRDATE:19270410T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19280422T020000\r\nRDATE:19280422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19290421T020000\r\nRDATE:19290421T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19300413T020000\r\nRDATE:19300413T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19310419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19251004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19330409T020000\r\nRDATE:19330409T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19331008T030000\r\nRDATE:19331008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19340422T020000\r\nRDATE:19340422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19350414T020000\r\nRDATE:19350414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19360419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19380410T020000\r\nRDATE:19380410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19341007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19390416T020000\r\nRDATE:19390416T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19400225T020000\r\nRDATE:19400225T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410504T020000\r\nRDATE:19410504T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410810T030000\r\nRDATE:19410810T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420405T020000\r\nRDATE:19420405T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420809T030000\r\nRDATE:19420809T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430404T020000\r\nRDATE:19430404T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430815T030000\r\nRDATE:19430815T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440402T020000\r\nRDATE:19440402T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440917T030000\r\nRDATE:19440917T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450402T020000\r\nRDATE:19450402T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19391119T030000\r\nRDATE:19391119T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450715T030000\r\nRDATE:19450715T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19460414T020000\r\nRDATE:19460414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470316T020000\r\nRDATE:19470316T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470413T020000\r\nRDATE:19470413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19451007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470810T030000\r\nRDATE:19470810T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19471102T030000\r\nRDATE:19471102T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19480314T020000\r\nRDATE:19480314T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19490403T020000\r\nRDATE:19490403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19481031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19501022T030000\r\nRDATE:19501022T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19511021T030000\r\nRDATE:19511021T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19521026T030000\r\nRDATE:19521026T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19500416T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19540411T020000\r\nRDATE:19540411T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19550417T020000\r\nRDATE:19550417T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19560422T020000\r\nRDATE:19560422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19570414T020000\r\nRDATE:19570414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19580420T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19600410T020000\r\nRDATE:19600410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19531004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19610326T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19640322T020000\r\nRDATE:19640322T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19611029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19651024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19650321T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19671029T030000\r\nRDATE:19671029T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19681027T000000\r\nRDATE:19681027T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19680218T020000\r\nRDATE:19680218T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19711031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19761024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19720319T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19781029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19811025T020000\r\nRDATE:19811025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19821024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19841028T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19881023T020000\r\nRDATE:19881023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19891029T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19931024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19810329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19961027T020000\r\nRDATE:19961027T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:(DST)\r\nDTSTART:19970330T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:(STD)\r\nDTSTART:19971026T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nLAST-MODIFIED:20260330T140943Z\r\nDTSTAMP:20260330T140943Z\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nSUMMARY:test 8\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T160000\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nDESCRIPTION:Default Mozilla Description\r\nSEQUENCE:1\r\nX-MOZ-GENERATION:1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:09:43 GMT", "ETag": "\"075d4a0c8d56a83e\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:09:43 GMT\r\nETag: \"075d4a0c8d56a83e\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:09:43+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:09:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "14799"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:09:43 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 14799\r\n\r\n\n\n\n /caldav/calendars/public/1.ics\n \n \n \"b2246c0ba36be704\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"97ffb830a549901c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/3.ics\n \n \n \"0f947e3952fcf681\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/4.ics\n \n \n \"4f06e2ef4d6d8e38\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/5.ics\n \n \n \"927953eb5c8dd26c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/6.ics\n \n \n \"93cf1bad962c75f4\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/7.ics\n \n \n \"7201c1345f1b2573\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/8.ics\n \n \n \"a81452cded01c6a1\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/9.ics\n \n \n \"978f53232a5bb40f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/10.ics\n \n \n \"b7eba5ab03b7f422\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"864b421e254e30ac\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"44d2d2c64b2e5412\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n \"fa7dfc8cbf381fc0\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n \"3f65af9c5527a154\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/15.ics\n \n \n \"006c790da9929803\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:98fd8f149a14e2dddeb7@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"9d5b8db2124188a7\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ddec804fa982f5df9ed5@calendar-wp-plugin\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:Smoke Daily Exception TZ\r\nDESCRIPTION:tz-key\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n \"075d4a0c8d56a83e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nDTSTAMP:20260330T140943Z\r\nSUMMARY:test 8\r\nDESCRIPTION:Default Mozilla Description\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:09:49+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:09:49 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5816"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:09:49 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 5816\r\n\r\n\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"b2246c0ba36be704\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"97ffb830a549901c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n text/calendar; charset=utf-8\"0f947e3952fcf681\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/4.ics\n \n \n text/calendar; charset=utf-8\"4f06e2ef4d6d8e38\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/5.ics\n \n \n text/calendar; charset=utf-8\"927953eb5c8dd26c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/6.ics\n \n \n text/calendar; charset=utf-8\"93cf1bad962c75f4\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/7.ics\n \n \n text/calendar; charset=utf-8\"7201c1345f1b2573\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/8.ics\n \n \n text/calendar; charset=utf-8\"a81452cded01c6a1\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/9.ics\n \n \n text/calendar; charset=utf-8\"978f53232a5bb40f\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/10.ics\n \n \n text/calendar; charset=utf-8\"b7eba5ab03b7f422\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n text/calendar; charset=utf-8\"864b421e254e30ac\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n text/calendar; charset=utf-8\"44d2d2c64b2e5412\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n text/calendar; charset=utf-8\"fa7dfc8cbf381fc0\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n text/calendar; charset=utf-8\"3f65af9c5527a154\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/15.ics\n \n \n text/calendar; charset=utf-8\"006c790da9929803\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/16.ics\n \n \n text/calendar; charset=utf-8\"9d5b8db2124188a7\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n text/calendar; charset=utf-8\"075d4a0c8d56a83e\"\n \n HTTP/1.1 200 OK\n \n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:09:49+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "232", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/15.ics", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:09:49 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "14799"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:09:49 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 14799\r\n\r\n\n\n\n /caldav/calendars/public/1.ics\n \n \n \"b2246c0ba36be704\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"97ffb830a549901c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/3.ics\n \n \n \"0f947e3952fcf681\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/4.ics\n \n \n \"4f06e2ef4d6d8e38\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/5.ics\n \n \n \"927953eb5c8dd26c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/6.ics\n \n \n \"93cf1bad962c75f4\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/7.ics\n \n \n \"7201c1345f1b2573\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/8.ics\n \n \n \"a81452cded01c6a1\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/9.ics\n \n \n \"978f53232a5bb40f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/10.ics\n \n \n \"b7eba5ab03b7f422\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"864b421e254e30ac\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"44d2d2c64b2e5412\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n \"fa7dfc8cbf381fc0\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n \"3f65af9c5527a154\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/15.ics\n \n \n \"006c790da9929803\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:98fd8f149a14e2dddeb7@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"9d5b8db2124188a7\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ddec804fa982f5df9ed5@calendar-wp-plugin\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:Smoke Daily Exception TZ\r\nDESCRIPTION:tz-key\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n \"075d4a0c8d56a83e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nDTSTAMP:20260330T140949Z\r\nSUMMARY:test 8\r\nDESCRIPTION:Default Mozilla Description\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:12:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14085", "If-Match": "\"075d4a0c8d56a83e\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19190330T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19201025T030000\r\nRDATE:19201025T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19210403T020000\r\nRDATE:19210403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19211003T030000\r\nRDATE:19211003T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19220326T020000\r\nRDATE:19220326T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19221008T030000\r\nRDATE:19221008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19230422T020000\r\nRDATE:19230422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19240413T020000\r\nRDATE:19240413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19230916T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19250419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19270410T020000\r\nRDATE:19270410T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19280422T020000\r\nRDATE:19280422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19290421T020000\r\nRDATE:19290421T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19300413T020000\r\nRDATE:19300413T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19310419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19251004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19330409T020000\r\nRDATE:19330409T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19331008T030000\r\nRDATE:19331008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19340422T020000\r\nRDATE:19340422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19350414T020000\r\nRDATE:19350414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19360419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19380410T020000\r\nRDATE:19380410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19341007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19390416T020000\r\nRDATE:19390416T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19400225T020000\r\nRDATE:19400225T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410504T020000\r\nRDATE:19410504T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410810T030000\r\nRDATE:19410810T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420405T020000\r\nRDATE:19420405T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420809T030000\r\nRDATE:19420809T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430404T020000\r\nRDATE:19430404T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430815T030000\r\nRDATE:19430815T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440402T020000\r\nRDATE:19440402T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440917T030000\r\nRDATE:19440917T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450402T020000\r\nRDATE:19450402T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19391119T030000\r\nRDATE:19391119T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450715T030000\r\nRDATE:19450715T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19460414T020000\r\nRDATE:19460414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470316T020000\r\nRDATE:19470316T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470413T020000\r\nRDATE:19470413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19451007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470810T030000\r\nRDATE:19470810T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19471102T030000\r\nRDATE:19471102T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19480314T020000\r\nRDATE:19480314T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19490403T020000\r\nRDATE:19490403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19481031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19501022T030000\r\nRDATE:19501022T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19511021T030000\r\nRDATE:19511021T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19521026T030000\r\nRDATE:19521026T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19500416T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19540411T020000\r\nRDATE:19540411T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19550417T020000\r\nRDATE:19550417T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19560422T020000\r\nRDATE:19560422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19570414T020000\r\nRDATE:19570414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19580420T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19600410T020000\r\nRDATE:19600410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19531004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19610326T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19640322T020000\r\nRDATE:19640322T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19611029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19651024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19650321T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19671029T030000\r\nRDATE:19671029T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19681027T000000\r\nRDATE:19681027T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19680218T020000\r\nRDATE:19680218T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19711031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19761024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19720319T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19781029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19811025T020000\r\nRDATE:19811025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19821024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19841028T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19881023T020000\r\nRDATE:19881023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19891029T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19931024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19810329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19961027T020000\r\nRDATE:19961027T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:(DST)\r\nDTSTART:19970330T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:(STD)\r\nDTSTART:19971026T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nLAST-MODIFIED:20260330T141240Z\r\nDTSTAMP:20260330T141240Z\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nSUMMARY:test 8\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260312T160000\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nDESCRIPTION:Default Mozilla Description\r\nSEQUENCE:1\r\nX-MOZ-GENERATION:1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:12:40 GMT", "ETag": "\"4b22d7e9f955d2a5\""}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:12:40 GMT\r\nETag: \"4b22d7e9f955d2a5\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:12:40+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:12:40 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "14799"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:12:40 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 14799\r\n\r\n\n\n\n /caldav/calendars/public/1.ics\n \n \n \"b2246c0ba36be704\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"97ffb830a549901c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/3.ics\n \n \n \"0f947e3952fcf681\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/4.ics\n \n \n \"4f06e2ef4d6d8e38\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/5.ics\n \n \n \"927953eb5c8dd26c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/6.ics\n \n \n \"93cf1bad962c75f4\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/7.ics\n \n \n \"7201c1345f1b2573\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/8.ics\n \n \n \"a81452cded01c6a1\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/9.ics\n \n \n \"978f53232a5bb40f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/10.ics\n \n \n \"b7eba5ab03b7f422\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"864b421e254e30ac\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"44d2d2c64b2e5412\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n \"fa7dfc8cbf381fc0\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n \"3f65af9c5527a154\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/15.ics\n \n \n \"006c790da9929803\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:98fd8f149a14e2dddeb7@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"9d5b8db2124188a7\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ddec804fa982f5df9ed5@calendar-wp-plugin\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:Smoke Daily Exception TZ\r\nDESCRIPTION:tz-key\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n \"4b22d7e9f955d2a5\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nDTSTAMP:20260330T141240Z\r\nSUMMARY:test 8\r\nDESCRIPTION:Default Mozilla Description\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:12:45+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:12:45 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5816"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:12:45 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 5816\r\n\r\n\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"b2246c0ba36be704\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"97ffb830a549901c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n text/calendar; charset=utf-8\"0f947e3952fcf681\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/4.ics\n \n \n text/calendar; charset=utf-8\"4f06e2ef4d6d8e38\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/5.ics\n \n \n text/calendar; charset=utf-8\"927953eb5c8dd26c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/6.ics\n \n \n text/calendar; charset=utf-8\"93cf1bad962c75f4\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/7.ics\n \n \n text/calendar; charset=utf-8\"7201c1345f1b2573\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/8.ics\n \n \n text/calendar; charset=utf-8\"a81452cded01c6a1\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/9.ics\n \n \n text/calendar; charset=utf-8\"978f53232a5bb40f\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/10.ics\n \n \n text/calendar; charset=utf-8\"b7eba5ab03b7f422\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n text/calendar; charset=utf-8\"864b421e254e30ac\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n text/calendar; charset=utf-8\"44d2d2c64b2e5412\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n text/calendar; charset=utf-8\"fa7dfc8cbf381fc0\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n text/calendar; charset=utf-8\"3f65af9c5527a154\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/15.ics\n \n \n text/calendar; charset=utf-8\"006c790da9929803\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/16.ics\n \n \n text/calendar; charset=utf-8\"9d5b8db2124188a7\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n text/calendar; charset=utf-8\"4b22d7e9f955d2a5\"\n \n HTTP/1.1 200 OK\n \n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 43446\r\n\r\n\n\n\n \n \n Calendar\n \n\n\n\n
Calendar Fixture Public Calendar UI
\n
\n
\n
\n
\n
\n \n
\n
\n
\n \n \n \n \n \n
\n
\n ICS Link\n CalDAV\n
\n
\n \n
\n
\n
\n
\n
\n
\n

Account Login

\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n

Register

\n
\n
\n
\n
\n
\n
\n
\n

Password Recovery

\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n

Event Details

\n
\n
\n
\n
\n
\n
\n
\n
\n

Event

\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n \n \n \n
\n
\n

Delete Single Occurrence (Exception)

\n
\n \n \n
\n
\n \n
\n \n
\n
\n
\n
\n\n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 6585\r\n\r\n{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9e61840edd26b59c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"6f79e4a470012e8c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"start_datetime\": \"2026-04-06T09:00:00+01:00\", \"end_datetime\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 10, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7dd021ab4c51e62e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"start_datetime\": \"2026-04-08T12:30:00+01:00\", \"end_datetime\": \"2026-04-08T13:30:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"382b171c7e467f49\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-30T17:00:00+01:00\", \"end_datetime\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-08-31\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"6a0b1853010a3060\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 6, \"uid\": \"fixture-ce-006@calendar-wp-plugin\", \"title\": \"Annual Conference\", \"description\": \"Annual community conference.\", \"location\": \"Main Hall\", \"category\": \"Events\", \"all_day_event\": false, \"start_datetime\": \"2026-06-15T10:00:00+01:00\", \"end_datetime\": \"2026-06-15T17:00:00+01:00\", \"repeat_type\": \"yearly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0344b53653b1897e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"start_datetime\": \"2026-04-07T15:00:00+01:00\", \"end_datetime\": \"2026-04-07T16:00:00+01:00\", \"repeat_type\": \"custom\", \"repeat_interval\": 2, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-07-31\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f3727cb12d14614a\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 8, \"uid\": \"fixture-ce-008@calendar-wp-plugin\", \"title\": \"DST Validation Event\", \"description\": \"Validates DST transition rendering.\", \"location\": \"Lab\", \"category\": \"QA\", \"all_day_event\": false, \"start_datetime\": \"2026-10-25T00:30:00+01:00\", \"end_datetime\": \"2026-10-25T02:30:00+00:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"2200904959f9c5cd\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 9, \"uid\": \"fixture-ce-009@calendar-wp-plugin\", \"title\": \"Leap Day Marker\", \"description\": \"Leap day recurrence behavior.\", \"location\": \"Calendar\", \"category\": \"QA\", \"all_day_event\": false, \"start_datetime\": \"2028-02-29T09:00:00+00:00\", \"end_datetime\": \"2028-02-29T10:00:00+00:00\", \"repeat_type\": \"yearly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"028aa2cacadd930b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"start_datetime\": \"2026-04-03T14:00:00+01:00\", \"end_datetime\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 8, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"47a32ea36bccb6cf\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}], \"meta\": {\"count\": 10}}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b626877cd3d20036\"", "Content-Length": "3132"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"b626877cd3d20036\"\r\nContent-Length: 3132\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9e61840edd26b59c\"", "Content-Length": "364"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"9e61840edd26b59c\"\r\nContent-Length: 364\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "ETag": "\"e5984c98962fda96\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nETag: \"e5984c98962fda96\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e5984c98962fda96\"", "Content-Length": "288"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"e5984c98962fda96\"\r\nContent-Length: 288\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "ETag": "\"a4f11de675293164\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nETag: \"a4f11de675293164\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a4f11de675293164\"", "Content-Length": "319"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"a4f11de675293164\"\r\nContent-Length: 319\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "ETag": "\"db9706d0865b79d9\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nETag: \"db9706d0865b79d9\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"db9706d0865b79d9\"", "Content-Length": "327"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"db9706d0865b79d9\"\r\nContent-Length: 327\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "ETag": "\"9b7f63d5c08cc7c8\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nETag: \"9b7f63d5c08cc7c8\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9b7f63d5c08cc7c8\"", "Content-Length": "322"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"9b7f63d5c08cc7c8\"\r\nContent-Length: 322\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 673\r\n\r\n{\"data\": {\"id\": 15, \"uid\": \"4e9ee4c86911a55106b9@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0323ee6baedfc4b5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e6f09ba57238f931\"", "Content-Length": "420"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: text/calendar; charset=utf-8\r\nETag: \"e6f09ba57238f931\"\r\nContent-Length: 420\r\n\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:4e9ee4c86911a55106b9@calendar-wp-plugin\r\nDTSTAMP:20260330T141436Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 655\r\n\r\n{\"data\": {\"id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"bbc40060424cca3d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 5212\r\n\r\n{\"data\": [{\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-06T10:00:00+00:00\", \"occurrence_end\": \"2026-03-06T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-07T10:00:00+00:00\", \"occurrence_end\": \"2026-03-07T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-08T10:00:00+00:00\", \"occurrence_end\": \"2026-03-08T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-09T10:00:00+00:00\", \"occurrence_end\": \"2026-03-09T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-10T10:00:00+00:00\", \"occurrence_end\": \"2026-03-10T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-12T10:00:00+00:00\", \"occurrence_end\": \"2026-03-12T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-13T10:00:00+00:00\", \"occurrence_end\": \"2026-03-13T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-14T10:00:00+00:00\", \"occurrence_end\": \"2026-03-14T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-15T10:00:00+00:00\", \"occurrence_end\": \"2026-03-15T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-16T10:00:00+00:00\", \"occurrence_end\": \"2026-03-16T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-17T10:00:00+00:00\", \"occurrence_end\": \"2026-03-17T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-18T10:00:00+00:00\", \"occurrence_end\": \"2026-03-18T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-19T10:00:00+00:00\", \"occurrence_end\": \"2026-03-19T11:00:00+00:00\", \"repeat_type\": \"daily\"}]}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT"}, "body": "HTTP/1.0 204 No Content\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:36 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 4906\r\n\r\n{\"data\": [{\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-06T10:00:00+00:00\", \"occurrence_end\": \"2026-03-06T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-07T10:00:00+00:00\", \"occurrence_end\": \"2026-03-07T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-08T10:00:00+00:00\", \"occurrence_end\": \"2026-03-08T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-09T10:00:00+00:00\", \"occurrence_end\": \"2026-03-09T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-12T10:00:00+00:00\", \"occurrence_end\": \"2026-03-12T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-13T10:00:00+00:00\", \"occurrence_end\": \"2026-03-13T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-14T10:00:00+00:00\", \"occurrence_end\": \"2026-03-14T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-15T10:00:00+00:00\", \"occurrence_end\": \"2026-03-15T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-16T10:00:00+00:00\", \"occurrence_end\": \"2026-03-16T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-17T10:00:00+00:00\", \"occurrence_end\": \"2026-03-17T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-18T10:00:00+00:00\", \"occurrence_end\": \"2026-03-18T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-19T10:00:00+00:00\", \"occurrence_end\": \"2026-03-19T11:00:00+00:00\", \"repeat_type\": \"daily\"}]}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:18080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:37 GMT", "ETag": "\"1c3e46425e90df29\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:37 GMT\r\nETag: \"1c3e46425e90df29\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:14:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:37 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 11006\r\n\r\n{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9e61840edd26b59c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"6f79e4a470012e8c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"start_datetime\": \"2026-04-06T09:00:00+01:00\", \"end_datetime\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 10, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7dd021ab4c51e62e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"start_datetime\": \"2026-04-08T12:30:00+01:00\", \"end_datetime\": \"2026-04-08T13:30:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"382b171c7e467f49\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-30T17:00:00+01:00\", \"end_datetime\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-08-31\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"6a0b1853010a3060\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 6, \"uid\": \"fixture-ce-006@calendar-wp-plugin\", \"title\": \"Annual Conference\", \"description\": \"Annual community conference.\", \"location\": \"Main Hall\", \"category\": \"Events\", \"all_day_event\": false, \"start_datetime\": \"2026-06-15T10:00:00+01:00\", \"end_datetime\": \"2026-06-15T17:00:00+01:00\", \"repeat_type\": \"yearly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0344b53653b1897e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"start_datetime\": \"2026-04-07T15:00:00+01:00\", \"end_datetime\": \"2026-04-07T16:00:00+01:00\", \"repeat_type\": \"custom\", \"repeat_interval\": 2, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-07-31\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f3727cb12d14614a\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 8, \"uid\": \"fixture-ce-008@calendar-wp-plugin\", \"title\": \"DST Validation Event\", \"description\": \"Validates DST transition rendering.\", \"location\": \"Lab\", \"category\": \"QA\", \"all_day_event\": false, \"start_datetime\": \"2026-10-25T00:30:00+01:00\", \"end_datetime\": \"2026-10-25T02:30:00+00:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"2200904959f9c5cd\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 9, \"uid\": \"fixture-ce-009@calendar-wp-plugin\", \"title\": \"Leap Day Marker\", \"description\": \"Leap day recurrence behavior.\", \"location\": \"Calendar\", \"category\": \"QA\", \"all_day_event\": false, \"start_datetime\": \"2028-02-29T09:00:00+00:00\", \"end_datetime\": \"2028-02-29T10:00:00+00:00\", \"repeat_type\": \"yearly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"028aa2cacadd930b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"start_datetime\": \"2026-04-03T14:00:00+01:00\", \"end_datetime\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 8, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1062edf9fc69e84f\\\"\", \"sync_version\": 2, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 11, \"uid\": \"smoke-vtimezone-parser-001\", \"title\": \"Smoke VTIMEZONE Parse\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-23T15:00:00+01:00\", \"end_datetime\": \"2026-04-23T16:00:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e5984c98962fda96\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 12, \"uid\": \"smoke-monthly-nth-001\", \"title\": \"Smoke Monthly Nth\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-26T15:00:00+01:00\", \"end_datetime\": \"2026-04-26T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a4f11de675293164\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 13, \"uid\": \"smoke-monthly-ordinal-001\", \"title\": \"Smoke Monthly Ordinal\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-11T15:00:00+01:00\", \"end_datetime\": \"2026-04-11T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 2, \"repeat_nth_weekday\": 6, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"db9706d0865b79d9\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 14, \"uid\": \"smoke-monthly-last-001\", \"title\": \"Smoke Monthly Last\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-25T15:00:00+01:00\", \"end_datetime\": \"2026-04-25T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": -1, \"repeat_nth_weekday\": 6, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9b7f63d5c08cc7c8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 15, \"uid\": \"4e9ee4c86911a55106b9@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e6f09ba57238f931\\\"\", \"sync_version\": 2, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 16, \"uid\": \"771bbfbfb45098265113@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"904acb970acebc29\\\"\", \"sync_version\": 3, \"updated_at\": \"2026-03-30T14:14:36+00:00\"}, {\"id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+01:00\", \"end_datetime\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1c3e46425e90df29\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:14:37+00:00\"}], \"meta\": {\"count\": 17}}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:37 GMT\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 4474\r\n\r\n{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-06T10:00:00+01:00\", \"occurrence_end\": \"2026-03-06T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-07T10:00:00+01:00\", \"occurrence_end\": \"2026-03-07T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-08T10:00:00+01:00\", \"occurrence_end\": \"2026-03-08T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-09T10:00:00+01:00\", \"occurrence_end\": \"2026-03-09T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-12T10:00:00+01:00\", \"occurrence_end\": \"2026-03-12T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-13T10:00:00+01:00\", \"occurrence_end\": \"2026-03-13T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-14T10:00:00+01:00\", \"occurrence_end\": \"2026-03-14T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-15T10:00:00+01:00\", \"occurrence_end\": \"2026-03-15T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-16T10:00:00+01:00\", \"occurrence_end\": \"2026-03-16T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-17T10:00:00+01:00\", \"occurrence_end\": \"2026-03-17T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-18T10:00:00+01:00\", \"occurrence_end\": \"2026-03-18T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-19T10:00:00+01:00\", \"occurrence_end\": \"2026-03-19T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_truncated": false}} +{"ts": "2026-03-30T14:14:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:18080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:14:37 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:14:37 GMT\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 43446\r\n\r\n\n\n\n \n \n Calendar\n \n\n\n\n
Calendar Fixture Public Calendar UI
\n
\n
\n
\n
\n
\n \n
\n
\n
\n \n \n \n \n \n
\n
\n ICS Link\n CalDAV\n
\n
\n \n
\n
\n
\n
\n
\n
\n

Account Login

\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n

Register

\n
\n
\n
\n
\n
\n
\n
\n

Password Recovery

\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n

Event Details

\n
\n
\n
\n
\n
\n
\n
\n
\n

Event

\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n \n \n \n
\n
\n

Delete Single Occurrence (Exception)

\n
\n \n \n
\n
\n \n
\n \n
\n
\n
\n
\n\n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14085", "If-Match": "\"4b22d7e9f955d2a5\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19190330T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19201025T030000\r\nRDATE:19201025T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19210403T020000\r\nRDATE:19210403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19211003T030000\r\nRDATE:19211003T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19220326T020000\r\nRDATE:19220326T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19221008T030000\r\nRDATE:19221008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19230422T020000\r\nRDATE:19230422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19240413T020000\r\nRDATE:19240413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19230916T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19250419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19270410T020000\r\nRDATE:19270410T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19280422T020000\r\nRDATE:19280422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19290421T020000\r\nRDATE:19290421T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19300413T020000\r\nRDATE:19300413T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19310419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19251004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19330409T020000\r\nRDATE:19330409T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19331008T030000\r\nRDATE:19331008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19340422T020000\r\nRDATE:19340422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19350414T020000\r\nRDATE:19350414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19360419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19380410T020000\r\nRDATE:19380410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19341007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19390416T020000\r\nRDATE:19390416T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19400225T020000\r\nRDATE:19400225T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410504T020000\r\nRDATE:19410504T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410810T030000\r\nRDATE:19410810T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420405T020000\r\nRDATE:19420405T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420809T030000\r\nRDATE:19420809T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430404T020000\r\nRDATE:19430404T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430815T030000\r\nRDATE:19430815T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440402T020000\r\nRDATE:19440402T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440917T030000\r\nRDATE:19440917T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450402T020000\r\nRDATE:19450402T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19391119T030000\r\nRDATE:19391119T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450715T030000\r\nRDATE:19450715T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19460414T020000\r\nRDATE:19460414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470316T020000\r\nRDATE:19470316T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470413T020000\r\nRDATE:19470413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19451007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470810T030000\r\nRDATE:19470810T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19471102T030000\r\nRDATE:19471102T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19480314T020000\r\nRDATE:19480314T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19490403T020000\r\nRDATE:19490403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19481031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19501022T030000\r\nRDATE:19501022T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19511021T030000\r\nRDATE:19511021T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19521026T030000\r\nRDATE:19521026T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19500416T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19540411T020000\r\nRDATE:19540411T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19550417T020000\r\nRDATE:19550417T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19560422T020000\r\nRDATE:19560422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19570414T020000\r\nRDATE:19570414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19580420T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19600410T020000\r\nRDATE:19600410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19531004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19610326T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19640322T020000\r\nRDATE:19640322T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19611029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19651024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19650321T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19671029T030000\r\nRDATE:19671029T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19681027T000000\r\nRDATE:19681027T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19680218T020000\r\nRDATE:19680218T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19711031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19761024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19720319T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19781029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19811025T020000\r\nRDATE:19811025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19821024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19841028T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19881023T020000\r\nRDATE:19881023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19891029T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19931024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19810329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19961027T020000\r\nRDATE:19961027T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:(DST)\r\nDTSTART:19970330T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:(STD)\r\nDTSTART:19971026T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nLAST-MODIFIED:20260330T141507Z\r\nDTSTAMP:20260330T141507Z\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nSUMMARY:test 8\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260312T160000\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nDESCRIPTION:Default Mozilla Description\r\nSEQUENCE:1\r\nX-MOZ-GENERATION:1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:07 GMT", "ETag": "\"898b4e9c84b4dade\""}, "body": "HTTP/1.0 201 Created\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:07 GMT\r\nETag: \"898b4e9c84b4dade\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:07+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:07 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "15724"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:07 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 15724\r\n\r\n\n\n\n /caldav/calendars/public/1.ics\n \n \n \"9e61840edd26b59c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"6f79e4a470012e8c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/3.ics\n \n \n \"7dd021ab4c51e62e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/4.ics\n \n \n \"382b171c7e467f49\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/5.ics\n \n \n \"6a0b1853010a3060\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/6.ics\n \n \n \"0344b53653b1897e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/7.ics\n \n \n \"f3727cb12d14614a\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/8.ics\n \n \n \"2200904959f9c5cd\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/9.ics\n \n \n \"028aa2cacadd930b\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/10.ics\n \n \n \"1062edf9fc69e84f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"e5984c98962fda96\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"a4f11de675293164\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n \"db9706d0865b79d9\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n \"9b7f63d5c08cc7c8\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/15.ics\n \n \n \"e6f09ba57238f931\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:4e9ee4c86911a55106b9@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"904acb970acebc29\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:771bbfbfb45098265113@calendar-wp-plugin\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke Daily Exception TZ\r\nDESCRIPTION:tz-key\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-exdate-import.ics\n \n \n \"1c3e46425e90df29\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-exdate-import-001\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:Smoke EXDATE Import\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n \"898b4e9c84b4dade\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nDTSTAMP:20260330T141507Z\r\nSUMMARY:test 8\r\nDESCRIPTION:Default Mozilla Description\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260312T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:13+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:13 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6140"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:13 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 6140\r\n\r\n\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"9e61840edd26b59c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"6f79e4a470012e8c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n text/calendar; charset=utf-8\"7dd021ab4c51e62e\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/4.ics\n \n \n text/calendar; charset=utf-8\"382b171c7e467f49\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/5.ics\n \n \n text/calendar; charset=utf-8\"6a0b1853010a3060\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/6.ics\n \n \n text/calendar; charset=utf-8\"0344b53653b1897e\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/7.ics\n \n \n text/calendar; charset=utf-8\"f3727cb12d14614a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/8.ics\n \n \n text/calendar; charset=utf-8\"2200904959f9c5cd\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/9.ics\n \n \n text/calendar; charset=utf-8\"028aa2cacadd930b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/10.ics\n \n \n text/calendar; charset=utf-8\"1062edf9fc69e84f\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n text/calendar; charset=utf-8\"e5984c98962fda96\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n text/calendar; charset=utf-8\"a4f11de675293164\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n text/calendar; charset=utf-8\"db9706d0865b79d9\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n text/calendar; charset=utf-8\"9b7f63d5c08cc7c8\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/15.ics\n \n \n text/calendar; charset=utf-8\"e6f09ba57238f931\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/16.ics\n \n \n text/calendar; charset=utf-8\"904acb970acebc29\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-exdate-import.ics\n \n \n text/calendar; charset=utf-8\"1c3e46425e90df29\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n text/calendar; charset=utf-8\"898b4e9c84b4dade\"\n \n HTTP/1.1 200 OK\n \n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:13+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "280", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/16.ics/caldav/calendars/public/15.ics", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:13 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "15724"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:13 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 15724\r\n\r\n\n\n\n /caldav/calendars/public/1.ics\n \n \n \"9e61840edd26b59c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"6f79e4a470012e8c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/3.ics\n \n \n \"7dd021ab4c51e62e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/4.ics\n \n \n \"382b171c7e467f49\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/5.ics\n \n \n \"6a0b1853010a3060\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/6.ics\n \n \n \"0344b53653b1897e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/7.ics\n \n \n \"f3727cb12d14614a\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/8.ics\n \n \n \"2200904959f9c5cd\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/9.ics\n \n \n \"028aa2cacadd930b\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/10.ics\n \n \n \"1062edf9fc69e84f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"e5984c98962fda96\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"a4f11de675293164\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n \"db9706d0865b79d9\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n \"9b7f63d5c08cc7c8\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/15.ics\n \n \n \"e6f09ba57238f931\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:4e9ee4c86911a55106b9@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"904acb970acebc29\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:771bbfbfb45098265113@calendar-wp-plugin\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke Daily Exception TZ\r\nDESCRIPTION:tz-key\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-exdate-import.ics\n \n \n \"1c3e46425e90df29\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-exdate-import-001\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:Smoke EXDATE Import\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n \"898b4e9c84b4dade\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nDTSTAMP:20260330T141513Z\r\nSUMMARY:test 8\r\nDESCRIPTION:Default Mozilla Description\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260312T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:29+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14128", "If-Match": "\"898b4e9c84b4dade\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19190330T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19201025T030000\r\nRDATE:19201025T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19210403T020000\r\nRDATE:19210403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19211003T030000\r\nRDATE:19211003T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19220326T020000\r\nRDATE:19220326T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19221008T030000\r\nRDATE:19221008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19230422T020000\r\nRDATE:19230422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19240413T020000\r\nRDATE:19240413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19230916T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19250419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19270410T020000\r\nRDATE:19270410T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19280422T020000\r\nRDATE:19280422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19290421T020000\r\nRDATE:19290421T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19300413T020000\r\nRDATE:19300413T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19310419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19251004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19330409T020000\r\nRDATE:19330409T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19331008T030000\r\nRDATE:19331008T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19340422T020000\r\nRDATE:19340422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19350414T020000\r\nRDATE:19350414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19360419T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19380410T020000\r\nRDATE:19380410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19341007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19390416T020000\r\nRDATE:19390416T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19400225T020000\r\nRDATE:19400225T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410504T020000\r\nRDATE:19410504T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19410810T030000\r\nRDATE:19410810T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420405T020000\r\nRDATE:19420405T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19420809T030000\r\nRDATE:19420809T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430404T020000\r\nRDATE:19430404T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19430815T030000\r\nRDATE:19430815T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440402T020000\r\nRDATE:19440402T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19440917T030000\r\nRDATE:19440917T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450402T020000\r\nRDATE:19450402T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19391119T030000\r\nRDATE:19391119T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19450715T030000\r\nRDATE:19450715T030000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19460414T020000\r\nRDATE:19460414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470316T020000\r\nRDATE:19470316T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+020000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470413T020000\r\nRDATE:19470413T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19451007T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+020000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19470810T030000\r\nRDATE:19470810T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19471102T030000\r\nRDATE:19471102T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19480314T020000\r\nRDATE:19480314T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19490403T020000\r\nRDATE:19490403T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19481031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19501022T030000\r\nRDATE:19501022T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19511021T030000\r\nRDATE:19511021T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19521026T030000\r\nRDATE:19521026T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19500416T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19540411T020000\r\nRDATE:19540411T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19550417T020000\r\nRDATE:19550417T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19560422T020000\r\nRDATE:19560422T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19570414T020000\r\nRDATE:19570414T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19580420T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19600410T020000\r\nRDATE:19600410T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19531004T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19610326T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19640322T020000\r\nRDATE:19640322T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19611029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19651024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19650321T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19671029T030000\r\nRDATE:19671029T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19681027T000000\r\nRDATE:19681027T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19680218T020000\r\nRDATE:19680218T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19711031T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19761024T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19720319T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19781029T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19811025T020000\r\nRDATE:19811025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19821024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19841028T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19881023T020000\r\nRDATE:19881023T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19891029T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19931024T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19810329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19961027T020000\r\nRDATE:19961027T020000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:(DST)\r\nDTSTART:19970330T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:(STD)\r\nDTSTART:19971026T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nLAST-MODIFIED:20260330T141529Z\r\nDTSTAMP:20260330T141529Z\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nSUMMARY:test 8\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260312T160000\r\nEXDATE;TZID=Europe/London:20260313T160000\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nDESCRIPTION:Default Mozilla Description\r\nSEQUENCE:1\r\nX-MOZ-GENERATION:1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:29 GMT", "ETag": "\"548f92e38ae08226\""}, "body": "HTTP/1.0 200 OK\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:29 GMT\r\nETag: \"548f92e38ae08226\"\r\n\r\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:29+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:29 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "15740"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:29 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 15740\r\n\r\n\n\n\n /caldav/calendars/public/1.ics\n \n \n \"9e61840edd26b59c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"6f79e4a470012e8c\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/3.ics\n \n \n \"7dd021ab4c51e62e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/4.ics\n \n \n \"382b171c7e467f49\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/5.ics\n \n \n \"6a0b1853010a3060\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-005@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Finance Close\r\nDESCRIPTION:Month-end close process.\r\nLOCATION:Finance Office\r\nCATEGORIES:Finance\r\nDTSTART;TZID=Europe/London:20260430T170000\r\nDTEND;TZID=Europe/London:20260430T180000\r\nRRULE:FREQ=MONTHLY;UNTIL=20260831T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/6.ics\n \n \n \"0344b53653b1897e\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-006@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Annual Conference\r\nDESCRIPTION:Annual community conference.\r\nLOCATION:Main Hall\r\nCATEGORIES:Events\r\nDTSTART;TZID=Europe/London:20260615T100000\r\nDTEND;TZID=Europe/London:20260615T170000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/7.ics\n \n \n \"f3727cb12d14614a\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/8.ics\n \n \n \"2200904959f9c5cd\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-008@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:DST Validation Event\r\nDESCRIPTION:Validates DST transition rendering.\r\nLOCATION:Lab\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20261025T003000\r\nDTEND;TZID=Europe/London:20261025T023000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/9.ics\n \n \n \"028aa2cacadd930b\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-009@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Leap Day Marker\r\nDESCRIPTION:Leap day recurrence behavior.\r\nLOCATION:Calendar\r\nCATEGORIES:QA\r\nDTSTART;TZID=Europe/London:20280229T090000\r\nDTEND;TZID=Europe/London:20280229T100000\r\nRRULE:FREQ=YEARLY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/10.ics\n \n \n \"1062edf9fc69e84f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"e5984c98962fda96\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"a4f11de675293164\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n \"db9706d0865b79d9\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n \"9b7f63d5c08cc7c8\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/15.ics\n \n \n \"e6f09ba57238f931\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:4e9ee4c86911a55106b9@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"904acb970acebc29\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:771bbfbfb45098265113@calendar-wp-plugin\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke Daily Exception TZ\r\nDESCRIPTION:tz-key\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-exdate-import.ics\n \n \n \"1c3e46425e90df29\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-exdate-import-001\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:Smoke EXDATE Import\r\nDTSTART;TZID=Europe/London:20260302T100000\r\nDTEND;TZID=Europe/London:20260302T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n \"548f92e38ae08226\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:ed617bc7-66c1-474d-a467-86565e680820\r\nDTSTAMP:20260330T141529Z\r\nSUMMARY:test 8\r\nDESCRIPTION:Default Mozilla Description\r\nDTSTART;TZID=Europe/London:20260302T160000\r\nDTEND;TZID=Europe/London:20260302T170000\r\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\r\nEXDATE;TZID=Europe/London:20260312T160000,20260313T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:15:35+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:15:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6140"}, "body": "HTTP/1.0 207 Multi-Status\r\nServer: CalendarFixture/0.1 Python/3.13.5\r\nDate: Mon, 30 Mar 2026 14:15:35 GMT\r\nContent-Type: application/xml; charset=utf-8\r\nContent-Length: 6140\r\n\r\n\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"9e61840edd26b59c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"6f79e4a470012e8c\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n text/calendar; charset=utf-8\"7dd021ab4c51e62e\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/4.ics\n \n \n text/calendar; charset=utf-8\"382b171c7e467f49\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/5.ics\n \n \n text/calendar; charset=utf-8\"6a0b1853010a3060\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/6.ics\n \n \n text/calendar; charset=utf-8\"0344b53653b1897e\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/7.ics\n \n \n text/calendar; charset=utf-8\"f3727cb12d14614a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/8.ics\n \n \n text/calendar; charset=utf-8\"2200904959f9c5cd\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/9.ics\n \n \n text/calendar; charset=utf-8\"028aa2cacadd930b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/10.ics\n \n \n text/calendar; charset=utf-8\"1062edf9fc69e84f\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n text/calendar; charset=utf-8\"e5984c98962fda96\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n text/calendar; charset=utf-8\"a4f11de675293164\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-ordinal.ics\n \n \n text/calendar; charset=utf-8\"db9706d0865b79d9\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-monthly-last.ics\n \n \n text/calendar; charset=utf-8\"9b7f63d5c08cc7c8\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/15.ics\n \n \n text/calendar; charset=utf-8\"e6f09ba57238f931\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/16.ics\n \n \n text/calendar; charset=utf-8\"904acb970acebc29\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/smoke-exdate-import.ics\n \n \n text/calendar; charset=utf-8\"1c3e46425e90df29\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/ed617bc7-66c1-474d-a467-86565e680820.ics\n \n \n text/calendar; charset=utf-8\"548f92e38ae08226\"\n \n HTTP/1.1 200 OK\n \n\n\n", "body_truncated": false}} +{"ts": "2026-03-30T14:20:15+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:15 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"34af04b9ccbadb9d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:16:53+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a33ed", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:20:15+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:15 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"138a8bec591c96d2\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142015Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T142015Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T142015Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T142015Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T14:20:15+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:15 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"34af04b9ccbadb9d\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142015Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T14:20:15+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:15 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:15+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:15 GMT", "ETag": "\"e13f867894f5d240\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e13f867894f5d240\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T142016Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "ETag": "\"3a6ba02a67b14874\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3a6ba02a67b14874\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T142016Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "ETag": "\"1710ae19fe56d567\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1710ae19fe56d567\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T142016Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "ETag": "\"98bbc37be6ca6788\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"98bbc37be6ca6788\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T142016Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 15, \"uid\": \"809b5d43371ed23850b0@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1f3f52be73f1461b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:20:16+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"190e382546a84122\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:809b5d43371ed23850b0@calendar-wp-plugin\r\nDTSTAMP:20260330T142016Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"bc3799c352e91742\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:20:16+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"8483fc55d1de7d7606fc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "ETag": "\"d037ce8fd6055b51\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"34af04b9ccbadb9d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:16:53+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a33ed", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:20:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:16 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:20:26+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/admin/diagnostics?limit=2&as=admin", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:20:26 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "2521"}, "body": "{\"data\": [{\"ts\": \"2026-03-30T14:20:16+00:00\", \"client\": \"127.0.0.1\", \"method\": \"GET\", \"path\": \"/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1\", \"request\": {\"headers\": {\"Host\": \"127.0.0.1:8080\", \"User-Agent\": \"curl/8.14.1\", \"Accept\": \"*/*\", \"X-WP-User\": \"admin\"}, \"body\": \"\", \"body_bytes\": 0, \"body_truncated\": false}, \"response\": {\"status\": 200, \"headers\": {\"Server\": \"CalendarFixture/0.1 Python/3.13.5\", \"Date\": \"Mon, 30 Mar 2026 14:20:16 GMT\", \"Content-Type\": \"application/json; charset=utf-8\", \"Content-Length\": \"4474\"}, \"body\": \"{\\\"data\\\": [{\\\"event_id\\\": 17, \\\"uid\\\": \\\"smoke-exdate-import-001\\\", \\\"title\\\": \\\"Smoke EXDATE Import\\\", \\\"description\\\": \\\"\\\", \\\"location\\\": \\\"\\\", \\\"category\\\": \\\"\\\", \\\"all_day_event\\\": false, \\\"occurrence_start\\\": \\\"2026-03-02T10:00:00+01:00\\\", \\\"occurrence_end\\\": \\\"2026-03-02T11:00:00+01:00\\\", \\\"repeat_type\\\": \\\"daily\\\"}, {\\\"event_id\\\": 17, \\\"uid\\\": \\\"smoke-exdate-import-001\\\", \\\"title\\\": \\\"Smoke EXDATE Import\\\", \\\"description\\\": \\\"\\\", \\\"location\\\": \\\"\\\", \\\"category\\\": \\\"\\\", \\\"all_day_event\\\": false, \\\"occurrence_start\\\": \\\"2026-03-03T10:00:00+01:00\\\", \\\"occurrence_end\\\": \\\"2026-03-03T11:00:00+01:00\\\", \\\"repeat_type\\\": \\\"daily\\\"}, {\\\"e ...(truncated)", "body_bytes": 2521, "body_truncated": false}} +{"ts": "2026-03-30T14:22:19+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:22:19 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5799"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:22:20+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "1071", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/smoke-exdate-import.ics/caldav/calendars/public/16.ics/caldav/calendars/public/15.ics/caldav/calendars/public/smoke-monthly-last.ics/caldav/calendars/public/smoke-monthly-ordinal.ics/caldav/calendars/public/smoke-monthly-nth.ics/caldav/calendars/public/smoke-vtimezone.ics/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/calendars/public/1.ics", "body_bytes": 1071, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:22:20 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "14800"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142220Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:22:44+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "If-Match": "\"d037ce8fd6055b51\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:22:44 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:22:49+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "If-Match": "\"d2a6091e74fce26b\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:22:49 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:23:21+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14110", "If-None-Match": "*", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:21 GMT", "ETag": "\"5e4c0898488797e4\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:23:21+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "body_bytes": 266, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:21 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13893"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142321Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:23:23+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:23 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:23:31+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14085", "If-Match": "\"5e4c0898488797e4\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:31 GMT", "ETag": "\"6638686e8b73b19c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:23:31+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "body_bytes": 266, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:31 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13936"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142331Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:23:33+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:33 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:23:44+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14128", "If-Match": "\"6638686e8b73b19c\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:44 GMT", "ETag": "\"fc290607cac191f8\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:23:44+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "body_bytes": 266, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:44 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13952"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142344Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:23:46+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:46 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:23:50+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/caldav/calendars/public/267d03e6-66cc-458b-aaf2-400f5b0e2108.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "If-Match": "\"fc290607cac191f8\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:23:50 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:24:33+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14116", "If-None-Match": "*", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:24:33 GMT", "ETag": "\"32987e446d88f0f9\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:24:33+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "body_bytes": 266, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:24:33 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13902"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142433Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:24:36+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:24:36 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:24:53+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14094", "If-Match": "\"32987e446d88f0f9\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:24:53 GMT", "ETag": "\"05a6e14b3e8da6a4\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:24:53+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "body_bytes": 266, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:24:53 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13945"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142453Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:25:01+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:25:01 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:25:26+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14137", "If-Match": "\"05a6e14b3e8da6a4\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:25:26 GMT", "ETag": "\"92da99cdae305c8c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:25:26+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "266", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "body_bytes": 266, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:25:26 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13961"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142526Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:25:35+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:25:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:25:46+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/caldav/calendars/public/ce2caf8e-6197-40a3-9c7b-e6eae7041903.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "If-Match": "\"92da99cdae305c8c\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:25:46 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:26:01+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/7.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14137", "If-Match": "\"279b10d3b419d6e8\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:01 GMT", "ETag": "\"37b3e390499adf51\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:26:01+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/7.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:01 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13055"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142601Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:26:11+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:11 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5168"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:26:23+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:23 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5168"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:26:38+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/4.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14108", "If-Match": "\"f4b1379e60ebbe47\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:38 GMT", "ETag": "\"13292670bde364d3\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:26:38+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/4.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:38 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "13098"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"34af04b9ccbadb9d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T142638Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"a33ed4025fd8b61d\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:26:58+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:26:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5168"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:27:17+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:27:17 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5168"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:27:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:27:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "7885"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:27:41+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:27:41 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4227"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"al", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:27:57+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:27:57 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "5168"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"34af04b9ccbadb9d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"a33ed4025fd8b61d\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:34:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:34:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9739"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"34af04b9ccbadb9d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:16:53+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a33ed", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:34:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:34:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3db8720849bf27c9\"", "Content-Length": "4502"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143454Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T143454Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T143454Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T143454Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T14:34:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:34:54 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"34af04b9ccbadb9d\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143454Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T14:34:54+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 409, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:34:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "84"}, "body": "{\"error\": {\"code\": \"conflict_error\", \"message\": \"occurrence already has exception\"}}", "body_bytes": 84, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4b16804fb6cdace4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:34:59+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"2bc41", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b06c8be2295443df\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4b16804fb6cdace4\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "ETag": "\"f7e38a114c5f0aa1\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f7e38a114c5f0aa1\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "ETag": "\"afd42f662e664a70\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"afd42f662e664a70\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "ETag": "\"ff1619de2d36d47d\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ff1619de2d36d47d\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "ETag": "\"3c34ae5dcf09bc94\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3c34ae5dcf09bc94\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 15, \"uid\": \"671dfedc5209e0cbc285@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"318e71f9de064c57\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:35:07+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5a4321ef70dcf8c4\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:671dfedc5209e0cbc285@calendar-wp-plugin\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"da0528511b5553ab\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:35:07+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "ETag": "\"17595aec3f430fad\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4b16804fb6cdace4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:34:59+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"2bc41", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "ETag": "\"9ad47bc1cfbe571b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:35:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:07 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9ad47bc1cfbe571b\"", "Content-Length": "316"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T143507Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 316, "body_truncated": false}} +{"ts": "2026-03-30T14:35:44+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=week&date=2026-04-27", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:44 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "1345"}, "body": "{\"data\": [{\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-29T12:30:00+01:00\", \"occurrence_end\": \"2026-04-29T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-29T12:30:00+01:00\", \"occurrence_end\": \"2026-04-29T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-30T17:00:00+01:00\", \"occurrence_end\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_sta ...(truncated)", "body_bytes": 1345, "body_truncated": false}} +{"ts": "2026-03-30T14:35:54+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-27", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:54 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:35:57+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-28", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:57 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:35:57+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-29", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:57 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "667"}, "body": "{\"data\": [{\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-29T12:30:00+01:00\", \"occurrence_end\": \"2026-04-29T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-29T12:30:00+01:00\", \"occurrence_end\": \"2026-04-29T13:30:00+01:00\", \"repeat_type\": \"weekly\"}], \"meta\": {\"count\": 2, \"view\": \"day\"}}", "body_bytes": 667, "body_truncated": false}} +{"ts": "2026-03-30T14:35:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "376"}, "body": "{\"data\": [{\"event_id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-30T17:00:00+01:00\", \"occurrence_end\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 376, "body_truncated": false}} +{"ts": "2026-03-30T14:35:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:35:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "395"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 395, "body_truncated": false}} +{"ts": "2026-03-30T14:36:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-05-02", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "395"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 395, "body_truncated": false}} +{"ts": "2026-03-30T14:36:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "376"}, "body": "{\"data\": [{\"event_id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-30T17:00:00+01:00\", \"occurrence_end\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 376, "body_truncated": false}} +{"ts": "2026-03-30T14:36:20+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:20 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:36:20+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:20 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9410"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"ui", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:36:20+00:00", "client": "127.0.0.1", "method": "GET", "path": "/favicon.ico", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "image/avif,image/jxl,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/calendar", "Sec-Fetch-Dest": "image", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=6", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:20 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:36:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=list&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "28439"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"ebaa6e153c2949c66437@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"ui", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:36:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:31+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-03-31", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:31 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:32+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:32 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "367"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 367, "body_truncated": false}} +{"ts": "2026-03-30T14:36:32+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-02", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:32 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:33+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:33 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "367"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 367, "body_truncated": false}} +{"ts": "2026-03-30T14:36:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-02", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:42 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-03", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:42 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "395"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 395, "body_truncated": false}} +{"ts": "2026-03-30T14:36:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-04", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:42 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:43+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-05", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:43 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "49"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"day\"}}", "body_bytes": 49, "body_truncated": false}} +{"ts": "2026-03-30T14:36:44+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=day&date=2026-04-06", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:36:44 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "354"}, "body": "{\"data\": [{\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}], \"meta\": {\"count\": 1, \"view\": \"day\"}}", "body_bytes": 354, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d67b5ff89b3e11f4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:37:14+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"dd76a", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"6e86a3a3cb79f222\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d67b5ff89b3e11f4\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "ETag": "\"cb820e20ce5f05f9\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cb820e20ce5f05f9\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "ETag": "\"fc237d6ea5bab15b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"fc237d6ea5bab15b\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "ETag": "\"a04576a14aacbb44\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a04576a14aacbb44\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "ETag": "\"d4593cbbeb887b80\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d4593cbbeb887b80\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 15, \"uid\": \"35ef9e3fa5fc95e6feeb@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"88afe21d04a4e934\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:37:16+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"2c338e3049044bb6\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:35ef9e3fa5fc95e6feeb@calendar-wp-plugin\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"870fa79824125dc3\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:37:16+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"05e54c633fc9404d7909@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "ETag": "\"032ada2f4689a236\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d67b5ff89b3e11f4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:37:14+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"dd76a", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "ETag": "\"41474f0130ac56c2\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"41474f0130ac56c2\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T143716Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11634"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d67b5ff89b3e11f4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:37:14+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"dd76a", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T14:37:16+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:37:16 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:38:27+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:27 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"a1c06c7625070956\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"31413754bab11f12\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T14:38:29+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "655", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/calendars/public/1.ics", "body_bytes": 655, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:29 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8815"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"a1c06c7625070956\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143829Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"31413754bab11f12\"\n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:38:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/4.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14108", "If-Match": "\"0959513a689d7faf\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:37 GMT", "ETag": "\"c3b404268b1e2a58\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:38:37+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/4.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:37 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8858"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"a1c06c7625070956\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T143837Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"31413754bab11f12\"\n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:38:43+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"a1c06c7625070956\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"31413754bab11f12\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T14:38:50+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:50 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "7109"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:38:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:58 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43446"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:38:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "51"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"month\"}}", "body_bytes": 51, "body_truncated": false}} +{"ts": "2026-03-30T14:38:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/favicon.ico", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "image/avif,image/jxl,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/calendar", "Sec-Fetch-Dest": "image", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=6", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:38:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:39:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:39:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "7109"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:39:09+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:39:09 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "3708"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"al ...(truncated)", "body_bytes": 3708, "body_truncated": false}} +{"ts": "2026-03-30T14:39:24+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:39:24 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"a1c06c7625070956\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"31413754bab11f12\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T14:42:51+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:42:51 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8858"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"a1c06c7625070956\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144251Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"31413754bab11f12\"\n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:43:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:43:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7751edcdd53548b6\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:43:34+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"052f8", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:43:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:43:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c97f86eaea834f45\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144335Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T144335Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T144335Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T144335Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T14:43:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:43:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"7751edcdd53548b6\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144335Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T14:43:36+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:43:36 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8815"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"7751edcdd53548b6\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144336Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"052f81957f9aae6d\"\n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:44:20+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:20 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"5ec78c2d14c408f9\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T144420Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T14:44:29+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:29 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7dba3d192408fd8d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:44:28+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"8dd5f", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:44:29+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:29 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb9acb23074ed19b\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144429Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T144429Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T144429Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T144429Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T14:44:29+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:29 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"7dba3d192408fd8d\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144429Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"072b1b494ba4f968\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "ETag": "\"5adcf0bbdfde8074\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5adcf0bbdfde8074\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "ETag": "\"1d474bd1d0cd4bf5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1d474bd1d0cd4bf5\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "ETag": "\"4bb73834167ee601\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4bb73834167ee601\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "ETag": "\"98b19a1e7ae18985\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"98b19a1e7ae18985\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 15, \"uid\": \"a63f8ecbbdc94c49bc3f@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7edae8a0700321c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:44:30+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5cdf8a9141d5dbc9\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:a63f8ecbbdc94c49bc3f@calendar-wp-plugin\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"fe9f3570de454c2f\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:44:30+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "ETag": "\"2e770edf61e4a71a\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7dba3d192408fd8d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:44:28+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"8dd5f", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "ETag": "\"5c32f479683cad08\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5c32f479683cad08\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T144430Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11634"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7dba3d192408fd8d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T14:44:28+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"8dd5f", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T14:44:30+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:30 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:44:58+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6130"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"53fb93409a868c3b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"15e21dd53bfeddfc\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:44:58+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "1143", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/smoke-cancelled-occurrence.ics/caldav/calendars/public/smoke-exdate-import.ics/caldav/calendars/public/16.ics/caldav/calendars/public/15.ics/caldav/calendars/public/smoke-monthly-last.ics/caldav/calendars/public/smoke-monthly-ordinal.ics/caldav/calendars/public/smoke-monthly-nth.ics/caldav/calendars/public/smoke-vtimezone.ics/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/calendars/public/1.ics", "body_bytes": 1143, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:44:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "15419"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"53fb93409a868c3b\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144458Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"15e21dd53bfeddfc\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T14:45:10+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/4.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14108", "If-Match": "\"34008884f1eb8394\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:45:10 GMT", "ETag": "\"2296d29d18e51e25\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:45:10+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/4.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:45:10 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1040"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"2296d29d18e51e25\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T144510Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 1040, "body_truncated": false}} +{"ts": "2026-03-30T14:45:12+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:45:12 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6130"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"53fb93409a868c3b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"15e21dd53bfeddfc\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:45:54+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:45:54 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6130"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"53fb93409a868c3b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"15e21dd53bfeddfc\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:46:14+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:14 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:46:14+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "10580"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"ui", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:46:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:18 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9409"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6046"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-06T1", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-06-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5317"}, "body": "{\"data\": [{\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-06-02T15:00:00+01:00\", \"occurrence_end\": \"2026-06-02T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"occurrence_start\": \"2026-06-03T12:30:00+01:00\", \"occurrence_end\": \"2026-06-03T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-06-03T12:30:00+01:00\", \"occurrence_end\": \"2026-06-03T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"occurrence_start\": \"2026-06-10T12:30:00+01:", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6046"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-06T1", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9409"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:34 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6046"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-06T1", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:37 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:46:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "10580"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"ui", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:46:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/favicon.ico", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "image/avif,image/jxl,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/calendar", "Sec-Fetch-Dest": "image", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=6", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:46:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:38 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9409"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:46:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:46:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6046"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-06T1", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:47:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=week&date=2026-04-27", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "1345"}, "body": "{\"data\": [{\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-29T12:30:00+01:00\", \"occurrence_end\": \"2026-04-29T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-29T12:30:00+01:00\", \"occurrence_end\": \"2026-04-29T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 5, \"uid\": \"fixture-ce-005@calendar-wp-plugin\", \"title\": \"Finance Close\", \"description\": \"Month-end close process.\", \"location\": \"Finance Office\", \"category\": \"Finance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-30T17:00:00+01:00\", \"occurrence_end\": \"2026-04-30T18:00:00+01:00\", \"repeat_type\": \"monthly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_sta ...(truncated)", "body_bytes": 1345, "body_truncated": false}} +{"ts": "2026-03-30T14:47:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9409"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:47:06+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:06 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6046"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-06T1", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:47:53+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/10.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14137", "If-Match": "\"f22edec26fd96749\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:53 GMT", "ETag": "\"e6fa695514466881\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:47:53+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "232", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/10.ics", "body_bytes": 232, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:53 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1070"}, "body": "\n\n\n /caldav/calendars/public/10.ics\n \n \n \"e6fa695514466881\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T144753Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260508T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 1070, "body_truncated": false}} +{"ts": "2026-03-30T14:47:53+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:53 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6130"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"53fb93409a868c3b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"15e21dd53bfeddfc\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:47:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:58 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:47:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:47:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "10580"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"ui", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:48:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9409"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:48:01+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5698"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-06T1", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:48:22+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:22 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6130"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"55da5569707995bb\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"527d6e363f17bcfa\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:48:22+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "655", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/calendars/public/1.ics", "body_bytes": 655, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:22 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8815"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"55da5569707995bb\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144822Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"527d6e363f17bcfa\"\n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:48:32+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/7.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14137", "If-Match": "\"df74a2a1b310e407\"", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:32 GMT", "ETag": "\"b18f9e3c652a40d4\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T14:48:32+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/7.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:32 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1069"}, "body": "\n\n\n /caldav/calendars/public/7.ics\n \n \n \"b18f9e3c652a40d4\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-007@calendar-wp-plugin\r\nDTSTAMP:20260330T144832Z\r\nSUMMARY:Fortnightly Coaching\r\nDESCRIPTION:Coaching check-in.\r\nLOCATION:Online\r\nCATEGORIES:Training\r\nDTSTART;TZID=Europe/London:20260407T150000\r\nDTEND;TZID=Europe/London:20260407T160000\r\nRRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20260731T235959\r\nEXDATE;TZID=Europe/London:20260519T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 1069, "body_truncated": false}} +{"ts": "2026-03-30T14:48:32+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://localhost:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:32 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6130"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"55da5569707995bb\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"527d6e363f17bcfa\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T14:48:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:37 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T14:48:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "10580"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"916c9cd30540959ad63f@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"ui", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T14:48:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/favicon.ico", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "image/avif,image/jxl,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/calendar", "Sec-Fetch-Dest": "image", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=6", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:48:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9409"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:48:41+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:48:41 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6050"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T14:49:47+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:49:47 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"bca3fc6eaa239803\"", "Content-Length": "5115"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T144947Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T144947Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T144947Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T144947Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T14:50:56+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/calendar, application/ics, text/plain;q=0.9", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:50:56 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"584d529babd5cae6\"", "Content-Length": "5115"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T145056Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T145056Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T145056Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T145056Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T14:50:56+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/calendar.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "314", "Depth": "0", "Origin": "http://localhost:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 314, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:50:56 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:50:56+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/.well-known/caldav", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "314", "Depth": "0", "Origin": "http://localhost:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 314, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:50:56 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:50:56+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "314", "Depth": "0", "Origin": "http://localhost:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 314, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:50:56 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T14:50:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/calendar,text/plain;q=0.8,*/*;q=0.5", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "none", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 14:50:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c047ccb96f0fbd50\"", "Content-Length": "5115"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T145059Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T145059Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T145059Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T145059Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"49da7b660d2eb90b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:20:48+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"14f25", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"76e9509bbd366af2\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"49da7b660d2eb90b\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"fa44a32e2b54fe98\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "ETag": "\"4740b3cc34a2cc56\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4740b3cc34a2cc56\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "ETag": "\"2be1c63715d4fd1d\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"2be1c63715d4fd1d\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "ETag": "\"cfd13b1114a34fa6\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cfd13b1114a34fa6\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "ETag": "\"67d3e29da97d3248\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"67d3e29da97d3248\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 15, \"uid\": \"fd51d305728f2287d9eb@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"15832d093f3d58d8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:21:37+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0dd759e3487894b3\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fd51d305728f2287d9eb@calendar-wp-plugin\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0bf138cc955a676d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:21:37+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"bad9b0a4bf071e1cf316@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "ETag": "\"ccabcbefa3ae427d\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"49da7b660d2eb90b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:20:48+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"14f25", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "ETag": "\"ecce7dcc33a5384e\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ecce7dcc33a5384e\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T152137Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11634"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"49da7b660d2eb90b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:20:48+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"14f25", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:21:37+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:37 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:21:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:21:38 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T15:44:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:44:42 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11634"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"49da7b660d2eb90b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:20:48+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"14f25", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:44:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:44:42 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ec592f4817d711b6\"", "Content-Length": "5319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154442Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T154442Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T154442Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154442Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T15:44:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:44:42 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"49da7b660d2eb90b\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154442Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:44:42+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:44:42 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"fa44a32e2b54fe98\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154442Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:44:42+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 409, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:44:42 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "84"}, "body": "{\"error\": {\"code\": \"conflict_error\", \"message\": \"occurrence already has exception\"}}", "body_bytes": 84, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4cd72d479d507116\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"402be", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0a0ab4f2ca5feab6\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4cd72d479d507116\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"318ecd47574f78e8\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "ETag": "\"d846dc38796f779e\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d846dc38796f779e\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "ETag": "\"c80614f78133f2b1\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c80614f78133f2b1\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "ETag": "\"40fb6f8f93bddee4\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"40fb6f8f93bddee4\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "ETag": "\"e437395a7324b090\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e437395a7324b090\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 15, \"uid\": \"89ac5e3af1c07027b9b8@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"2ca833fe353a18f5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:58+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/15/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/15.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ce147ab01d399c28\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:89ac5e3af1c07027b9b8@calendar-wp-plugin\r\nDTSTAMP:20260330T154558Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"dda7eb88fc839463\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:58+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/16/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 16, \"uid\": \"4d11fce48beae092f87b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"81bd198374822883\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11006"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4cd72d479d507116\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"402be", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"c920974d8ef1116c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c920974d8ef1116c\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11634"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4cd72d479d507116\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"402be", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11634"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4cd72d479d507116\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"402be", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"18a0ba0a5476aa53\"", "Content-Length": "5319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4cd72d479d507116\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"318ecd47574f78e8\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"fe2e1195eb2a1dfe\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"fe2e1195eb2a1dfe\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"f9b9356a1517a241\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f9b9356a1517a241\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"f36d586abbe055b0\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f36d586abbe055b0\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"2bc1acbc670b28f9\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"2bc1acbc670b28f9\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 19, \"uid\": \"b397f1a2bd1d27bd909e@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"5cbce152b812cece\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:59+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/19/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/19.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0a27b46fe6ea00d7\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:b397f1a2bd1d27bd909e@calendar-wp-plugin\r\nDTSTAMP:20260330T154559Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"58df47550d8b8d1e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:59+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/20/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/20/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/20/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/20/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 20, \"uid\": \"42cf8591d4bbfc538331@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"aba0878b20403f58\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12946"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4cd72d479d507116\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"402be", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:45:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:45:59 GMT", "ETag": "\"27e69cf4e5e13efe\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:46:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:46:00 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"27e69cf4e5e13efe\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T154600Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:46:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:46:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12946"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"4cd72d479d507116\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:45:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"402be", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:46:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:46:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 18, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:46:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:46:00 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"5e82b8a608ec2f69\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:22+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9e96e", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9f62fea5ac3aa027\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154825Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T154825Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T154825Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154825Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5e82b8a608ec2f69\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154825Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"ed65e21db7fdf5b1\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154825Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"3256d792f303942ee5ec@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"05e6f17954013126\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:25+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T15:48:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"3256d792f303942ee5ec@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"3256d792f303942ee5ec@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"3256d792f303942ee5ec@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T15:48:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"732ad7c020d51395\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:37+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"996ea", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:48:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"84d45e72b4e73efa\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154839Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T154839Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T154839Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154839Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:48:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"732ad7c020d51395\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154839Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:48:39+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:39 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"a7862a0f1a83e217\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154839Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:48:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:39+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"54b5dbcbbac70e6b80a0@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d62c926281c67f37\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:39+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"54b5dbcbbac70e6b80a0@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"54b5dbcbbac70e6b80a0@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"54b5dbcbbac70e6b80a0@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"54b5dbcbbac70e6b80a0@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"54b5dbcbbac70e6b80a0@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "ETag": "\"03110b46b4736657\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"03110b46b4736657\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T154840Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "ETag": "\"b1d8098f2c401ca5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b1d8098f2c401ca5\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T154840Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "ETag": "\"e35466e93feda311\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e35466e93feda311\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T154840Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "ETag": "\"ca53342667463a28\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ca53342667463a28\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T154840Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"c497eb71f64543b5c7e6@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"ee105eed23956cc3\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:40+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1efae4799ce0bd1e\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:c497eb71f64543b5c7e6@calendar-wp-plugin\r\nDTSTAMP:20260330T154840Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"c92a15baad77b616\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:40+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"22636e248d3c1a08f8a3@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "ETag": "\"1075b85ca18a439c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"732ad7c020d51395\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:37+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"996ea", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "ETag": "\"a580ff422270d850\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a580ff422270d850\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T154840Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"732ad7c020d51395\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:37+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"996ea", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:48:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:40 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e6a46365a4181790\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:57+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d80b6", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e80ad55b0cf34f87\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e6a46365a4181790\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"3d214f4f81cae67d\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"b1d82373467e236cc487@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f1958b4788675043\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:59+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"b1d82373467e236cc487@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"b1d82373467e236cc487@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"b1d82373467e236cc487@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"b1d82373467e236cc487@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"b1d82373467e236cc487@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "ETag": "\"5f98042911cdc2aa\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5f98042911cdc2aa\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "ETag": "\"3eb343a6df4e1f4a\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3eb343a6df4e1f4a\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "ETag": "\"8eaecebe6704179d\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"8eaecebe6704179d\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "ETag": "\"1b74dcc0ab5f571c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1b74dcc0ab5f571c\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T154859Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:48:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:48:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"aaa8340a3d7ccd6a8df7@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d7f9a72c4aaad948\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:59+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"058abb73473f9ec4\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:aaa8340a3d7ccd6a8df7@calendar-wp-plugin\r\nDTSTAMP:20260330T154900Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"29e14a872aca1250\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:49:00+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6570fd6aeace58578ae1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "ETag": "\"031e00c7b2437a31\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e6a46365a4181790\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:57+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d80b6", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "ETag": "\"e715872930057161\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e715872930057161\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T154900Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e6a46365a4181790\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:48:57+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"d80b6", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:49:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:49:00 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7b4a62ce837068b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:54:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"3d75a", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"48ac2dd451d7868f\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a7b4a62ce837068b\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"ab57181d2c00fcd5\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"d9d622c02ebfdbf83dab@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"cbd5aab08f2de7f2\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:54:59+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"d9d622c02ebfdbf83dab@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"d9d622c02ebfdbf83dab@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"d9d622c02ebfdbf83dab@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"d9d622c02ebfdbf83dab@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"d9d622c02ebfdbf83dab@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "ETag": "\"a11c28068216c2d6\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a11c28068216c2d6\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "ETag": "\"d96489542d0c9095\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d96489542d0c9095\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "ETag": "\"6e95caf3538b8583\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"6e95caf3538b8583\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "ETag": "\"bf238a72a05154c6\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"bf238a72a05154c6\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"84911fe28ff2f9fbdb45@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"54261a0506aa9388\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:54:59+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"26ffe89dee62df60\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:84911fe28ff2f9fbdb45@calendar-wp-plugin\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"7bc1ad808fbfd5fd\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:54:59+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"8af4eae9f0c214d146d5@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "ETag": "\"fe95f3bc4c2e74e1\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7b4a62ce837068b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:54:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"3d75a", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "ETag": "\"0bbc85496d9bb64a\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:54:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:54:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0bbc85496d9bb64a\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T155459Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:55:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:55:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7b4a62ce837068b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:54:56+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"3d75a", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:55:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:55:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:55:00+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:55:00 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7bc6e565475a9a8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:56:42+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0388f", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"75c7a06e3f9dde8c\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a7bc6e565475a9a8\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"dba8ac559404c969\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"c5a0ca5d685c8908a802@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"01c1e859a0c1d3ee\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:56:45+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"c5a0ca5d685c8908a802@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"c5a0ca5d685c8908a802@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"c5a0ca5d685c8908a802@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"c5a0ca5d685c8908a802@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"c5a0ca5d685c8908a802@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "ETag": "\"e2be2b99c39985f7\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e2be2b99c39985f7\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "ETag": "\"a380dceb2ca70302\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"a380dceb2ca70302\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "ETag": "\"62cfa347f4a3e87c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"62cfa347f4a3e87c\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "ETag": "\"3e18aabb2c471d1b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3e18aabb2c471d1b\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"21ed32483f6fe0854775@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e195c8c718c74ea2\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:56:45+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5aeda0097db3158a\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:21ed32483f6fe0854775@calendar-wp-plugin\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"107f90ee4fdcd3da\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:56:45+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"ff4ad7f18ee79cf13356@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "ETag": "\"25be1f0412a1607c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7bc6e565475a9a8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:56:42+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0388f", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "ETag": "\"b4d712a7912ed997\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b4d712a7912ed997\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T155645Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a7bc6e565475a9a8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T15:56:42+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0388f", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T15:56:45+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 15:56:45 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"50884b9c7f52d7b4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:00:11+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9cc94", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1d9cf2007a92fe76\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"50884b9c7f52d7b4\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"d227b3c11080654f\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"76660280451647fdf0ba@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"8bbc978d26a619c8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:00:13+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"76660280451647fdf0ba@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"76660280451647fdf0ba@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"76660280451647fdf0ba@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"76660280451647fdf0ba@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"76660280451647fdf0ba@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "ETag": "\"5e3a16a0bd9129c8\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5e3a16a0bd9129c8\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "ETag": "\"cc6a3dc3e8885595\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cc6a3dc3e8885595\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "ETag": "\"39ef190e9e85067e\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"39ef190e9e85067e\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "ETag": "\"57c287b65a632230\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"57c287b65a632230\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"0609308738390cd66d4c@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"71eca1563d48f3ec\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:00:13+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"614335fbe22791d5\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:0609308738390cd66d4c@calendar-wp-plugin\r\nDTSTAMP:20260330T160013Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"406094a1e9d195af\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:00:13+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"6bf853766043d7c101b6@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "ETag": "\"0afcaa4cdff054dd\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"50884b9c7f52d7b4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:00:11+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9cc94", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T16:00:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:00:14+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:14 GMT", "ETag": "\"16f6332f4cb07a1b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:00:14+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:14 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"16f6332f4cb07a1b\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T160014Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T16:00:14+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"50884b9c7f52d7b4\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:00:11+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9cc94", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T16:00:14+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T16:00:14+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:00:14 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T16:01:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:01:58 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T16:01:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:01:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "51"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"month\"}}", "body_bytes": 51, "body_truncated": false}} +{"ts": "2026-03-30T16:02:01+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "7109"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:02:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-05-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4034"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-01T14:00:00+01:00\", \"occurrence_end\": \"2026-05-01T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"occurrence_start\": \"2026-05-04T00:00:00+00:00\", \"occurrence_end\": \"2026-05-05T00:00:00+00:00\", \"repeat_type\": \"none\"}, {\"event_id\": 7, \"uid\": \"fixture-ce-007@calendar-wp-plugin\", \"title\": \"Fortnightly Coaching\", \"description\": \"Coaching check-in.\", \"location\": \"Online\", \"category\": \"Training\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-05T15:00:00+01:00\", \"occurrence_end\": \"2026-05-05T16:00:00+01:00\", \"repeat_type\": \"custom\"}, {\"event_id\": 4, \"uid\": \"fixture-ce-004@calendar-wp-plugin\", \"title\": \"Community Lunch\", \"description\": \"Weekly community lunch.\", \"location\": \"Cafeteria\", \"category\": \"Community\", \"all_day_event\": false, \"", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:02:04+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:04 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "7109"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:02:15+00:00", "client": "127.0.0.1", "method": "GET", "path": "/admin.php?as=admin", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:15 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "7183"}, "body": "[omitted html payload: 3935 bytes]", "body_bytes": 3935, "body_truncated": true}} +{"ts": "2026-03-30T16:02:17+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-admin/admin.php?page=calendar-diagnostics&as=admin", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/admin.php?as=admin", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:17 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "8898"}, "body": "[omitted html payload: 3935 bytes]", "body_bytes": 3935, "body_truncated": true}} +{"ts": "2026-03-30T16:02:17+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/admin/diagnostics?limit=20&as=admin", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-diagnostics&as=admin", "Content-Type": "application/json", "X-WP-User": "admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:17 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "28256"}, "body": "{\"data\": [{\"ts\": \"2026-03-30T16:00:13+00:00\", \"client\": \"127.0.0.1\", \"method\": \"POST\", \"path\": \"/wp-json/calendar/v1/events\", \"request\": {\"headers\": {\"Host\": \"127.0.0.1:8080\", \"User-Agent\": \"curl/8.14.1\", \"Accept\": \"*/*\", \"Content-Type\": \"application/json\", \"X-WP-User\": \"admin\", \"Content-Length\": \"295\"}, \"body\": \"{\\\"title\\\": \\\"Smoke Daily Exception TZ\\\", \\\"description\\\": \\\"tz-key\\\", \\\"location\\\": \\\"\\\", \\\"category\\\": \\\"\\\", \\\"all_day_event\\\": false, \\\"start_datetime\\\": \\\"2026-03-02T10:00:00+00:00\\\", \\\"end_datetime\\\": \\\"2026-03-02T11:00:00+00:00\\\", \\\"repeat_type\\\": \\\"daily\\\", \\\"repeat_interval\\\": 1, \\\"repeat_range_mode\\\": \\\"until\\\", \\\"repeat_until\\\": \\\"2026-03-19\\\"}\", \"body_bytes\": 295, \"body_truncated\": false}, \"response\": {\"status\": 201, \"headers\": {\"Server\": \"CalendarFixture/0.1 Python/3.13.5\", \"Date\": \"Mon, 30 Mar 2026 16:00:13 GMT\", \"Content-Type\": \"application/json; charset=utf-8\", \"Content-Length\": \"655\"}, \"body\": \"{\\\"data\\\": {\\\"id\\\": 17, \\\"uid\\\": \\\"6bf853766043d7c101b6@calendar-wp-plugin\\\", \\\"title\\\": \\\"Smoke Daily Exception TZ\\\", \\\"description\\\": \\\"tz-key\\\", \\\"location\\\": \\\"\\\", \\\"category\\\": \\\"\\\", \\\"all_day_event\\\": false, \\\"start_datetime\\\": \\\"2026-03-02T10:00:00+00:00\\\", \\\"", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T16:02:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-admin/admin.php?page=calendar-users&as=admin", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-diagnostics&as=admin", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:24 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "9478"}, "body": "[omitted html payload: 3935 bytes]", "body_bytes": 3935, "body_truncated": true}} +{"ts": "2026-03-30T16:02:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/admin/users?as=admin", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "Content-Type": "application/json", "X-WP-User": "admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "527"}, "body": "{\"data\": [{\"id\": 1, \"email\": \"rw_user@example.test\", \"email_verified_at\": \"2026-03-30T16:01:41+00:00\", \"account_status\": \"active\", \"updated_at\": \"2026-03-30T16:01:41+00:00\"}, {\"id\": 2, \"email\": \"adrians@chezstephens.org.uk\", \"email_verified_at\": \"2026-03-30T16:01:41+00:00\", \"account_status\": \"active\", \"updated_at\": \"2026-03-30T16:01:41+00:00\"}, {\"id\": 3, \"email\": \"pending_user@example.test\", \"email_verified_at\": \"2026-03-30T16:01:41+00:00\", \"account_status\": \"pending_approval\", \"updated_at\": \"2026-03-30T16:01:41+00:00\"}]}", "body_bytes": 527, "body_truncated": false}} +{"ts": "2026-03-30T16:02:28+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:28 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T16:02:28+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:02:28 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "51"}, "body": "{\"data\": [], \"meta\": {\"count\": 0, \"view\": \"month\"}}", "body_bytes": 51, "body_truncated": false}} +{"ts": "2026-03-30T16:03:20+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/calendar, application/ics, text/plain;q=0.9", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:20 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:20+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "203", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 203, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:20 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:20+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "314", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 314, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:20 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/calendar, application/ics, text/plain;q=0.9", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "314", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 314, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/calendar, application/ics, text/plain;q=0.9", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "Content-Type": "text/plain; charset=utf-8", "Content-Length": "30"}, "body": "caldav shared public calendar\n", "body_bytes": 30, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "314", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "\n", "body_bytes": 314, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "431"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 431, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "203", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 203, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/principals/user/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "158", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 158, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:25+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "203", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "\n", "body_bytes": 203, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:25 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "431"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 431, "body_truncated": false}} +{"ts": "2026-03-30T16:03:26+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/principals/user/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "158", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "\n", "body_bytes": 158, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:26 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T16:03:26+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "217", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 217, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:26 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:26+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "215", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 215, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:26 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:26+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "215", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "\n", "body_bytes": 215, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:26 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T16:03:26+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "217", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "\n", "body_bytes": 217, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:26 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n", "body_bytes": 337, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:28 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:28+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "337", "Depth": "0", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache", "Authorization": "***"}, "body": "\n", "body_bytes": 337, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:28 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "509"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 509, "body_truncated": false}} +{"ts": "2026-03-30T16:03:28+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:28 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:28+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:28 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"7afd6bfad476334a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"bd549a6776fb7591\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T16:03:28+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "655", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/calendars/public/1.ics", "body_bytes": 655, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:28 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8815"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"7afd6bfad476334a\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T160328Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"bd549a6776fb7591\"\n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T16:03:43+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/3.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14098", "If-Match": "\"39d8705698ac45a3\"", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:43 GMT", "ETag": "\"1b040a223eb85dbe\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:43+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/3.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1030"}, "body": "\n\n\n /caldav/calendars/public/3.ics\n \n \n \"1b040a223eb85dbe\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T160343Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEXDATE;TZID=Europe/London:20260414T090000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 1030, "body_truncated": false}} +{"ts": "2026-03-30T16:03:43+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"7afd6bfad476334a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"bd549a6776fb7591\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T16:03:50+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/4.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "14108", "If-Match": "\"195229643e51b679\"", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-TZINFO:Europe/London[2025b]\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:-000115\r\nTZNAME:Europe/London(STD)\r\nDTSTART:18471201T000000\r\nRDATE:18471201T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19160521T020000\r\nRDATE:19160521T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19161001T030000\r\nRDATE:19161001T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19170408T020000\r\nRDATE:19170408T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19170917T030000\r\nRDATE:19170917T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:+010000\r\nTZOFFSETFROM:+000000\r\nTZNAME:Europe/London(DST)\r\nDTSTART:19180324T020000\r\nRDATE:19180324T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETTO:+000000\r\nTZOFFSETFROM:+010000\r\nTZNAME:Europe/London(STD)\r\nDTSTART:19180930T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929 ...(truncated)", "body_bytes": 4097, "body_truncated": true}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:50 GMT", "ETag": "\"2c3b4b86c7b93404\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:03:50+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "231", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/4.ics", "body_bytes": 231, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:50 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1040"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"2c3b4b86c7b93404\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T160350Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260422T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 1040, "body_truncated": false}} +{"ts": "2026-03-30T16:03:51+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:51 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"7afd6bfad476334a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"bd549a6776fb7591\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T16:03:56+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:03:56 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6476"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:04:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/users/me", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "Authorization": "***", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:04:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "137"}, "body": "{\"data\": {\"id\": 2, \"email\": \"adrians@chezstephens.org.uk\", \"account_status\": \"active\", \"email_verified_at\": \"2026-03-30T16:01:41+00:00\"}}", "body_bytes": 137, "body_truncated": false}} +{"ts": "2026-03-30T16:04:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:04:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6476"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:05:01+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/10", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "Authorization": "***", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:05:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "686"}, "body": "{\"data\": {\"id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"start_datetime\": \"2026-04-03T14:00:00+01:00\", \"end_datetime\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 8, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"2972ba81ca1c2300\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T16:01:41+00:00\"}}", "body_bytes": 686, "body_truncated": false}} +{"ts": "2026-03-30T16:05:07+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/10/occurrences?from=2026-04-01&months=3", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "Authorization": "***", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:05:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "2794"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-10T14:00:00+01:00\", \"occurrence_end\": \"2026-04-10T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-17T14:00:00+01:00\", \"occurrence_end\": \"2026-04-17T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete excepti ...(truncated)", "body_bytes": 2794, "body_truncated": false}} +{"ts": "2026-03-30T16:05:11+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14%3A00%3A00%2B01%3A00", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "Content-Type": "application/json", "Authorization": "***", "Origin": "http://localhost:8080", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:05:11 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T16:05:11+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/10/occurrences?from=2026-04-01&months=3", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "Authorization": "***", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:05:11 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "2446"}, "body": "{\"data\": [{\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-10T14:00:00+01:00\", \"occurrence_end\": \"2026-04-10T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-24T14:00:00+01:00\", \"occurrence_end\": \"2026-04-24T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete excepti ...(truncated)", "body_bytes": 2446, "body_truncated": false}} +{"ts": "2026-03-30T16:05:11+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-04-01", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:05:11 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6128"}, "body": "{\"data\": [{\"event_id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\"}, {\"event_id\": 10, \"uid\": \"fixture-ce-010@calendar-wp-plugin\", \"title\": \"Therapy Session\", \"description\": \"Used for single-occurrence delete exception tests.\", \"location\": \"Clinic\", \"category\": \"Health\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T14:00:00+01:00\", \"occurrence_end\": \"2026-04-03T15:00:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-06T09:00:00+01:00\", \"occurrence_end\": \"2026-04-06T09:15:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 3, \"uid\": \"fixture-ce-003@calendar-wp-plugin\", \"title\": \"Daily Standup\", \"description\": \"15 minute sync.\", \"location\": \"Online\", \"category\": \"Team\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T16:33:31+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:33:31 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"7afd6bfad476334a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"bd549a6776fb7591\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T16:33:31+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "232", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/10.ics", "body_bytes": 232, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 16:33:31 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1070"}, "body": "\n\n\n /caldav/calendars/public/10.ics\n \n \n \"41b7bbfae11cdaad\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-010@calendar-wp-plugin\r\nDTSTAMP:20260330T163331Z\r\nSUMMARY:Therapy Session\r\nDESCRIPTION:Used for single-occurrence delete exception tests.\r\nLOCATION:Clinic\r\nCATEGORIES:Health\r\nDTSTART;TZID=Europe/London:20260403T140000\r\nDTEND;TZID=Europe/London:20260403T150000\r\nRRULE:FREQ=WEEKLY;COUNT=8\r\nEXDATE;TZID=Europe/London:20260417T140000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 1070, "body_truncated": false}} +{"ts": "2026-03-30T17:03:35+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:03:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "3570"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"7afd6bfad476334a\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"bd549a6776fb7591\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3570, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"b382c678635b840b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:29:00+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1d2c2", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"bb575c8c2154be15\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b382c678635b840b\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"5e18ae38222b91d2\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"00c5f32762e8c0b85a1c@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"c7092bf3eb675ef3\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:29:02+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"00c5f32762e8c0b85a1c@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"00c5f32762e8c0b85a1c@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"00c5f32762e8c0b85a1c@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"00c5f32762e8c0b85a1c@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"00c5f32762e8c0b85a1c@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "ETag": "\"2105e046d5fa798f\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"2105e046d5fa798f\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "ETag": "\"c13600e6f9aee896\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c13600e6f9aee896\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "ETag": "\"685db0d2f5593d0b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:02 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"685db0d2f5593d0b\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T172902Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "ETag": "\"5ed4a7bad55cd265\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5ed4a7bad55cd265\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T172903Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"2ea801a45310bab83a50@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"cec610533c64bc27\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:29:03+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c02269cb3a8e8d71\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:2ea801a45310bab83a50@calendar-wp-plugin\r\nDTSTAMP:20260330T172903Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f28f7a5c7c266a7e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:29:03+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"371b0caae432edbf9a59@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "ETag": "\"e70d8316450066f2\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"b382c678635b840b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:29:00+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1d2c2", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "ETag": "\"159009a65264b9e1\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"159009a65264b9e1\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T172903Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"b382c678635b840b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:29:00+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1d2c2", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T17:29:03+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:29:03 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"465df57ed39797a5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:30:33+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"651ef", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4be039fb110f13fc\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T173035Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T173035Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T173035Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T173035Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"465df57ed39797a5\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T173035Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"2c12143b41cf6e99\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T173035Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"d90ef129e8487aac2a02@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"3b5c92f57b27a7e7\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:30:35+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"d90ef129e8487aac2a02@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"d90ef129e8487aac2a02@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"d90ef129e8487aac2a02@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"d90ef129e8487aac2a02@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"d90ef129e8487aac2a02@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "ETag": "\"48303f38375ccfe9\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"48303f38375ccfe9\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T173036Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "ETag": "\"0a5f4111009ba192\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0a5f4111009ba192\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T173036Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "ETag": "\"7802f78bb8d6e8f3\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"7802f78bb8d6e8f3\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T173036Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "ETag": "\"4e08622f0a349692\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4e08622f0a349692\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T173036Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"f7dee92cb08d5885ad53@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f74a13c50da08642\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:30:36+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d7729a08966aa16f\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:f7dee92cb08d5885ad53@calendar-wp-plugin\r\nDTSTAMP:20260330T173036Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"bbadcd70ff645ca1\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:30:36+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"47420837950da56232ab@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "ETag": "\"e5bd42e8bfd1b0d1\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"465df57ed39797a5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:30:33+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"651ef", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "ETag": "\"d3ca454bd41251d6\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d3ca454bd41251d6\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T173036Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"465df57ed39797a5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:30:33+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"651ef", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T17:30:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:30:36 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T17:33:35+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:33:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6437"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"465df57ed39797a5\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"651ef0024e9c7082\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T17:33:35+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "1191", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/smoke-cancelled-occurrence.ics/caldav/calendars/public/smoke-exdate-import.ics/caldav/calendars/public/17.ics/caldav/calendars/public/16.ics/caldav/calendars/public/smoke-monthly-last.ics/caldav/calendars/public/smoke-monthly-ordinal.ics/caldav/calendars/public/smoke-monthly-nth.ics/caldav/calendars/public/smoke-vtimezone.ics/caldav/calendars/public/11.ics/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/calendars/public/1.ics", "body_bytes": 1191, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:33:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "16545"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"465df57ed39797a5\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T173335Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"651ef0024e9c7082\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f7ad3938cf996256\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:38:32+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1c535", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d8126b67df01f0e2\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f7ad3938cf996256\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"4a24c041bfbc3ec2\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"3c9e7cfeb04a4a52d8d6@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"ac348cbf9c8977b5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:38:34+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"3c9e7cfeb04a4a52d8d6@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"3c9e7cfeb04a4a52d8d6@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"3c9e7cfeb04a4a52d8d6@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"3c9e7cfeb04a4a52d8d6@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"3c9e7cfeb04a4a52d8d6@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "ETag": "\"1506481eda0c5763\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1506481eda0c5763\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "ETag": "\"cb739417b1b3a094\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cb739417b1b3a094\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "ETag": "\"44d87bb8dd04cf86\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:34+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:34 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"44d87bb8dd04cf86\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T173834Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "ETag": "\"9dd5cba7baa23fc5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9dd5cba7baa23fc5\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T173835Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"d682d215052e032021f3@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"62e14734e3ca22de\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:38:35+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"694b221e5c8ab44a\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:d682d215052e032021f3@calendar-wp-plugin\r\nDTSTAMP:20260330T173835Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"63022f41345a4800\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:38:35+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"82cb03deb443fd591f6d@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "ETag": "\"5bb55dd889f66154\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f7ad3938cf996256\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:38:32+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1c535", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "ETag": "\"05afd85905bfe8d9\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"05afd85905bfe8d9\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T173835Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f7ad3938cf996256\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:38:32+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"1c535", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T17:38:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:38:35 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "6585"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"509f2bd7d2009ce8\"", "Content-Length": "3132"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3132, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 11, \"uid\": \"2ccb5f2076be62c35936@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e6c9698e1085c76b\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:12+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"2ccb5f2076be62c35936@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"2ccb5f2076be62c35936@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"2ccb5f2076be62c35936@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/11/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/11/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 11, \"uid\": \"2ccb5f2076be62c35936@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 11, \"uid\": \"2ccb5f2076be62c35936@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "ETag": "\"ff359fb275c089c9\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ff359fb275c089c9\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "ETag": "\"2764dba1363599bf\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"2764dba1363599bf\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "ETag": "\"dca2e1bdfd4a6703\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"dca2e1bdfd4a6703\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "ETag": "\"f7121f9cf89d5d41\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f7121f9cf89d5d41\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 16, \"uid\": \"bafafa747a751cf525e1@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"8f8aa2c3e6011a47\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:12+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/16/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/16.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"57d6b2e2283725e0\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:bafafa747a751cf525e1@calendar-wp-plugin\r\nDTSTAMP:20260330T174012Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"38b30150be27a031\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:12+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/17/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/17/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "ETag": "\"5409204c5488ce37\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "11645"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:40:12+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:12 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "ETag": "\"f3393675b839a951\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f3393675b839a951\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T174013Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12273"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T17:40:13+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "66"}, "body": "{\"email\": \"sec-nodisclose@example.test\", \"password\": \"***\"}", "body_bytes": 66, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:13 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"data\": {\"user_id\": 4, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:14+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/forgot-password", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "39"}, "body": "{\"email\": \"adrians@chezstephens.org.uk\"}", "body_bytes": 39, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "70"}, "body": "{\"data\": {\"status\": \"ok\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 70, "body_truncated": false}} +{"ts": "2026-03-30T17:40:14+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-1@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"data\": {\"user_id\": 5, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:14+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-2@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"data\": {\"user_id\": 6, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:14+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-3@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"data\": {\"user_id\": 7, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:14+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-4@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:14 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"data\": {\"user_id\": 8, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-5@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"data\": {\"user_id\": 9, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-6@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 10, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "{\"email\": \"sec-rate-7@example.test\", \"password\": \"***\"}", "body_bytes": 62, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 11, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "62"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 429, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"error\": {\"code\": \"rate_limited\", \"message\": \"Too many registration attempts. Try again later.\"}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/sec-1774892415.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Length": "201", "Content-Type": "application/x-www-form-urlencoded"}, "body": "[omitted non-text payload: 201 bytes, content-type=application/x-www-form-urlencoded]", "body_bytes": 201, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "ETag": "\"3cc5e1cc73127fbf\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/sec-1774892415.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3cc5e1cc73127fbf\"", "Content-Length": "287"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:sec-smoke-uid@example.test\r\nDTSTAMP:20260330T174015Z\r\nSUMMARY:Security Smoke Event\r\nDTSTART;TZID=Europe/London:20260415T100000\r\nDTEND;TZID=Europe/London:20260415T110000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 287, "body_truncated": false}} +{"ts": "2026-03-30T17:40:15+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/999999.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 17:40:15 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T18:03:41+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:03:41 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6756"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"eb53d018557c8fca\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"190a40db82a13a9b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T18:03:41+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "1251", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/sec-1774892415.ics/caldav/calendars/public/smoke-cancelled-occurrence.ics/caldav/calendars/public/smoke-exdate-import.ics/caldav/calendars/public/17.ics/caldav/calendars/public/16.ics/caldav/calendars/public/smoke-monthly-last.ics/caldav/calendars/public/smoke-monthly-ordinal.ics/caldav/calendars/public/smoke-monthly-nth.ics/caldav/calendars/public/smoke-vtimezone.ics/caldav/calendars/public/11.ics/caldav/calendars/public/10.ics/caldav/calendars/public/9.ics/caldav/calendars/public/8.ics/caldav/calendars/public/7.ics/caldav/calendars/public/6.ics/caldav/calendars/public/5.ics/caldav/calendars/public/4.ics/caldav/calendars/public/3.ics/caldav/calendars/public/2.ics/caldav/ca ...(truncated)", "body_bytes": 1251, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:03:41 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "17327"}, "body": "\n\n\n /caldav/calendars/public/1.ics\n \n \n \"eb53d018557c8fca\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T180341Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/2.ics\n \n \n \"190a40db82a13a9b\"\n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-30T18:07:05+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/wp-admin/admin.php?page=calendar-users&as=admin", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:07:05 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T18:07:05+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/public/events?view=month&date=2026-03-30", "request": {"headers": {"Host": "localhost:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", "Accept": "*/*", "Accept-Language": "en-GB,en;q=0.9", "Accept-Encoding": "gzip, deflate, br, zstd", "Referer": "http://localhost:8080/calendar", "DNT": "1", "Sec-GPC": "1", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:07:05 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "9410"}, "body": "{\"data\": [{\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"uid\": \"5b12ff0070324c83a9d7@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 17, \"ui", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T18:33:43+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:33:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "6756"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"eb53d018557c8fca\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"190a40db82a13a9b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T18:33:43+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "328", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/17.ics/caldav/calendars/public/16.ics/caldav/calendars/public/11.ics", "body_bytes": 328, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:33:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "2816"}, "body": "\n\n\n /caldav/calendars/public/11.ics\n \n \n \"1dc48ba9c8e4c1e0\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:2ccb5f2076be62c35936@calendar-wp-plugin\r\nDTSTAMP:20260330T183343Z\r\nSUMMARY:Smoke Occurrences API\r\nDESCRIPTION:occ-endpoint\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T110000\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEXDATE;TZID=Europe/London:20260402T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/16.ics\n \n \n \"57d6 ...(truncated)", "body_bytes": 2816, "body_truncated": false}} +{"ts": "2026-03-30T18:58:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:19 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12887"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T18:58:23+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "66"}, "body": "{\"email\": \"sec-nodisclose@example.test\", \"password\": \"***\"}", "body_bytes": 66, "body_truncated": false}, "response": {"status": 409, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "71"}, "body": "{\"error\": {\"code\": \"conflict_error\", \"message\": \"user already exists\"}}", "body_bytes": 71, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "12887"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"2b728e795356b3a1\"", "Content-Length": "5828"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 21, \"uid\": \"31f9cb4f86804b4a5b99@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"074904f1a79b3def\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T18:58:24+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/21/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 21, \"uid\": \"31f9cb4f86804b4a5b99@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 21, \"uid\": \"31f9cb4f86804b4a5b99@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 21, \"uid\": \"31f9cb4f86804b4a5b99@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/21/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/21/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/21/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 21, \"uid\": \"31f9cb4f86804b4a5b99@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 21, \"uid\": \"31f9cb4f86804b4a5b99@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "ETag": "\"fd09919574f61846\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"fd09919574f61846\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "ETag": "\"55b725a1dc9a9f7c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"55b725a1dc9a9f7c\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "ETag": "\"93bc4c15adc79a61\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"93bc4c15adc79a61\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T185824Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "ETag": "\"3a4eeb42242c3576\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3a4eeb42242c3576\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T185825Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 22, \"uid\": \"43a308b5b85320514c11@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"3f98362302bad93a\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T18:58:25+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/22/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/22.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"10c5aa3ff8a36c74\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:43a308b5b85320514c11@calendar-wp-plugin\r\nDTSTAMP:20260330T185825Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"b1b9932905a7b74c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T18:58:25+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/23/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/23/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/23/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/23/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 23, \"uid\": \"57e4605078dcee8f54a2@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "ETag": "\"ad165d5f81f5c072\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "14838"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "ETag": "\"7d8fff7329f9a530\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"7d8fff7329f9a530\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T185825Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "14838"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T18:58:25+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:58:25 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T18:59:04+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:04 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "14838"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T18:59:04+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:04 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:59:04+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:04 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:59:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "77"}, "body": "{\"email\": \"sec-nodisclose-1774897144@example.test\", \"password\": \"***\"}", "body_bytes": 77, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 12, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/forgot-password", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "39"}, "body": "{\"email\": \"adrians@chezstephens.org.uk\"}", "body_bytes": 39, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "70"}, "body": "{\"data\": {\"status\": \"ok\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 70, "body_truncated": false}} +{"ts": "2026-03-30T18:59:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774897144-1@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 13, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774897144-2@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 14, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:07+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774897144-3@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:07 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 15, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774897144-4@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 16, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774897144-5@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 17, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774897144-6@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 18, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 429, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"error\": {\"code\": \"rate_limited\", \"message\": \"Too many registration attempts. Try again later.\"}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/sec-1774897148.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Length": "201", "Content-Type": "application/x-www-form-urlencoded"}, "body": "[omitted non-text payload: 201 bytes, content-type=application/x-www-form-urlencoded]", "body_bytes": 201, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "ETag": "\"74426c4ef3fe3bdd\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/sec-1774897148.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"74426c4ef3fe3bdd\"", "Content-Length": "287"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:sec-smoke-uid@example.test\r\nDTSTAMP:20260330T185908Z\r\nSUMMARY:Security Smoke Event\r\nDTSTART;TZID=Europe/London:20260415T100000\r\nDTEND;TZID=Europe/London:20260415T110000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 287, "body_truncated": false}} +{"ts": "2026-03-30T18:59:08+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/999999.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:08 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T18:59:17+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:17 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:59:17+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:17 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T18:59:17+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "117"}, "body": "", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:17 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:27 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar", "body_bytes": 119, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:27 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T18:59:27+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "167"}, "body": "", "body_bytes": 167, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 18:59:27 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T19:03:43+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:03:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "7677"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"eb53d018557c8fca\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"190a40db82a13a9b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T19:03:43+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "780", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/23.ics/caldav/calendars/public/22.ics/caldav/calendars/public/21.ics/caldav/calendars/public/sec-1774897148.ics/caldav/calendars/public/smoke-cancelled-occurrence.ics/caldav/calendars/public/smoke-exdate-import.ics/caldav/calendars/public/smoke-monthly-last.ics/caldav/calendars/public/smoke-monthly-ordinal.ics/caldav/calendars/public/smoke-monthly-nth.ics/caldav/calendars/public/smoke-vtimezone.ics", "body_bytes": 780, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:03:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8597"}, "body": "\n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"fd09919574f61846\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T190343Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"55b725a1dc9a9f7c\"\n BEGIN:V ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T19:06:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:06:40 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:06:40+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:06:40 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:06:40+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "117"}, "body": "", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:06:40 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar", "body_bytes": 119, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:06:40 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T19:06:40+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "167"}, "body": "", "body_bytes": 167, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:06:40 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T19:16:05+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:16:05 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:16:05+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:16:05 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:16:05+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "117"}, "body": "", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:16:05 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar", "body_bytes": 119, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:16:05 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T19:16:05+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "167"}, "body": "", "body_bytes": 167, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:16:05 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "14838"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"435242ad54fe20be\"", "Content-Length": "6806"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 24, \"uid\": \"ff961c0d028da4c33d68@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"13736f3159f811a0\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:27:35+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/24/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 24, \"uid\": \"ff961c0d028da4c33d68@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 24, \"uid\": \"ff961c0d028da4c33d68@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 24, \"uid\": \"ff961c0d028da4c33d68@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/24/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/24/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/24/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 24, \"uid\": \"ff961c0d028da4c33d68@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 24, \"uid\": \"ff961c0d028da4c33d68@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "ETag": "\"227c6db7bb9af481\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"227c6db7bb9af481\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "ETag": "\"e59b158ebd60c483\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e59b158ebd60c483\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "ETag": "\"0d9783a0beedd0ef\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0d9783a0beedd0ef\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "ETag": "\"de783c88365b72e6\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"de783c88365b72e6\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 25, \"uid\": \"3c229224b9c6e55380f3@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"11dc511cc506316c\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:27:35+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/25/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/25.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"7c606408b74ecb81\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:3c229224b9c6e55380f3@calendar-wp-plugin\r\nDTSTAMP:20260330T192735Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"6e797a165f88a673\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:27:35+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/26/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/26/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/26/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:35+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/26/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:35 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 26, \"uid\": \"5d29e505c8af669ff7f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "ETag": "\"7675cb635bbcecc0\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "16789"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "ETag": "\"4a343b62858a1e3f\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4a343b62858a1e3f\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T192736Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "16789"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:27:36+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:27:36 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:33:43+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "8598"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"eb53d018557c8fca\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"190a40db82a13a9b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T19:33:43+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "720", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/26.ics/caldav/calendars/public/25.ics/caldav/calendars/public/24.ics/caldav/calendars/public/smoke-cancelled-occurrence.ics/caldav/calendars/public/smoke-exdate-import.ics/caldav/calendars/public/smoke-monthly-last.ics/caldav/calendars/public/smoke-monthly-ordinal.ics/caldav/calendars/public/smoke-monthly-nth.ics/caldav/calendars/public/smoke-vtimezone.ics", "body_bytes": 720, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:43 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "7815"}, "body": "\n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"227c6db7bb9af481\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T193343Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"e59b158ebd60c483\"\n BEGIN:V ...(truncated)", "body_bytes": 3919, "body_truncated": true}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "16789"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f0955aca05643a7e\"", "Content-Length": "7784"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 27, \"uid\": \"fa1d8a27cfbf4aa40bd7@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a36c468e33bf09f5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:33:52+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/27/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 27, \"uid\": \"fa1d8a27cfbf4aa40bd7@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 27, \"uid\": \"fa1d8a27cfbf4aa40bd7@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 27, \"uid\": \"fa1d8a27cfbf4aa40bd7@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/27/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/27/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/27/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 27, \"uid\": \"fa1d8a27cfbf4aa40bd7@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 27, \"uid\": \"fa1d8a27cfbf4aa40bd7@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "ETag": "\"6350421b1463753a\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"6350421b1463753a\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "ETag": "\"5cfa99671f4b7beb\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5cfa99671f4b7beb\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "ETag": "\"65be3c124db419c5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"65be3c124db419c5\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "ETag": "\"3905a2d554e7b5ba\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3905a2d554e7b5ba\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 28, \"uid\": \"aea231c47fe7081e8d96@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"5820328298180a9e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:33:52+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/28/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/28.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"c73446c499d71fd1\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:aea231c47fe7081e8d96@calendar-wp-plugin\r\nDTSTAMP:20260330T193352Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:33:52+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:52 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"82b317029b0183ad\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:33:52+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/29/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/29/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/29/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/29/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 29, \"uid\": \"66c44f2dcba00c571e7c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "ETag": "\"3a78837c720108cb\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "18740"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "ETag": "\"cefb5ce31068ad03\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cefb5ce31068ad03\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T193353Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "18740"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:33:53+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:33:53 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "18740"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9602848cf7b23ee1\"", "Content-Length": "8762"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 30, \"uid\": \"372f5751ecd17b5b1356@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"9310ebe908eeec52\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:35:18+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/30/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 30, \"uid\": \"372f5751ecd17b5b1356@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 30, \"uid\": \"372f5751ecd17b5b1356@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 30, \"uid\": \"372f5751ecd17b5b1356@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/30/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/30/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/30/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 30, \"uid\": \"372f5751ecd17b5b1356@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 30, \"uid\": \"372f5751ecd17b5b1356@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "ETag": "\"5bdb56afb01d0635\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5bdb56afb01d0635\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "ETag": "\"d944656d7163bc58\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:18+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:18 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d944656d7163bc58\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T193518Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "ETag": "\"e4df6ba9e5bc5ebc\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e4df6ba9e5bc5ebc\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T193519Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "ETag": "\"de42e0a9050f4298\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"de42e0a9050f4298\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T193519Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 31, \"uid\": \"57916be3224eecf16f71@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"c671f8dde75b0a3e\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:35:19+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/31/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/31.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"1d4c253672ed1649\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:57916be3224eecf16f71@calendar-wp-plugin\r\nDTSTAMP:20260330T193519Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"bc93364ad884fc9a\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:35:19+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/32/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/32/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/32/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/32/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 32, \"uid\": \"875b585d579132363b4b@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "ETag": "\"7a677c31a5560dce\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "20691"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "ETag": "\"0333f0a3bac7134c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0333f0a3bac7134c\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T193519Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "20691"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:35:19+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:35:19 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:37:42+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:37:42 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:37:42+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:37:42 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:37:42+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "117"}, "body": "", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:37:42 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar", "body_bytes": 119, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:37:42 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T19:37:42+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "167"}, "body": "", "body_bytes": 167, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:37:42 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "20691"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"fe1ebad7407f5a79\"", "Content-Length": "9740"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3905, "body_truncated": true}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 33, \"uid\": \"9ba79385ddc86c0ed142@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"47d5fe1d950c41e8\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:43:23+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/33/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 33, \"uid\": \"9ba79385ddc86c0ed142@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 33, \"uid\": \"9ba79385ddc86c0ed142@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 33, \"uid\": \"9ba79385ddc86c0ed142@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/33/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/33/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/33/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 33, \"uid\": \"9ba79385ddc86c0ed142@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 33, \"uid\": \"9ba79385ddc86c0ed142@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "ETag": "\"428b0774334217a0\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"428b0774334217a0\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "ETag": "\"566c179900289f19\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"566c179900289f19\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "ETag": "\"5f03a7c2f0425eed\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5f03a7c2f0425eed\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T194323Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:43:23+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:23 GMT", "ETag": "\"cbde1523187e131a\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cbde1523187e131a\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T194324Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 34, \"uid\": \"6792256d04b9f2249471@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"09fbd59129f4f866\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:43:24+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/34/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/34.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cccf56daf59f3994\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:6792256d04b9f2249471@calendar-wp-plugin\r\nDTSTAMP:20260330T194324Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"664c73b2a2759081\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:43:24+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/35/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/35/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/35/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/35/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 35, \"uid\": \"999a1dcfe70585b069f1@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "ETag": "\"46a37a3457db1c66\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "22642"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "ETag": "\"5649d2d0c6078ad2\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"5649d2d0c6078ad2\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T194324Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "22642"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:43:24+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:24 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "22642"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"65fca5862933b6d8\"", "Content-Length": "10718"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3904, "body_truncated": true}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 36, \"uid\": \"9ba924e112c6328c7991@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"631138c973aa434a\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:43:38+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/36/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 36, \"uid\": \"9ba924e112c6328c7991@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 36, \"uid\": \"9ba924e112c6328c7991@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 36, \"uid\": \"9ba924e112c6328c7991@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/36/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/36/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/36/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 36, \"uid\": \"9ba924e112c6328c7991@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 36, \"uid\": \"9ba924e112c6328c7991@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "ETag": "\"d4b92d5e931bb87e\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"d4b92d5e931bb87e\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "ETag": "\"0ffe94cdaf00da8f\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:38+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:38 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0ffe94cdaf00da8f\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T194338Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "ETag": "\"deaa13cf4c78a1a2\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"deaa13cf4c78a1a2\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T194339Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "ETag": "\"b14dcf1638b1116b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b14dcf1638b1116b\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T194339Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 37, \"uid\": \"dc869cdd20b57c4ce060@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"a42acbb8f1012c50\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:43:39+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/37/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/37.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"7ec2bdd719b303a2\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:dc869cdd20b57c4ce060@calendar-wp-plugin\r\nDTSTAMP:20260330T194339Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"f35557f2b1ee363d\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:43:39+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/38/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/38/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/38/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/38/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 38, \"uid\": \"08597e0cd8484e4afa74@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "ETag": "\"0670fbaf6ffb7eb5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "24593"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "ETag": "\"69637d4b3fac5b1c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"69637d4b3fac5b1c\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T194339Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "24593"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:43:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:43:39 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "24593"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "24593"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"b9666df54fddc7d2\"", "Content-Length": "11696"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3904, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "117"}, "body": "", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 39, \"uid\": \"5583f348e82b4d30498e@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"6afb4a6b08afb64f\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:45:22+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/principals/user/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "0", "Content-Type": "application/xml", "Content-Length": "119"}, "body": "", "body_bytes": 119, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/39/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 39, \"uid\": \"5583f348e82b4d30498e@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 39, \"uid\": \"5583f348e82b4d30498e@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 39, \"uid\": \"5583f348e82b4d30498e@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "167"}, "body": "", "body_bytes": 167, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/39/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/39/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/39/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 39, \"uid\": \"5583f348e82b4d30498e@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 39, \"uid\": \"5583f348e82b4d30498e@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "ETag": "\"e54b1ce109a46cc8\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e54b1ce109a46cc8\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "ETag": "\"3ca3370cf9a8bc3d\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3ca3370cf9a8bc3d\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "ETag": "\"391d992cae22bea5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"391d992cae22bea5\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "ETag": "\"0f6f0463ee50274c\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"0f6f0463ee50274c\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 40, \"uid\": \"780f5c74fb0ff81f4d77@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"601dc1b8caa7090a\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:45:22+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/40/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/40.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"9af632dd6f1dd8f5\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:780f5c74fb0ff81f4d77@calendar-wp-plugin\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"8cce090cd63776d1\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:45:22+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/41/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/41/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/41/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/41/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 41, \"uid\": \"a2738714eb50fc15415c@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "ETag": "\"cc710e449dbfa1ae\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "26544"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "ETag": "\"050cf9004963ffed\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:22+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:22 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"050cf9004963ffed\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T194522Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:45:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "26544"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:45:23+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "77"}, "body": "{\"email\": \"sec-nodisclose-1774899922@example.test\", \"password\": \"***\"}", "body_bytes": 77, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 19, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:23 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:45:23+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:23 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:45:24+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/forgot-password", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "39"}, "body": "{\"email\": \"adrians@chezstephens.org.uk\"}", "body_bytes": 39, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "70"}, "body": "{\"data\": {\"status\": \"ok\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 70, "body_truncated": false}} +{"ts": "2026-03-30T19:45:24+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-1@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 20, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:24+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-2@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:24 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 21, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-3@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 22, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-4@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 23, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-5@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 24, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:25+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-6@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:25 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 25, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:26+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774899922-7@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:26 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 26, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:45:26+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 429, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:26 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"error\": {\"code\": \"rate_limited\", \"message\": \"Too many registration attempts. Try again later.\"}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T19:45:26+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/sec-1774899926.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Length": "201", "Content-Type": "application/x-www-form-urlencoded"}, "body": "[omitted non-text payload: 201 bytes, content-type=application/x-www-form-urlencoded]", "body_bytes": 201, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:26 GMT", "ETag": "\"60902c44e50b1c11\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:45:26+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/sec-1774899926.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:26 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"60902c44e50b1c11\"", "Content-Length": "287"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:sec-smoke-uid@example.test\r\nDTSTAMP:20260330T194526Z\r\nSUMMARY:Security Smoke Event\r\nDTSTART;TZID=Europe/London:20260415T100000\r\nDTEND;TZID=Europe/London:20260415T110000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 287, "body_truncated": false}} +{"ts": "2026-03-30T19:45:26+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/999999.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:45:26 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "26544"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"e2b1a46432ffc137\"", "Content-Length": "12674"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3904, "body_truncated": true}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/xml; charset=utf-8", "Depth": "1", "Content-Length": "239"}, "body": "\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 42, \"uid\": \"cc7484132e5ceca0e789@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"ae03a0cef700c3da\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:47:39+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/42/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 42, \"uid\": \"cc7484132e5ceca0e789@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 42, \"uid\": \"cc7484132e5ceca0e789@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 42, \"uid\": \"cc7484132e5ceca0e789@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/42/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/42/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/42/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 42, \"uid\": \"cc7484132e5ceca0e789@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 42, \"uid\": \"cc7484132e5ceca0e789@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "ETag": "\"17ccc871adec4bc3\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"17ccc871adec4bc3\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "ETag": "\"f3ef2b7fc18dcec7\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f3ef2b7fc18dcec7\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "ETag": "\"f5d439a17623207e\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"f5d439a17623207e\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "ETag": "\"ec62ff1e4b136e42\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"ec62ff1e4b136e42\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T194739Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:47:39+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:39 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 43, \"uid\": \"672c78f46dcc88e86186@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"773becc6bc4fddc5\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:47:39+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/43/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/43.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"90276a2d43ab3d9f\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:672c78f46dcc88e86186@calendar-wp-plugin\r\nDTSTAMP:20260330T194740Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"32aa098f1ad6c404\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:47:40+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/44/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/44/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/44/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/44/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 44, \"uid\": \"4d93e11a90cee95c5c1a@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "ETag": "\"f9cb0b8409520dff\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "28495"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "ETag": "\"43a1d243dfe2e00d\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"43a1d243dfe2e00d\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T194740Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "28495"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:47:40+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:47:40 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "28495"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "28495"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"8651b7f9d8461cda\"", "Content-Length": "13652"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-TIMEZONE:Europe/London\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-002@calendar-wp-plugin\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Office Closed\r\nDESCRIPTION:Public holiday closure.\r\nLOCATION:HQ\r\nCATEGORIES:Operations\r\nDTSTART;VALUE=DATE:20260504\r\nDTEND;VALUE=DATE:20260505\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-003@calendar-wp-plugin\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Daily Standup\r\nDESCRIPTION:15 minute sync.\r\nLOCATION:Online\r\nCATEGORIES:Team\r\nDTSTART;TZID=Europe/London:20260406T090000\r\nDTEND;TZID=Europe/London:20260406T091500\r\nRRULE:FREQ=DAILY;COUNT=10\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\n ...(truncated)", "body_bytes": 3904, "body_truncated": true}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 401, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "WWW-Authenticate": "Basic realm=\"calendar-caldav-fixture\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "OPTIONS", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "DAV": "1, 2, calendar-access", "Allow": "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/1.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"eb53d018557c8fca\"", "Content-Length": "364"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-001@calendar-wp-plugin\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Board Meeting\r\nDESCRIPTION:Quarterly board review.\r\nLOCATION:Room A\r\nCATEGORIES:Governance\r\nDTSTART;TZID=Europe/London:20260401T100000\r\nDTEND;TZID=Europe/London:20260401T113000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 364, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "117"}, "body": "", "body_bytes": 117, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "1348"}, "body": "\n\n\n /caldav/\n \n \n /caldav/principals/user/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n\n \n /caldav/calendars/public/4.ics\n\n", "body_bytes": 239, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "997"}, "body": "\n\n\n /caldav/calendars/public/4.ics\n \n \n \"691a56c3c4084218\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:fixture-ce-004@calendar-wp-plugin\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Community Lunch\r\nDESCRIPTION:Weekly community lunch.\r\nLOCATION:Cafeteria\r\nCATEGORIES:Community\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n", "body_bytes": 997, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/10/occurrences/2026-04-17T14:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "287"}, "body": "{\"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"count\", \"repeat_count\": 3}", "body_bytes": 287, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "647"}, "body": "{\"data\": {\"id\": 45, \"uid\": \"ae50f5d7efb7ef19f4cf@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"count\", \"repeat_count\": 3, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"0896ee7c0c565fe2\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:56:58+00:00\"}}", "body_bytes": 647, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/principals/user/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "0", "Content-Type": "application/xml", "Content-Length": "119"}, "body": "", "body_bytes": 119, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "445"}, "body": "\n\n\n /caldav/principals/user/\n \n \n /caldav/calendars/\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 445, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/45/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "937"}, "body": "{\"data\": [{\"event_id\": 45, \"uid\": \"ae50f5d7efb7ef19f4cf@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 45, \"uid\": \"ae50f5d7efb7ef19f4cf@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-02T10:00:00+01:00\", \"occurrence_end\": \"2026-04-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 45, \"uid\": \"ae50f5d7efb7ef19f4cf@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 937, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/45/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/45/occurrences/2026-04-02T10:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Depth": "1", "Content-Type": "application/xml", "Content-Length": "167"}, "body": "", "body_bytes": 167, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "728"}, "body": "\n\n\n /caldav/calendars/\n \n \n \n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n", "body_bytes": 728, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/45/occurrences?from=2026-04-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "628"}, "body": "{\"data\": [{\"event_id\": 45, \"uid\": \"ae50f5d7efb7ef19f4cf@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-01T10:00:00+01:00\", \"occurrence_end\": \"2026-04-01T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 45, \"uid\": \"ae50f5d7efb7ef19f4cf@calendar-wp-plugin\", \"title\": \"Smoke Occurrences API\", \"description\": \"occ-endpoint\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-04-03T10:00:00+01:00\", \"occurrence_end\": \"2026-04-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}]}", "body_bytes": 628, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "454"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:Europe/London\nBEGIN:STANDARD\nDTSTART:18471201T000000\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=9\nTZOFFSETFROM:+0115\nTZOFFSETTO:+0000\nTZNAME:GMT\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nUID:smoke-vtimezone-parser-001\nSUMMARY:Smoke VTIMEZONE Parse\nDTSTART;TZID=Europe/London:20260423T150000\nDTEND;TZID=Europe/London:20260423T160000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 454, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "ETag": "\"4777d0172be8ffc5\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-vtimezone.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"4777d0172be8ffc5\"", "Content-Length": "288"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 288, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "261"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-nth-001\nSUMMARY:Smoke Monthly Nth\nDTSTART;TZID=Europe/London:20260402T150000\nDTEND;TZID=Europe/London:20260402T160000\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 261, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "ETag": "\"93aae3c5c0e310d1\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:58+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-nth.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:58 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"93aae3c5c0e310d1\"", "Content-Length": "319"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-nth-001\r\nDTSTAMP:20260330T195658Z\r\nSUMMARY:Smoke Monthly Nth\r\nDTSTART;TZID=Europe/London:20260426T150000\r\nDTEND;TZID=Europe/London:20260426T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 319, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "259"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-ordinal-001\nSUMMARY:Smoke Monthly Ordinal\nDTSTART;TZID=Europe/London:20260411T150000\nDTEND;TZID=Europe/London:20260411T160000\nRRULE:FREQ=MONTHLY;BYDAY=2SA\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 259, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "ETag": "\"3d1c488be8943444\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-ordinal.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"3d1c488be8943444\"", "Content-Length": "327"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-ordinal-001\r\nDTSTAMP:20260330T195659Z\r\nSUMMARY:Smoke Monthly Ordinal\r\nDTSTART;TZID=Europe/London:20260411T150000\r\nDTEND;TZID=Europe/London:20260411T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 327, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "264"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-monthly-last-001\nSUMMARY:Smoke Monthly Last\nDTSTART;TZID=Europe/London:20260425T150000\nDTEND;TZID=Europe/London:20260425T160000\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 264, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "ETag": "\"bce963d66714a73b\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-monthly-last.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"bce963d66714a73b\"", "Content-Length": "322"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-monthly-last-001\r\nDTSTAMP:20260330T195659Z\r\nSUMMARY:Smoke Monthly Last\r\nDTSTART;TZID=Europe/London:20260425T150000\r\nDTEND;TZID=Europe/London:20260425T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SA;BYSETPOS=-1\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 322, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "362"}, "body": "{\"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-19T15:00:00+01:00\", \"end_datetime\": \"2026-05-19T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\"}", "body_bytes": 362, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "673"}, "body": "{\"data\": {\"id\": 46, \"uid\": \"9fd41c1cc728b146f791@calendar-wp-plugin\", \"title\": \"Smoke 4th Sunday Anchor\", \"description\": \"anchor-normalization\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-05-24T15:00:00+01:00\", \"end_datetime\": \"2026-05-24T16:00:00+01:00\", \"repeat_type\": \"monthly\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"weekday_of_month\", \"repeat_nth_day\": null, \"repeat_nth_pos\": 4, \"repeat_nth_weekday\": 0, \"repeat_range_mode\": \"no_end\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"65b610e6c9d5be76\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:56:59+00:00\"}}", "body_bytes": 673, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/46/occurrences/2026-05-24T15:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/46.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"cb0262906b4e16d2\"", "Content-Length": "420"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:9fd41c1cc728b146f791@calendar-wp-plugin\r\nDTSTAMP:20260330T195659Z\r\nSUMMARY:Smoke 4th Sunday Anchor\r\nDESCRIPTION:anchor-normalization\r\nDTSTART;TZID=Europe/London:20260524T150000\r\nDTEND;TZID=Europe/London:20260524T160000\r\nRRULE:FREQ=MONTHLY;BYDAY=SU;BYSETPOS=4\r\nEXDATE;TZID=Europe/London:20260524T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 420, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "X-WP-User": "admin", "Content-Length": "295"}, "body": "{\"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_range_mode\": \"until\", \"repeat_until\": \"2026-03-19\"}", "body_bytes": 295, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "655"}, "body": "{\"data\": {\"id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"start_datetime\": \"2026-03-02T10:00:00+00:00\", \"end_datetime\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"until\", \"repeat_count\": null, \"repeat_until\": \"2026-03-19\", \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"e65810e02d6e9a50\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T19:56:59+00:00\"}}", "body_bytes": 655, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/47/occurrences/2026-03-11T11:00:00+01:00", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/47/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "5212"}, "body": "{\"data\": [{\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "DELETE", "path": "/wp-json/calendar/v1/events/47/occurrences/2026-03-10", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 204, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT"}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/47/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4906"}, "body": "{\"data\": [{\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+00:00\", \"occurrence_end\": \"2026-03-02T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+00:00\", \"occurrence_end\": \"2026-03-03T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+00:00\", \"occurrence_end\": \"2026-03-04T11:00:00+00:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 47, \"uid\": \"5058caddf2fc8c58b4dc@calendar-wp-plugin\", \"title\": \"Smoke Daily Exception TZ\", \"description\": \"tz-key\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+00:00\", \"occurrence_end\": \"2026-03-05T11:00:00", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-exdate-import.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "323"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-exdate-import-001\nSUMMARY:Smoke EXDATE Import\nDTSTART;TZID=Europe/London:20260302T100000\nDTEND;TZID=Europe/London:20260302T110000\nRRULE:FREQ=DAILY;UNTIL=20260319T235959\nEXDATE;TZID=Europe/London:20260310T100000,20260311T100000\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 323, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "ETag": "\"3894b8874c8aec1f\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "30451"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/18/occurrences?from=2026-03-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "4474"}, "body": "{\"data\": [{\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-02T10:00:00+01:00\", \"occurrence_end\": \"2026-03-02T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-03T10:00:00+01:00\", \"occurrence_end\": \"2026-03-03T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-04T10:00:00+01:00\", \"occurrence_end\": \"2026-03-04T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE Import\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-03-05T10:00:00+01:00\", \"occurrence_end\": \"2026-03-05T11:00:00+01:00\", \"repeat_type\": \"daily\"}, {\"event_id\": 18, \"uid\": \"smoke-exdate-import-001\", \"title\": \"Smoke EXDATE ", "body_bytes": 3928, "body_truncated": true}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "text/calendar; charset=utf-8", "Content-Length": "467"}, "body": "BEGIN:VCALENDAR\nPRODID:-//Smoke//EN\nVERSION:2.0\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nSUMMARY:Smoke Cancelled Occurrence\nDTSTART;TZID=Europe/London:20260408T123000\nDTEND;TZID=Europe/London:20260408T133000\nRRULE:FREQ=WEEKLY\nEND:VEVENT\nBEGIN:VEVENT\nUID:smoke-cancelled-occurrence-001\nRECURRENCE-ID;TZID=Europe/London:20260506T123000\nDTSTART;TZID=Europe/London:20260506T123000\nDTEND;TZID=Europe/London:20260506T133000\nSTATUS:CANCELLED\nEND:VEVENT\nEND:VCALENDAR\n", "body_bytes": 467, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "ETag": "\"14340d12265ad2dd\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/smoke-cancelled-occurrence.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"14340d12265ad2dd\"", "Content-Length": "359"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-cancelled-occurrence-001\r\nDTSTAMP:20260330T195659Z\r\nSUMMARY:Smoke Cancelled Occurrence\r\nDTSTART;TZID=Europe/London:20260408T123000\r\nDTEND;TZID=Europe/London:20260408T133000\r\nRRULE:FREQ=WEEKLY\r\nEXDATE;TZID=Europe/London:20260506T123000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 359, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "30452"}, "body": "{\"data\": [{\"id\": 1, \"uid\": \"fixture-ce-001@calendar-wp-plugin\", \"title\": \"Board Meeting\", \"description\": \"Quarterly board review.\", \"location\": \"Room A\", \"category\": \"Governance\", \"all_day_event\": false, \"start_datetime\": \"2026-04-01T10:00:00+01:00\", \"end_datetime\": \"2026-04-01T11:30:00+01:00\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"eb53d018557c8fca\\\"\", \"sync_version\": 1, \"updated_at\": \"2026-03-30T17:40:10+00:00\"}, {\"id\": 2, \"uid\": \"fixture-ce-002@calendar-wp-plugin\", \"title\": \"Office Closed\", \"description\": \"Public holiday closure.\", \"location\": \"HQ\", \"category\": \"Operations\", \"all_day_event\": true, \"start_datetime\": \"2026-05-04\", \"end_datetime\": \"2026-05-05\", \"repeat_type\": \"none\", \"repeat_interval\": 1, \"repeat_nth_mode\": \"\", \"repeat_nth_day\": null, \"repeat_nth_pos\": null, \"repeat_nth_weekday\": null, \"repeat_range_mode\": \"none\", \"repeat_count\": null, \"repeat_until\": null, \"timezone\": \"Europe/London\", \"calendar_id\": \"public\", \"etag\": \"\\\"190a4", "body_bytes": 3927, "body_truncated": true}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/wp-json/calendar/v1/events/19/occurrences?from=2026-05-01&months=1", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "X-WP-User": "admin"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "892"}, "body": "{\"data\": [{\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-13T12:30:00+01:00\", \"occurrence_end\": \"2026-05-13T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-20T12:30:00+01:00\", \"occurrence_end\": \"2026-05-20T13:30:00+01:00\", \"repeat_type\": \"weekly\"}, {\"event_id\": 19, \"uid\": \"smoke-cancelled-occurrence-001\", \"title\": \"Smoke Cancelled Occurrence\", \"description\": \"\", \"location\": \"\", \"category\": \"\", \"all_day_event\": false, \"occurrence_start\": \"2026-05-27T12:30:00+01:00\", \"occurrence_end\": \"2026-05-27T13:30:00+01:00\", \"repeat_type\": \"weekly\"}]}", "body_bytes": 892, "body_truncated": false}} +{"ts": "2026-03-30T19:56:59+00:00", "client": "127.0.0.1", "method": "GET", "path": "/calendar", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:56:59 GMT", "Content-Type": "text/html; charset=utf-8", "Content-Length": "43447"}, "body": "[omitted html payload: 3934 bytes]", "body_bytes": 3934, "body_truncated": true}} +{"ts": "2026-03-30T19:57:00+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "77"}, "body": "{\"email\": \"sec-nodisclose-1774900618@example.test\", \"password\": \"***\"}", "body_bytes": 77, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 27, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:00+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/forgot-password", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "39"}, "body": "{\"email\": \"adrians@chezstephens.org.uk\"}", "body_bytes": 39, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "70"}, "body": "{\"data\": {\"status\": \"ok\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 70, "body_truncated": false}} +{"ts": "2026-03-30T19:57:00+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-1@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 28, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:00+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-2@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 29, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:00+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-3@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:00 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 30, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:01+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-4@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 31, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:01+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-5@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 32, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:01+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-6@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 33, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:01+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "{\"email\": \"sec-rate-1774900618-7@example.test\", \"password\": \"***\"}", "body_bytes": 73, "body_truncated": false}, "response": {"status": 201, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "99"}, "body": "{\"data\": {\"user_id\": 34, \"status\": \"pending_approval\", \"email_status\": \"sent\", \"email_sent\": true}}", "body_bytes": 99, "body_truncated": false}} +{"ts": "2026-03-30T19:57:01+00:00", "client": "127.0.0.1", "method": "POST", "path": "/wp-json/calendar/v1/users/register", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Type": "application/json", "Content-Length": "73"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 429, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:01 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "98"}, "body": "{\"error\": {\"code\": \"rate_limited\", \"message\": \"Too many registration attempts. Try again later.\"}}", "body_bytes": 98, "body_truncated": false}} +{"ts": "2026-03-30T19:57:01+00:00", "client": "127.0.0.1", "method": "PUT", "path": "/caldav/calendars/public/sec-1774900621.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*", "Content-Length": "201", "Content-Type": "application/x-www-form-urlencoded"}, "body": "[omitted non-text payload: 201 bytes, content-type=application/x-www-form-urlencoded]", "body_bytes": 201, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:01 GMT", "ETag": "\"32633048dcc96299\""}, "body": "", "body_bytes": 0, "body_truncated": false}} +{"ts": "2026-03-30T19:57:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/sec-1774900621.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 200, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:02 GMT", "Content-Type": "text/calendar; charset=utf-8", "ETag": "\"32633048dcc96299\"", "Content-Length": "287"}, "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:sec-smoke-uid@example.test\r\nDTSTAMP:20260330T195702Z\r\nSUMMARY:Security Smoke Event\r\nDTSTART;TZID=Europe/London:20260415T100000\r\nDTEND;TZID=Europe/London:20260415T110000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", "body_bytes": 287, "body_truncated": false}} +{"ts": "2026-03-30T19:57:02+00:00", "client": "127.0.0.1", "method": "GET", "path": "/caldav/calendars/public/999999.ics", "request": {"headers": {"Host": "127.0.0.1:8080", "Authorization": "***", "User-Agent": "curl/8.14.1", "Accept": "*/*"}, "body": "", "body_bytes": 0, "body_truncated": false}, "response": {"status": 404, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Mon, 30 Mar 2026 19:57:02 GMT", "Content-Type": "application/json; charset=utf-8", "Content-Length": "62"}, "body": "{\"error\": {\"code\": \"not_found\", \"message\": \"Route not found\"}}", "body_bytes": 62, "body_truncated": false}} +{"ts": "2026-03-31T04:26:37+00:00", "client": "127.0.0.1", "method": "PROPFIND", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "144", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n", "body_bytes": 144, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Tue, 31 Mar 2026 04:26:37 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "15045"}, "body": "\n\n\n /caldav/calendars/public/\n \n \n Public Calendar\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/1.ics\n \n \n text/calendar; charset=utf-8\"eb53d018557c8fca\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/2.ics\n \n \n text/calendar; charset=utf-8\"190a40db82a13a9b\"\n \n HTTP/1.1 200 OK\n \n\n\n /caldav/calendars/public/3.ics\n \n \n ...(truncated)", "body_bytes": 3918, "body_truncated": true}} +{"ts": "2026-03-31T04:26:37+00:00", "client": "127.0.0.1", "method": "REPORT", "path": "/caldav/calendars/public/", "request": {"headers": {"Host": "127.0.0.1:8080", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Thunderbird/140.8.1", "Accept": "text/xml", "Accept-Language": "en-GB,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Charset": "utf-8,*;q=0.1", "Content-Type": "text/xml; charset=utf-8", "Content-Length": "1644", "Depth": "1", "Origin": "http://127.0.0.1:8080", "DNT": "1", "Authorization": "***", "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=4", "Pragma": "no-cache", "Cache-Control": "no-cache"}, "body": "\n/caldav/calendars/public/47.ics/caldav/calendars/public/46.ics/caldav/calendars/public/45.ics/caldav/calendars/public/44.ics/caldav/calendars/public/43.ics/caldav/calendars/public/42.ics/caldav/calendars/public/41.ics/caldav/calendars/public/40.ics/caldav/calendars/public/39.ics/caldav/calendars/public/38.ics/caldav/calendars/public/37.ics/caldav/calendars/public/36.ics/caldav/calendars/public/35.ics/caldav/calendars/public/34.ics/caldav/calendars/public/33.ics/caldav/calendars/public/32.ics/caldav/calendars/public/31.ics/caldav/calendars/public/30.ics/caldav/calendars/public/29.ics/caldav/calendars/public/28.ics/caldav/calendars/public/27.ics/caldav/calendars/publ ...(truncated)", "body_bytes": 1644, "body_truncated": false}, "response": {"status": 207, "headers": {"Server": "CalendarFixture/0.1 Python/3.13.5", "Date": "Tue, 31 Mar 2026 04:26:37 GMT", "Content-Type": "application/xml; charset=utf-8", "Content-Length": "24725"}, "body": "\n\n\n /caldav/calendars/public/smoke-vtimezone.ics\n \n \n \"4777d0172be8ffc5\"\n BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\nBEGIN:VEVENT\r\nUID:smoke-vtimezone-parser-001\r\nDTSTAMP:20260331T042637Z\r\nSUMMARY:Smoke VTIMEZONE Parse\r\nDTSTART;TZID=Europe/London:20260423T150000\r\nDTEND;TZID=Europe/London:20260423T160000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n\n \n HTTP/1.1 200 OK\n \n \n\n /caldav/calendars/public/smoke-monthly-nth.ics\n \n \n \"93aae3c5c0e310d1\"\n BEGIN:V ...(truncated)", "body_bytes": 3918, "body_truncated": true}} diff --git a/fixture/init.sh b/fixture/init.sh new file mode 100755 index 0000000..a56c6ca --- /dev/null +++ b/fixture/init.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +python3 fixture/server.py init diff --git a/fixture/reset.sh b/fixture/reset.sh new file mode 100755 index 0000000..71b24f5 --- /dev/null +++ b/fixture/reset.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +python3 fixture/server.py reset diff --git a/fixture/run.sh b/fixture/run.sh new file mode 100755 index 0000000..9e2ee07 --- /dev/null +++ b/fixture/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +python3 fixture/server.py run --host "${FIXTURE_HOST:-127.0.0.1}" --port "${FIXTURE_PORT:-8080}" diff --git a/fixture/seed.sh b/fixture/seed.sh new file mode 100755 index 0000000..5b060a0 --- /dev/null +++ b/fixture/seed.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +python3 fixture/server.py seed diff --git a/fixture/server.py b/fixture/server.py new file mode 100644 index 0000000..208a9a8 --- /dev/null +++ b/fixture/server.py @@ -0,0 +1,4230 @@ +#!/usr/bin/env python3 +""" +Local fixture harness for the calendar plugin requirements. + +Commands: + python3 fixture/server.py init + python3 fixture/server.py reset + python3 fixture/server.py seed + python3 fixture/server.py run --host 127.0.0.1 --port 8080 +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import hmac +import json +import os +import re +import secrets +import smtplib +import sqlite3 +import ssl +import textwrap +import time +import calendar as pycalendar +from collections import deque +from email.message import EmailMessage +from dataclasses import dataclass +from datetime import date, datetime, timedelta, timezone +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple +from urllib.parse import parse_qs, unquote, urlparse + + +ROOT = Path(__file__).resolve().parent +DB_PATH = ROOT / "fixture.db" +DEFAULT_TIMEZONE = "Europe/London" +SHARED_CALENDAR_ID = "public" +API_BASE = "/wp-json/calendar/v1" +PBKDF2_ITERATIONS = 210_000 +VERIFY_TOKEN_TTL_SECONDS = 24 * 60 * 60 +RESET_TOKEN_TTL_SECONDS = 30 * 60 +_RATE_LIMIT_STATE: Dict[str, List[float]] = {} + + +def fixture_trace_log_path() -> Path: + configured = (os.getenv("FIXTURE_HTTP_TRACE_LOG") or "").strip() + if configured: + p = Path(configured) + return p if p.is_absolute() else (ROOT / p) + return ROOT / "http_trace.log" + + +def utc_now_iso() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat() + + +def hash_password(value: str) -> str: + # Stronger hash format for fixture: pbkdf2_sha256$iterations$salt_hex$digest_hex + salt = secrets.token_bytes(16) + digest = hashlib.pbkdf2_hmac("sha256", value.encode("utf-8"), salt, PBKDF2_ITERATIONS) + return f"pbkdf2_sha256${PBKDF2_ITERATIONS}${salt.hex()}${digest.hex()}" + + +def verify_password(value: str, stored_hash: str) -> bool: + if not stored_hash: + return False + if stored_hash.startswith("pbkdf2_sha256$"): + parts = stored_hash.split("$") + if len(parts) != 4: + return False + try: + iterations = int(parts[1]) + salt = bytes.fromhex(parts[2]) + expected = bytes.fromhex(parts[3]) + except ValueError: + return False + actual = hashlib.pbkdf2_hmac("sha256", value.encode("utf-8"), salt, iterations) + return hmac.compare_digest(actual, expected) + # Backward-compatible legacy support for old unsalted SHA-256 hashes. + if re.fullmatch(r"[0-9a-f]{64}", stored_hash): + legacy = hashlib.sha256(value.encode("utf-8")).hexdigest() + return hmac.compare_digest(legacy, stored_hash) + return False + + +def _rate_limited(scope: str, key: str, limit: int, window_seconds: int) -> bool: + now = time.monotonic() + bucket = f"{scope}:{key}" + hits = _RATE_LIMIT_STATE.get(bucket, []) + cutoff = now - window_seconds + hits = [t for t in hits if t >= cutoff] + blocked = len(hits) >= limit + hits.append(now) + _RATE_LIMIT_STATE[bucket] = hits[-max(limit * 2, 16) :] + return blocked + + +def mk_etag(seed: str) -> str: + digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16] + return f'"{digest}"' + + +def parse_basic_auth(header_value: Optional[str]) -> Optional[Tuple[str, str]]: + if not header_value or not header_value.startswith("Basic "): + return None + encoded = header_value[6:].strip() + try: + raw = base64.b64decode(encoded).decode("utf-8") + except Exception: + return None + if ":" not in raw: + return None + username, password = raw.split(":", 1) + return username, password + + +def _read_env_file(path: Path) -> Dict[str, str]: + out: Dict[str, str] = {} + if not path.exists(): + return out + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + out[key.strip()] = value.strip().strip('"').strip("'") + return out + + +def _smtp_settings() -> Dict[str, str]: + env = _read_env_file(ROOT.parent / "credentials" / ".env") + merged = dict(env) + merged.update({k: v for k, v in os.environ.items() if k.startswith("SMTP_")}) + return merged + + +def send_fixture_email(to_email: str, subject: str, body_text: str) -> Tuple[bool, str]: + cfg = _smtp_settings() + host = (cfg.get("SMTP_HOST") or "").strip() + port = int((cfg.get("SMTP_PORT") or "587").strip()) + username = (cfg.get("SMTP_USERNAME") or "").strip() + password = cfg.get("SMTP_PASSWORD") or "" + from_email = (cfg.get("SMTP_FROM") or "").strip() + use_tls = (cfg.get("SMTP_USE_TLS") or "true").strip().lower() in {"1", "true", "yes", "on"} + if not host or not from_email or not to_email: + return False, "smtp_not_configured" + msg = EmailMessage() + msg["From"] = from_email + msg["To"] = to_email + msg["Subject"] = subject + msg.set_content(body_text) + try: + with smtplib.SMTP(host, port, timeout=10) as server: + if use_tls: + server.starttls(context=ssl.create_default_context()) + if username: + server.login(username, password) + server.send_message(msg) + return True, "sent" + except Exception as exc: # pragma: no cover - environment dependent + return False, f"smtp_error:{exc.__class__.__name__}:{exc}" + + +def ensure_db() -> sqlite3.Connection: + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def create_schema(conn: sqlite3.Connection) -> None: + cur = conn.cursor() + cur.executescript( + """ + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + location TEXT NOT NULL DEFAULT '', + category TEXT NOT NULL DEFAULT '', + all_day_event INTEGER NOT NULL DEFAULT 0, + start_datetime TEXT NOT NULL, + end_datetime TEXT NOT NULL, + repeat_type TEXT NOT NULL DEFAULT 'none', + repeat_interval INTEGER NOT NULL DEFAULT 1, + repeat_nth_mode TEXT NOT NULL DEFAULT '', + repeat_nth_day INTEGER NULL, + repeat_nth_pos INTEGER NULL, + repeat_nth_weekday INTEGER NULL, + repeat_range_mode TEXT NOT NULL DEFAULT 'none', + repeat_count INTEGER NULL, + repeat_until TEXT NULL, + timezone TEXT NOT NULL DEFAULT 'Europe/London', + calendar_id TEXT NOT NULL DEFAULT 'public', + etag TEXT NOT NULL, + sync_version INTEGER NOT NULL DEFAULT 1, + last_modified_by_user_id INTEGER NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS recurrence_exceptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_id INTEGER NOT NULL, + occurrence_key TEXT NOT NULL, + exception_type TEXT NOT NULL, + override_payload TEXT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE (event_id, occurrence_key) + ); + + CREATE TABLE IF NOT EXISTS caldav_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + email_verified_at TEXT NULL, + account_status TEXT NOT NULL, + access_level TEXT NOT NULL, + request_state TEXT NOT NULL DEFAULT 'none', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS user_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + token_type TEXT NOT NULL, + token_hash TEXT NOT NULL, + expires_at TEXT NOT NULL, + used_at TEXT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + actor_type TEXT NOT NULL, + actor_id TEXT NOT NULL, + action TEXT NOT NULL, + target_type TEXT NOT NULL, + target_id TEXT NOT NULL, + result TEXT NOT NULL, + context_json TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS plugin_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + """ + ) + cols = {r["name"] for r in conn.execute("PRAGMA table_info(events)").fetchall()} + if "caldav_resource" not in cols: + conn.execute("ALTER TABLE events ADD COLUMN caldav_resource TEXT NULL") + conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_events_caldav_resource ON events(caldav_resource)") + conn.execute("UPDATE events SET caldav_resource = id || '.ics' WHERE caldav_resource IS NULL") + conn.commit() + + +def drop_all(conn: sqlite3.Connection) -> None: + cur = conn.cursor() + cur.executescript( + """ + DROP TABLE IF EXISTS audit_log; + DROP TABLE IF EXISTS user_tokens; + DROP TABLE IF EXISTS caldav_users; + DROP TABLE IF EXISTS recurrence_exceptions; + DROP TABLE IF EXISTS events; + DROP TABLE IF EXISTS plugin_settings; + """ + ) + conn.commit() + + +def log_audit( + conn: sqlite3.Connection, + actor_type: str, + actor_id: str, + action: str, + target_type: str, + target_id: str, + result: str, + context: Dict[str, Any], +) -> None: + conn.execute( + """ + INSERT INTO audit_log (actor_type, actor_id, action, target_type, target_id, result, context_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + actor_type, + actor_id, + action, + target_type, + target_id, + result, + json.dumps(context, sort_keys=True), + utc_now_iso(), + ), + ) + conn.commit() + + +def seed_data(conn: sqlite3.Connection) -> None: + now = utc_now_iso() + users = [ + ("rw_user@example.test", "rwpass123456", now, "active", "write", "approved"), + ("adrians@chezstephens.org.uk", "brillig1", now, "active", "write", "approved"), + ("pending_user@example.test", "pending123456", now, "pending_approval", "write", "requested"), + ] + for email, pw, verified, status, level, request_state in users: + conn.execute( + """ + INSERT OR IGNORE INTO caldav_users + (email, password_hash, email_verified_at, account_status, access_level, request_state, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + (email, hash_password(pw), verified, status, level, request_state, now, now), + ) + + fixture_events: List[Dict[str, Any]] = [ + { + "id": 1, + "uid": "fixture-ce-001@calendar-wp-plugin", + "title": "Board Meeting", + "description": "Quarterly board review.", + "location": "Room A", + "category": "Governance", + "all_day_event": 0, + "start_datetime": "2026-04-01T10:00:00+01:00", + "end_datetime": "2026-04-01T11:30:00+01:00", + "repeat_type": "none", + "repeat_interval": 1, + "repeat_range_mode": "none", + "repeat_count": None, + "repeat_until": None, + }, + { + "id": 2, + "uid": "fixture-ce-002@calendar-wp-plugin", + "title": "Office Closed", + "description": "Public holiday closure.", + "location": "HQ", + "category": "Operations", + "all_day_event": 1, + "start_datetime": "2026-05-04", + "end_datetime": "2026-05-05", + "repeat_type": "none", + "repeat_interval": 1, + "repeat_range_mode": "none", + "repeat_count": None, + "repeat_until": None, + }, + { + "id": 3, + "uid": "fixture-ce-003@calendar-wp-plugin", + "title": "Daily Standup", + "description": "15 minute sync.", + "location": "Online", + "category": "Team", + "all_day_event": 0, + "start_datetime": "2026-04-06T09:00:00+01:00", + "end_datetime": "2026-04-06T09:15:00+01:00", + "repeat_type": "daily", + "repeat_interval": 1, + "repeat_range_mode": "count", + "repeat_count": 10, + "repeat_until": None, + }, + { + "id": 4, + "uid": "fixture-ce-004@calendar-wp-plugin", + "title": "Community Lunch", + "description": "Weekly community lunch.", + "location": "Cafeteria", + "category": "Community", + "all_day_event": 0, + "start_datetime": "2026-04-08T12:30:00+01:00", + "end_datetime": "2026-04-08T13:30:00+01:00", + "repeat_type": "weekly", + "repeat_interval": 1, + "repeat_range_mode": "no_end", + "repeat_count": None, + "repeat_until": None, + }, + { + "id": 5, + "uid": "fixture-ce-005@calendar-wp-plugin", + "title": "Finance Close", + "description": "Month-end close process.", + "location": "Finance Office", + "category": "Finance", + "all_day_event": 0, + "start_datetime": "2026-04-30T17:00:00+01:00", + "end_datetime": "2026-04-30T18:00:00+01:00", + "repeat_type": "monthly", + "repeat_interval": 1, + "repeat_range_mode": "until", + "repeat_count": None, + "repeat_until": "2026-08-31", + }, + { + "id": 6, + "uid": "fixture-ce-006@calendar-wp-plugin", + "title": "Annual Conference", + "description": "Annual community conference.", + "location": "Main Hall", + "category": "Events", + "all_day_event": 0, + "start_datetime": "2026-06-15T10:00:00+01:00", + "end_datetime": "2026-06-15T17:00:00+01:00", + "repeat_type": "yearly", + "repeat_interval": 1, + "repeat_range_mode": "count", + "repeat_count": 3, + "repeat_until": None, + }, + { + "id": 7, + "uid": "fixture-ce-007@calendar-wp-plugin", + "title": "Fortnightly Coaching", + "description": "Coaching check-in.", + "location": "Online", + "category": "Training", + "all_day_event": 0, + "start_datetime": "2026-04-07T15:00:00+01:00", + "end_datetime": "2026-04-07T16:00:00+01:00", + "repeat_type": "custom", + "repeat_interval": 2, + "repeat_range_mode": "until", + "repeat_count": None, + "repeat_until": "2026-07-31", + }, + { + "id": 8, + "uid": "fixture-ce-008@calendar-wp-plugin", + "title": "DST Validation Event", + "description": "Validates DST transition rendering.", + "location": "Lab", + "category": "QA", + "all_day_event": 0, + "start_datetime": "2026-10-25T00:30:00+01:00", + "end_datetime": "2026-10-25T02:30:00+00:00", + "repeat_type": "none", + "repeat_interval": 1, + "repeat_range_mode": "none", + "repeat_count": None, + "repeat_until": None, + }, + { + "id": 9, + "uid": "fixture-ce-009@calendar-wp-plugin", + "title": "Leap Day Marker", + "description": "Leap day recurrence behavior.", + "location": "Calendar", + "category": "QA", + "all_day_event": 0, + "start_datetime": "2028-02-29T09:00:00+00:00", + "end_datetime": "2028-02-29T10:00:00+00:00", + "repeat_type": "yearly", + "repeat_interval": 1, + "repeat_range_mode": "count", + "repeat_count": 3, + "repeat_until": None, + }, + { + "id": 10, + "uid": "fixture-ce-010@calendar-wp-plugin", + "title": "Therapy Session", + "description": "Used for single-occurrence delete exception tests.", + "location": "Clinic", + "category": "Health", + "all_day_event": 0, + "start_datetime": "2026-04-03T14:00:00+01:00", + "end_datetime": "2026-04-03T15:00:00+01:00", + "repeat_type": "weekly", + "repeat_interval": 1, + "repeat_range_mode": "count", + "repeat_count": 8, + "repeat_until": None, + }, + ] + + for event in fixture_events: + etag = mk_etag(f"{event['uid']}:{now}:1") + conn.execute( + """ + INSERT OR REPLACE INTO events + (id, uid, title, description, location, category, all_day_event, start_datetime, end_datetime, + repeat_type, repeat_interval, repeat_nth_mode, repeat_nth_day, repeat_nth_pos, repeat_nth_weekday, + repeat_range_mode, repeat_count, repeat_until, timezone, + calendar_id, caldav_resource, etag, sync_version, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + event["id"], + event["uid"], + event["title"], + event["description"], + event["location"], + event["category"], + event["all_day_event"], + event["start_datetime"], + event["end_datetime"], + event["repeat_type"], + event["repeat_interval"], + event.get("repeat_nth_mode", ""), + event.get("repeat_nth_day"), + event.get("repeat_nth_pos"), + event.get("repeat_nth_weekday"), + event["repeat_range_mode"], + event["repeat_count"], + event["repeat_until"], + DEFAULT_TIMEZONE, + SHARED_CALENDAR_ID, + f"{event['id']}.ics", + etag, + 1, + now, + now, + ), + ) + + conn.execute("DELETE FROM recurrence_exceptions") + conn.execute("INSERT OR REPLACE INTO plugin_settings (key, value, updated_at) VALUES (?, ?, ?)", ("presentation_name", "Calendar", now)) + conn.execute("INSERT OR REPLACE INTO plugin_settings (key, value, updated_at) VALUES (?, ?, ?)", ("url_slug", "", now)) + conn.execute("INSERT OR REPLACE INTO plugin_settings (key, value, updated_at) VALUES (?, ?, ?)", ("ics_access_mode", "public_read", now)) + conn.execute("INSERT OR REPLACE INTO plugin_settings (key, value, updated_at) VALUES (?, ?, ?)", ("caldav_calendar_name", "Public Calendar", now)) + conn.commit() + + +def event_to_dict(row: sqlite3.Row) -> Dict[str, Any]: + return { + "id": row["id"], + "uid": row["uid"], + "title": row["title"], + "description": row["description"], + "location": row["location"], + "category": row["category"], + "all_day_event": bool(row["all_day_event"]), + "start_datetime": row["start_datetime"], + "end_datetime": row["end_datetime"], + "repeat_type": row["repeat_type"], + "repeat_interval": row["repeat_interval"], + "repeat_nth_mode": row["repeat_nth_mode"], + "repeat_nth_day": row["repeat_nth_day"], + "repeat_nth_pos": row["repeat_nth_pos"], + "repeat_nth_weekday": row["repeat_nth_weekday"], + "repeat_range_mode": row["repeat_range_mode"], + "repeat_count": row["repeat_count"], + "repeat_until": row["repeat_until"], + "timezone": row["timezone"], + "calendar_id": row["calendar_id"], + "etag": row["etag"], + "sync_version": row["sync_version"], + "updated_at": row["updated_at"], + } + + +def get_setting(conn: sqlite3.Connection, key: str, default_value: str) -> str: + row = conn.execute("SELECT value FROM plugin_settings WHERE key = ?", (key,)).fetchone() + if not row: + return default_value + return row["value"] + + +def set_setting(conn: sqlite3.Connection, key: str, value: str) -> None: + conn.execute( + "INSERT OR REPLACE INTO plugin_settings (key, value, updated_at) VALUES (?, ?, ?)", + (key, value, utc_now_iso()), + ) + conn.commit() + + +def _parse_dt_maybe_date(value: str) -> datetime: + if "T" in value: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + return datetime.fromisoformat(f"{value}T00:00:00+00:00") + + +def _canonical_occurrence_key(value: str) -> Optional[str]: + raw = (value or "").strip() + if not raw: + return None + try: + dt = _parse_dt_maybe_date(raw) + except ValueError: + return None + return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat() + + +def _normalize_occurrence_key_for_event(value: str, event_start_iso: str) -> Optional[str]: + raw = (value or "").strip() + if not raw: + return None + if "T" in raw: + return _canonical_occurrence_key(raw) + # Allow date-only delete keys by inheriting time+offset from event start. + try: + base = _parse_dt_maybe_date(event_start_iso) + d = date.fromisoformat(raw) + except ValueError: + return None + candidate = base.replace(year=d.year, month=d.month, day=d.day) + return candidate.astimezone(timezone.utc).replace(microsecond=0).isoformat() + + +def _normalize_monthly_anchor( + start_iso: str, + end_iso: str, + repeat_type: str, + repeat_nth_mode: str, + repeat_nth_day: Optional[int], + repeat_nth_pos: Optional[int], + repeat_nth_weekday: Optional[int], +) -> Tuple[str, str]: + if repeat_type != "monthly": + return start_iso, end_iso + try: + start_dt = datetime.fromisoformat(start_iso.replace("Z", "+00:00")) + end_dt = datetime.fromisoformat(end_iso.replace("Z", "+00:00")) + except ValueError: + return start_iso, end_iso + duration = end_dt - start_dt + adjusted_start = start_dt + if repeat_nth_mode == "day_of_month" and repeat_nth_day: + day = max(1, min(int(repeat_nth_day), pycalendar.monthrange(start_dt.year, start_dt.month)[1])) + adjusted_start = start_dt.replace(day=day) + elif repeat_nth_mode == "weekday_of_month" and repeat_nth_pos and repeat_nth_weekday is not None: + nth_day = _nth_weekday_of_month(start_dt.year, start_dt.month, int(repeat_nth_weekday), int(repeat_nth_pos)) + if nth_day is not None: + adjusted_start = start_dt.replace(day=nth_day) + adjusted_end = adjusted_start + duration + return adjusted_start.isoformat(), adjusted_end.isoformat() + + +def _add_months(dt: datetime, months: int) -> datetime: + year = dt.year + (dt.month - 1 + months) // 12 + month = (dt.month - 1 + months) % 12 + 1 + day = min(dt.day, pycalendar.monthrange(year, month)[1]) + return dt.replace(year=year, month=month, day=day) + + +def _add_years(dt: datetime, years: int) -> datetime: + try: + return dt.replace(year=dt.year + years) + except ValueError: + # Leap-day fallback + return dt.replace(month=2, day=28, year=dt.year + years) + + +def _nth_weekday_of_month(year: int, month: int, weekday: int, pos: int) -> Optional[int]: + # weekday: 0=Sunday..6=Saturday, pos: 1..5 or -1 (last) + if not (0 <= weekday <= 6 and (1 <= pos <= 5 or pos == -1)): + return None + py_weekday = (weekday - 1) % 7 # python: Monday=0..Sunday=6 + if pos == -1: + days_in_month = pycalendar.monthrange(year, month)[1] + for day in range(days_in_month, 0, -1): + if date(year, month, day).weekday() == py_weekday: + return day + return None + day = 1 + hits = 0 + days_in_month = pycalendar.monthrange(year, month)[1] + while day <= days_in_month: + dt = date(year, month, day) + if dt.weekday() == py_weekday: + hits += 1 + if hits == pos: + return day + day += 1 + return None + + +def _window_for_view(view: str, anchor: date) -> Tuple[datetime, datetime]: + if view == "day": + start = datetime.fromisoformat(f"{anchor.isoformat()}T00:00:00+00:00") + return start, start + timedelta(days=1) + if view == "week": + monday = anchor - timedelta(days=anchor.weekday()) + start = datetime.fromisoformat(f"{monday.isoformat()}T00:00:00+00:00") + return start, start + timedelta(days=7) + if view == "month": + first = anchor.replace(day=1) + start = datetime.fromisoformat(f"{first.isoformat()}T00:00:00+00:00") + end = _add_months(start, 1) + return start, end + if view == "year": + first = date(anchor.year, 1, 1) + start = datetime.fromisoformat(f"{first.isoformat()}T00:00:00+00:00") + end = _add_years(start, 1) + return start, end + start = datetime.fromisoformat(f"{anchor.isoformat()}T00:00:00+00:00") + return start - timedelta(days=30), start + timedelta(days=90) + + +def _occurrence_overlap(s1: datetime, e1: datetime, s2: datetime, e2: datetime) -> bool: + return s1 < e2 and e1 > s2 + + +def make_uid(seed: str) -> str: + digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:20] + return f"{digest}@calendar-wp-plugin" + + +def create_ics_event(event: Dict[str, Any], exdates: List[str]) -> str: + lines = [ + "BEGIN:VEVENT", + f"UID:{event['uid']}", + f"DTSTAMP:{_to_ics_ts(utc_now_iso())}", + f"SUMMARY:{_ics_escape(event['title'])}", + ] + if event.get("description"): + lines.append(f"DESCRIPTION:{_ics_escape(event['description'])}") + if event.get("location"): + lines.append(f"LOCATION:{_ics_escape(event['location'])}") + if event.get("category"): + lines.append(f"CATEGORIES:{_ics_escape(event['category'])}") + + if event.get("all_day_event"): + lines.append(f"DTSTART;VALUE=DATE:{_to_ics_date(event['start_datetime'])}") + lines.append(f"DTEND;VALUE=DATE:{_to_ics_date(event['end_datetime'])}") + else: + lines.append(f"DTSTART;TZID={DEFAULT_TIMEZONE}:{_to_ics_local(event['start_datetime'])}") + lines.append(f"DTEND;TZID={DEFAULT_TIMEZONE}:{_to_ics_local(event['end_datetime'])}") + + rrule = _build_rrule(event) + if rrule: + lines.append(f"RRULE:{rrule}") + if exdates: + start_tz = None + try: + start_tz = datetime.fromisoformat(event["start_datetime"].replace("Z", "+00:00")).tzinfo + except ValueError: + start_tz = None + normalized_exdates = [] + for x in exdates: + try: + dt = datetime.fromisoformat(x.replace("Z", "+00:00")) + if start_tz is not None and dt.tzinfo is not None: + dt = dt.astimezone(start_tz) + normalized_exdates.append(dt.strftime("%Y%m%dT%H%M%S")) + except ValueError: + continue + joined = ",".join(normalized_exdates) + if joined: + lines.append(f"EXDATE;TZID={DEFAULT_TIMEZONE}:{joined}") + + lines.append("END:VEVENT") + return "\r\n".join(lines) + + +def _build_rrule(event: Dict[str, Any]) -> Optional[str]: + freq_map = { + "daily": "DAILY", + "weekly": "WEEKLY", + "monthly": "MONTHLY", + "yearly": "YEARLY", + "custom": "WEEKLY", # First-pass simplification for fixture. + } + repeat_type = event.get("repeat_type") + if repeat_type not in freq_map or repeat_type == "none": + return None + parts = [f"FREQ={freq_map[repeat_type]}"] + interval = int(event.get("repeat_interval") or 1) + if interval > 1: + parts.append(f"INTERVAL={interval}") + if repeat_type == "monthly": + nth_mode = event.get("repeat_nth_mode") or "" + if nth_mode == "day_of_month" and event.get("repeat_nth_day"): + parts.append(f"BYMONTHDAY={int(event['repeat_nth_day'])}") + elif nth_mode == "weekday_of_month" and event.get("repeat_nth_pos") and event.get("repeat_nth_weekday") is not None: + weekday_map = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"] + pos = int(event["repeat_nth_pos"]) + wd = int(event["repeat_nth_weekday"]) + if 0 <= wd <= 6 and (1 <= pos <= 5 or pos == -1): + parts.append(f"BYDAY={weekday_map[wd]}") + parts.append(f"BYSETPOS={pos}") + mode = event.get("repeat_range_mode") + if mode == "count" and event.get("repeat_count"): + parts.append(f"COUNT={int(event['repeat_count'])}") + elif mode == "until" and event.get("repeat_until"): + # date-only UNTIL for first pass + until = event["repeat_until"].replace("-", "") + parts.append(f"UNTIL={until}T235959") + return ";".join(parts) + + +def _to_ics_ts(iso_ts: str) -> str: + dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00")).astimezone(timezone.utc) + return dt.strftime("%Y%m%dT%H%M%SZ") + + +def _to_ics_local(iso_ts: str) -> str: + dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00")) + return dt.strftime("%Y%m%dT%H%M%S") + + +def _to_ics_date(iso_ts: str) -> str: + if "T" in iso_ts: + dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00")) + return dt.strftime("%Y%m%d") + return iso_ts.replace("-", "") + + +def _ics_escape(value: str) -> str: + return ( + value.replace("\\", "\\\\") + .replace(";", r"\;") + .replace(",", r"\,") + .replace("\n", r"\n") + ) + + +@dataclass +class AuthContext: + actor_type: str + actor_id: str + role: str + + +class _TraceWriter: + def __init__(self, handler: "FixtureHandler", raw): + self._handler = handler + self._raw = raw + + def write(self, data): + self._handler._trace_capture_response_bytes(data) + return self._raw.write(data) + + def flush(self): + return self._raw.flush() + + def __getattr__(self, name): + return getattr(self._raw, name) + + +class FixtureHandler(BaseHTTPRequestHandler): + server_version = "CalendarFixture/0.1" + _trace_body_cap = 4096 + _trace_body_preview_cap = 1200 + + def setup(self) -> None: + super().setup() + self.wfile = _TraceWriter(self, self.wfile) + self._trace_started = False + self._trace_request_body = b"" + self._trace_response_status: Optional[int] = None + self._trace_response_headers: List[Tuple[str, str]] = [] + self._trace_response_body = bytearray() + self._cached_request_body: Optional[bytes] = None + + def _trace_start(self) -> None: + self._trace_started = True + self._trace_request_body = b"" + self._trace_response_status = None + self._trace_response_headers = [] + self._trace_response_body = bytearray() + self._cached_request_body = None + + def _trace_capture_response_bytes(self, data: Any) -> None: + if not self._trace_started: + return + raw = data.encode("utf-8", errors="replace") if isinstance(data, str) else bytes(data) + remaining = self._trace_body_cap - len(self._trace_response_body) + if remaining > 0: + self._trace_response_body.extend(raw[:remaining]) + + def send_response(self, code: int, message: Optional[str] = None) -> None: + if getattr(self, "_trace_started", False): + self._trace_response_status = int(code) + super().send_response(code, message) + + def send_header(self, keyword: str, value: str) -> None: + if getattr(self, "_trace_started", False): + self._trace_response_headers.append((keyword, value)) + super().send_header(keyword, value) + + def _trace_log_path(self) -> Path: + return fixture_trace_log_path() + + def _redact_headers(self, headers: Dict[str, str]) -> Dict[str, str]: + out: Dict[str, str] = {} + for k, v in headers.items(): + if k.lower() in {"authorization", "cookie", "set-cookie"}: + out[k] = "***" + else: + out[k] = v + return out + + def _sanitize_json_for_log(self, value: Any) -> Any: + if isinstance(value, dict): + out: Dict[str, Any] = {} + for k, v in value.items(): + if str(k).lower() in {"password", "new_password", "token"}: + out[k] = "***" + else: + out[k] = self._sanitize_json_for_log(v) + return out + if isinstance(value, list): + return [self._sanitize_json_for_log(v) for v in value] + return value + + def _decode_body_for_log(self, body: bytes, content_type: str) -> str: + if not body: + return "" + ct = (content_type or "").lower() + raw = body[: self._trace_body_cap] + text = raw.decode("utf-8", errors="replace") + if "application/json" in ct: + try: + compact = json.dumps(self._sanitize_json_for_log(json.loads(text)), ensure_ascii=True) + if len(compact) > self._trace_body_preview_cap: + return compact[: self._trace_body_preview_cap] + " ...(truncated)" + return compact + except json.JSONDecodeError: + return text[: self._trace_body_preview_cap] + if "text/html" in ct: + return f"[omitted html payload: {len(body)} bytes]" + if "application/xml" in ct or "text/xml" in ct: + return text[: self._trace_body_preview_cap] + (" ...(truncated)" if len(text) > self._trace_body_preview_cap else "") + if "text/calendar" in ct: + return text[: self._trace_body_preview_cap] + (" ...(truncated)" if len(text) > self._trace_body_preview_cap else "") + if ct.startswith("text/"): + return text[: self._trace_body_preview_cap] + (" ...(truncated)" if len(text) > self._trace_body_preview_cap else "") + return f"[omitted non-text payload: {len(body)} bytes, content-type={content_type or 'unknown'}]" + + def _strip_http_response_preamble(self, raw: bytes) -> bytes: + if not raw.startswith(b"HTTP/"): + return raw + marker = b"\r\n\r\n" + idx = raw.find(marker) + if idx == -1: + return raw + return raw[idx + len(marker) :] + + def _read_trace_entries(self, limit: int = 20) -> List[Dict[str, Any]]: + path = self._trace_log_path() + if not path.exists(): + return [] + out: deque[Dict[str, Any]] = deque(maxlen=max(1, min(limit, 200))) + with path.open("r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + out.append(json.loads(line)) + except json.JSONDecodeError: + continue + return list(out) + + def _trace_finish(self) -> None: + if not self._trace_started: + return + req_headers = self._redact_headers({k: v for (k, v) in self.headers.items()}) + resp_headers: Dict[str, str] = {} + for k, v in self._trace_response_headers: + resp_headers[k] = "***" if k.lower() == "set-cookie" else v + req_ct = req_headers.get("Content-Type", "") + resp_ct = resp_headers.get("Content-Type", "") + response_raw = bytes(self._trace_response_body) + response_body_raw = self._strip_http_response_preamble(response_raw) + entry = { + "ts": utc_now_iso(), + "client": self.client_address[0] if self.client_address else "", + "method": self.command, + "path": self.path, + "request": { + "headers": req_headers, + "body": self._decode_body_for_log(self._trace_request_body, req_ct), + "body_bytes": len(self._trace_request_body), + "body_truncated": len(self._trace_request_body) > self._trace_body_cap, + }, + "response": { + "status": self._trace_response_status, + "headers": resp_headers, + "body": self._decode_body_for_log(response_body_raw, resp_ct), + "body_bytes": len(response_body_raw), + "body_truncated": len(self._trace_response_body) >= self._trace_body_cap, + }, + } + path = self._trace_log_path() + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as f: + f.write(json.dumps(entry, ensure_ascii=True) + "\n") + self._trace_started = False + + def _read_request_body_bytes(self) -> bytes: + if self._cached_request_body is not None: + return self._cached_request_body + length = int(self.headers.get("Content-Length", "0") or "0") + if length <= 0: + data = b"" + else: + data = self.rfile.read(length) + self._cached_request_body = data + self._trace_request_body = data[: self._trace_body_cap + 1] + return data + + def _slug_value(self) -> str: + conn = ensure_db() + try: + raw = get_setting(conn, "url_slug", "").strip() + finally: + conn.close() + raw = raw.strip("/") + return raw + + def _slug_prefix(self) -> str: + slug = self._slug_value() + return f"/{slug}" if slug else "" + + def _strip_slug(self, path: str) -> Optional[str]: + prefix = self._slug_prefix() + if not prefix: + return path + if path == prefix: + return "/" + if path.startswith(prefix + "/"): + return path[len(prefix) :] + return None + + def _with_slug(self, path: str) -> str: + prefix = self._slug_prefix() + return f"{prefix}{path}" if prefix else path + + def _request_query(self) -> Dict[str, List[str]]: + return parse_qs(urlparse(self.path).query) + + def _json_body(self) -> Dict[str, Any]: + data = self._read_request_body_bytes() + if not data: + return {} + return json.loads(data.decode("utf-8")) + + def _write_json(self, status: int, payload: Dict[str, Any]) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _write_text(self, status: int, text: str, content_type: str = "text/plain; charset=utf-8") -> None: + body = text.encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _not_found(self) -> None: + self._write_json(404, {"error": {"code": "not_found", "message": "Route not found"}}) + + def _authenticate_wp_admin(self) -> Optional[AuthContext]: + # Fixture-only auth: header X-WP-User or query ?as=admin|editor. + wp_user = self.headers.get("X-WP-User", "") + if not wp_user: + wp_user = (self._request_query().get("as") or [""])[0] + if wp_user == "admin": + return AuthContext("wp_user", "admin", "wp_admin") + if wp_user == "editor": + return AuthContext("wp_user", "editor", "wp_editor") + return None + + def _authenticate_caldav_user(self) -> Optional[AuthContext]: + creds = parse_basic_auth(self.headers.get("Authorization")) + if not creds: + return None + email, password = creds + conn = ensure_db() + row = conn.execute( + """ + SELECT id, email, password_hash, account_status, email_verified_at + FROM caldav_users WHERE email = ? + """, + (email,), + ).fetchone() + if not row: + conn.close() + return None + if not verify_password(password, row["password_hash"]): + conn.close() + return None + if row["account_status"] != "active" or not row["email_verified_at"]: + conn.close() + return None + # Transparent legacy-hash upgrade on successful login. + if re.fullmatch(r"[0-9a-f]{64}", row["password_hash"]): + conn.execute( + "UPDATE caldav_users SET password_hash = ?, updated_at = ? WHERE id = ?", + (hash_password(password), utc_now_iso(), int(row["id"])), + ) + conn.commit() + conn.close() + return AuthContext("caldav_user", str(row["id"]), "caldav_write") + + def _require_api_admin(self) -> Optional[AuthContext]: + auth = self._authenticate_wp_admin() + if not auth: + self._write_json(401, {"error": {"code": "authentication_error", "message": "Admin authentication required"}}) + return None + return auth + + def _require_event_read_auth(self) -> Optional[AuthContext]: + wp = self._authenticate_wp_admin() + if wp: + return wp + caldav = self._authenticate_caldav_user() + if caldav: + return caldav + self._write_json(401, {"error": {"code": "authentication_error", "message": "Login required"}}) + return None + + def _require_event_write_auth(self) -> Optional[AuthContext]: + wp = self._authenticate_wp_admin() + if wp and wp.role in {"wp_admin", "wp_editor"}: + return wp + caldav = self._authenticate_caldav_user() + if caldav and caldav.role == "caldav_write": + return caldav + if wp or caldav: + self._write_json(403, {"error": {"code": "authorization_error", "message": "write capability required"}}) + return None + self._write_json(401, {"error": {"code": "authentication_error", "message": "Login required"}}) + return None + + def _validate_event_payload(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]: + title = (payload.get("title") or "").strip() + start = payload.get("start_datetime") + end = payload.get("end_datetime") + if not title: + self._write_json(422, {"error": {"code": "validation_error", "message": "title is required"}}) + return None + if not start or not end: + self._write_json(422, {"error": {"code": "validation_error", "message": "start_datetime and end_datetime are required"}}) + return None + try: + ds = datetime.fromisoformat(start.replace("Z", "+00:00")) + de = datetime.fromisoformat(end.replace("Z", "+00:00")) + except ValueError: + self._write_json(422, {"error": {"code": "validation_error", "message": "invalid datetime format"}}) + return None + if de < ds: + self._write_json(422, {"error": {"code": "validation_error", "message": "end_datetime must be >= start_datetime"}}) + return None + normalized = { + "title": title, + "description": payload.get("description", ""), + "location": payload.get("location", ""), + "category": payload.get("category", ""), + "all_day_event": 1 if payload.get("all_day_event") else 0, + "start_datetime": start, + "end_datetime": end, + "repeat_type": payload.get("repeat_type", "none"), + "repeat_interval": int(payload.get("repeat_interval", 1)), + "repeat_nth_mode": payload.get("repeat_nth_mode", "") or "", + "repeat_nth_day": payload.get("repeat_nth_day"), + "repeat_nth_pos": payload.get("repeat_nth_pos"), + "repeat_nth_weekday": payload.get("repeat_nth_weekday"), + "repeat_range_mode": payload.get("repeat_range_mode", "none"), + "repeat_count": payload.get("repeat_count"), + "repeat_until": payload.get("repeat_until"), + "timezone": payload.get("timezone", DEFAULT_TIMEZONE), + } + normalized_start, normalized_end = _normalize_monthly_anchor( + normalized["start_datetime"], + normalized["end_datetime"], + str(normalized["repeat_type"] or "none"), + str(normalized["repeat_nth_mode"] or ""), + int(normalized["repeat_nth_day"]) if normalized["repeat_nth_day"] not in (None, "") else None, + int(normalized["repeat_nth_pos"]) if normalized["repeat_nth_pos"] not in (None, "") else None, + int(normalized["repeat_nth_weekday"]) if normalized["repeat_nth_weekday"] not in (None, "") else None, + ) + normalized["start_datetime"] = normalized_start + normalized["end_datetime"] = normalized_end + return normalized + + def do_OPTIONS(self) -> None: + self._trace_start() + try: + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self.send_response(404) + self.end_headers() + return + if path.startswith("/caldav"): + self.send_response(200) + self.send_header("DAV", "1, 2, calendar-access") + self.send_header("Allow", "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE") + self.end_headers() + return + self.send_response(200) + self.send_header("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + self.end_headers() + finally: + self._trace_finish() + + def do_GET(self) -> None: + self._trace_start() + try: + parsed = urlparse(self.path) + path = self._strip_slug(parsed.path) + if path is None: + self._not_found() + return + + if path == "/": + prefix = self._slug_prefix() + self._write_text( + 200, + "calendar fixture is running\n" + f"slug prefix: {prefix or '(none)'}\n" + f"admin: {self._with_slug('/admin.php')}?as=admin\n" + f"public calendar: {self._with_slug('/calendar')}\n" + f"api: {self._with_slug('/wp-json/calendar/v1/events')}\n" + f"ics: {self._with_slug('/calendar.ics')}\n" + f"caldav: {self._with_slug('/caldav/')}\n", + ) + return + + if path in {"/admin.php", "/admin.php/"}: + self._handle_admin_page(parsed) + return + + if path.startswith("/wp-admin/admin.php"): + self._handle_admin_page(parsed) + return + + if path == "/calendar.ics": + self._handle_ics_get() + return + + if path in {"/calendar", "/calendar/"}: + self._handle_public_calendar_page() + return + + if path in {"/calendar-sidebar", "/calendar-sidebar/"}: + self._handle_sidebar_shortcode_page() + return + + if path.startswith(API_BASE): + self._handle_api_get(path, parsed) + return + + if path.startswith("/caldav/"): + self._handle_caldav_get(path) + return + + self._not_found() + finally: + self._trace_finish() + + def _base_page(self, title: str, body_html: str) -> str: + return f""" + + + + + {_html_escape(title)} + + + +{body_html} + +""" + + def _handle_public_calendar_page(self) -> None: + page_html = """ +
Calendar Fixture Public Calendar UI
+
+
+
+
+
+ +
+
+
+ + + + + +
+
+ ICS Link + CalDAV +
+
+ +
+
+
+
+ + + +
+ +""" + page_html = page_html.replace("__ICS__", self._with_slug("/calendar.ics")) + page_html = page_html.replace("__CALDAV__", self._with_slug("/caldav/")) + page_html = page_html.replace("__ROOT__", json.dumps(self._slug_prefix())) + html = self._base_page("Calendar", page_html) + self._write_text(200, html, "text/html; charset=utf-8") + + def _handle_sidebar_shortcode_page(self) -> None: + page_html = """ +
Calendar Fixture Sidebar Shortcode Preview
+
+
+
Shortcode: [calendar_sidebar_upcoming]
+
Window: next 14 days
+
+
+
+ +""" + page_html = page_html.replace("__ROOT__", json.dumps(self._slug_prefix())) + html = self._base_page("Calendar Sidebar Shortcode", page_html) + self._write_text(200, html, "text/html; charset=utf-8") + + def do_POST(self) -> None: + self._trace_start() + try: + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self._not_found() + return + if path.startswith(API_BASE): + self._handle_api_post(path) + return + self._not_found() + finally: + self._trace_finish() + + def do_PUT(self) -> None: + self._trace_start() + try: + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self._not_found() + return + if path.startswith(API_BASE): + self._handle_api_put(path) + return + if path.startswith("/caldav/"): + self._handle_caldav_put(path) + return + self._not_found() + finally: + self._trace_finish() + + def do_PATCH(self) -> None: + self._trace_start() + try: + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self._not_found() + return + if path.startswith(API_BASE): + self._handle_api_patch(path) + return + self._not_found() + finally: + self._trace_finish() + + def do_DELETE(self) -> None: + self._trace_start() + try: + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self._not_found() + return + if path.startswith(API_BASE): + self._handle_api_delete(path) + return + if path.startswith("/caldav/"): + self._handle_caldav_delete(path) + return + self._not_found() + finally: + self._trace_finish() + + def do_PROPFIND(self) -> None: + self._trace_start() + try: + self._read_request_body_bytes() + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self._not_found() + return + if not path.startswith("/caldav/"): + self._not_found() + return + auth = self._authenticate_caldav_user() + if not auth: + self.send_response(401) + self.send_header("WWW-Authenticate", 'Basic realm="calendar-caldav-fixture"') + self.end_headers() + return + prefix = self._slug_prefix() + caldav_root = f"{prefix}/caldav/" + principals_root = f"{prefix}/caldav/principals/" + principal_href = f"{prefix}/caldav/principals/user/" + calendars_root = f"{prefix}/caldav/calendars/" + public_calendar = f"{prefix}/caldav/calendars/public/" + conn = ensure_db() + caldav_calendar_name = get_setting(conn, "caldav_calendar_name", "Public Calendar") + conn.close() + def response_block(href: str, prop_xml: str) -> str: + return textwrap.dedent( + f"""\ + + {href} + + + {prop_xml} + + HTTP/1.1 200 OK + + + """ + ).strip() + + root_props = ( + "" + f"{principal_href}" + ) + principal_props = ( + "" + f"{calendars_root}" + ) + calendar_home_props = "" + public_calendar_props = ( + "" + f"{_xml_escape(caldav_calendar_name)}" + "" + "" + "" + ) + + responses: List[str] = [] + canonical = path if path.endswith("/") else f"{path}/" + if canonical in {"/caldav/", "/caldav"}: + responses.append(response_block(caldav_root, root_props)) + if self.headers.get("Depth", "0") != "0": + responses.append(response_block(principal_href, principal_props)) + responses.append(response_block(calendars_root, calendar_home_props)) + responses.append(response_block(public_calendar, public_calendar_props)) + elif canonical == "/caldav/principals/": + responses.append(response_block(principals_root, "")) + if self.headers.get("Depth", "0") != "0": + responses.append(response_block(principal_href, principal_props)) + elif canonical == "/caldav/principals/user/": + responses.append(response_block(principal_href, principal_props)) + elif canonical == "/caldav/calendars/": + responses.append(response_block(calendars_root, calendar_home_props)) + if self.headers.get("Depth", "0") != "0": + responses.append(response_block(public_calendar, public_calendar_props)) + elif canonical == "/caldav/calendars/public/": + responses.append(response_block(public_calendar, public_calendar_props)) + if self.headers.get("Depth", "0") != "0": + conn = ensure_db() + rows = conn.execute( + "SELECT id, etag, caldav_resource AS resource FROM events WHERE calendar_id = ? AND caldav_resource IS NOT NULL ORDER BY id ASC", + (SHARED_CALENDAR_ID,), + ).fetchall() + conn.close() + for row in rows: + href = f"{prefix}/caldav/calendars/public/{row['resource']}" + item_props = ( + "" + "text/calendar; charset=utf-8" + f"{row['etag']}" + ) + responses.append(response_block(href, item_props)) + else: + self._not_found() + return + + body = ( + '\n' + '\n' + + "\n".join(responses) + + "\n\n" + ) + self.send_response(207) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(body.encode("utf-8")))) + self.end_headers() + self.wfile.write(body.encode("utf-8")) + finally: + self._trace_finish() + + def do_REPORT(self) -> None: + self._trace_start() + try: + request_body = self._read_request_body_bytes().decode("utf-8", errors="replace") + path = self._strip_slug(urlparse(self.path).path) + if path is None: + self._not_found() + return + if not path.startswith("/caldav/"): + self._not_found() + return + auth = self._authenticate_caldav_user() + if not auth: + self.send_response(401) + self.send_header("WWW-Authenticate", 'Basic realm="calendar-caldav-fixture"') + self.end_headers() + return + conn = ensure_db() + all_rows = conn.execute( + "SELECT * FROM events WHERE calendar_id = ? AND caldav_resource IS NOT NULL ORDER BY id ASC", (SHARED_CALENDAR_ID,) + ).fetchall() + conn.close() + rows = list(all_rows) + requested_hrefs = re.findall(r"\s*([^<]+)\s*", request_body, re.IGNORECASE) + if "calendar-multiget" in request_body.lower() and requested_hrefs: + wanted_resources: set[str] = set() + for href in requested_hrefs: + href_path = urlparse(href.strip()).path + stripped = self._strip_slug(href_path) + if stripped is None: + stripped = href_path + filename = _caldav_filename_from_path(stripped) + if filename: + wanted_resources.add(filename) + if wanted_resources: + rows = [r for r in all_rows if str(r["caldav_resource"]) in wanted_resources] + else: + rows = [] + prefix = self._slug_prefix() + responses = [] + for row in rows: + evt = event_to_dict(row) + href = f"{prefix}/caldav/calendars/public/{row['caldav_resource']}" + ics = self._event_as_ics(evt) + responses.append( + textwrap.dedent( + f"""\ + + {href} + + + {row["etag"]} + {_xml_escape(ics)} + + HTTP/1.1 200 OK + + + """ + ).strip() + ) + body = ( + '\n' + '\n' + + "\n".join(responses) + + "\n\n" + ) + self.send_response(207) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(body.encode("utf-8")))) + self.end_headers() + self.wfile.write(body.encode("utf-8")) + finally: + self._trace_finish() + + def _handle_admin_page(self, parsed) -> None: + auth = self._authenticate_wp_admin() + if not auth: + html = self._base_page( + "Admin Access", + f""" +
Calendar Fixture Admin
+
+
+
Admin auth required (use ?as=admin or ?as=editor in fixture).
+

Useful Links

+ +
+
+""", + ) + self._write_text(401, html, "text/html; charset=utf-8") + return + query = parse_qs(parsed.query) + page_values = query.get("page") or [] + page = page_values[0] if page_values else "" + as_user = (query.get("as") or ["admin"])[0] + allowed = {"calendar-users", "calendar-setup", "calendar-diagnostics"} + if page and page not in allowed: + self._write_text(404, "unknown admin page") + return + if page in {"calendar-users", "calendar-setup", "calendar-diagnostics"} and auth.role != "wp_admin": + self._write_text(403, "forbidden") + return + nav = f""" +
+ Calendar Fixture Admin   + Users + Setup + Diagnostics + Public Calendar +
+
+
Authenticated as: {auth.actor_id} ({auth.role})
+""" + if not page: + body = nav + f""" +
+

Admin Pages

+ +
+
+""" + html = self._base_page("Admin Index", body) + if page == "calendar-users": + body = nav + self._admin_users_body(as_user) + "" + html = self._base_page("Users", body) + elif page == "calendar-setup": + body = nav + self._admin_setup_body(as_user) + "" + html = self._base_page("Setup", body) + elif page == "calendar-diagnostics": + body = nav + self._admin_diagnostics_body(as_user) + "" + html = self._base_page("Diagnostics", body) + self._write_text(200, html, "text/html; charset=utf-8") + + def _admin_edit_body(self, as_user: str) -> str: + return f""" +
+

Events

+
+
+
+ + +
+
+
+

Events

+
TitleStartRepeatActions
+
+ +

+  
+"""
+
+    def _admin_users_body(self, as_user: str) -> str:
+        return f"""
+  
+

Users

+ + + + +
IDEmailEmail VerifiedStatusAction
+
+

+  
+"""
+
+    def _admin_setup_body(self, as_user: str) -> str:
+        return f"""
+  
+

Setup

+
+
+
+
+ + +
+
+

Shortcode Semantics

+

[calendar] renders the full interactive calendar page (views, filters, login, and event edit for approved users).

+

[calendar_sidebar_upcoming] renders a compact upcoming-events list for sidebar/widget use, showing occurrences for the next 14 days.

+

If URL Slug is set, all plugin routes are prefixed with that slug (for example: /my-slug/calendar, /my-slug/calendar-sidebar, /my-slug/calendar.ics, /my-slug/caldav/).

+
+

+  
+"""
+
+    def _admin_diagnostics_body(self, as_user: str) -> str:
+        return f"""
+  
+

Diagnostics

+

Shows the most recent HTTP trace events (requests/responses) from the fixture log.

+ + + + +
TimestampMethodPathStatusRequestResponse
+
+

+  
+"""
+
+    def _handle_api_get(self, path: str, parsed) -> None:
+        if path == f"{API_BASE}/public/events":
+            query = parse_qs(parsed.query)
+            view = (query.get("view") or ["month"])[0]
+            date_str = (query.get("date") or [date.today().isoformat()])[0]
+            try:
+                anchor = date.fromisoformat(date_str)
+            except ValueError:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid date"}})
+                return
+            window_start, window_end = _window_for_view(view, anchor)
+            conn = ensure_db()
+            rows = conn.execute("SELECT * FROM events WHERE calendar_id = ? ORDER BY id ASC", (SHARED_CALENDAR_ID,)).fetchall()
+            ex_rows = conn.execute(
+                "SELECT event_id, occurrence_key FROM recurrence_exceptions WHERE exception_type = 'deleted_occurrence'"
+            ).fetchall()
+            conn.close()
+            ex_map: Dict[int, set] = {}
+            for ex in ex_rows:
+                ex_map.setdefault(ex["event_id"], set()).add(ex["occurrence_key"])
+            occurrences: List[Dict[str, Any]] = []
+            for row in rows:
+                evt = event_to_dict(row)
+                occurrences.extend(self._expand_event(evt, window_start, window_end, ex_map.get(evt["id"], set())))
+            occurrences.sort(key=lambda x: x["occurrence_start"])
+            self._write_json(200, {"data": occurrences, "meta": {"count": len(occurrences), "view": view}})
+            return
+
+        if path == f"{API_BASE}/public/sidebar-events":
+            anchor = date.today()
+            window_start = datetime.fromisoformat(f"{anchor.isoformat()}T00:00:00+00:00")
+            window_end = window_start + timedelta(days=14)
+            conn = ensure_db()
+            rows = conn.execute("SELECT * FROM events WHERE calendar_id = ? ORDER BY id ASC", (SHARED_CALENDAR_ID,)).fetchall()
+            ex_rows = conn.execute(
+                "SELECT event_id, occurrence_key FROM recurrence_exceptions WHERE exception_type = 'deleted_occurrence'"
+            ).fetchall()
+            conn.close()
+            ex_map: Dict[int, set] = {}
+            for ex in ex_rows:
+                ex_map.setdefault(ex["event_id"], set()).add(ex["occurrence_key"])
+            occurrences: List[Dict[str, Any]] = []
+            for row in rows:
+                evt = event_to_dict(row)
+                occurrences.extend(self._expand_event(evt, window_start, window_end, ex_map.get(evt["id"], set())))
+            occurrences.sort(key=lambda x: x["occurrence_start"])
+            self._write_json(200, {"data": occurrences, "meta": {"count": len(occurrences), "window_days": 14}})
+            return
+
+        if path == f"{API_BASE}/events":
+            auth = self._require_event_read_auth()
+            if not auth:
+                return
+            conn = ensure_db()
+            rows = conn.execute(
+                "SELECT * FROM events WHERE calendar_id = ? ORDER BY id ASC", (SHARED_CALENDAR_ID,)
+            ).fetchall()
+            conn.close()
+            items = [event_to_dict(r) for r in rows]
+            self._write_json(200, {"data": items, "meta": {"count": len(items)}})
+            return
+
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/events/(\d+)", path)
+        if m:
+            auth = self._require_event_read_auth()
+            if not auth:
+                return
+            event_id = int(m.group(1))
+            conn = ensure_db()
+            row = conn.execute(
+                "SELECT * FROM events WHERE id = ? AND calendar_id = ?", (event_id, SHARED_CALENDAR_ID)
+            ).fetchone()
+            conn.close()
+            if not row:
+                self._write_json(404, {"error": {"code": "not_found", "message": "event not found"}})
+                return
+            self._write_json(200, {"data": event_to_dict(row)})
+            return
+
+        if path == f"{API_BASE}/admin/users":
+            auth = self._require_api_admin()
+            if not auth:
+                return
+            if auth.role != "wp_admin":
+                self._write_json(403, {"error": {"code": "authorization_error", "message": "admin role required"}})
+                return
+            conn = ensure_db()
+            rows = conn.execute(
+                "SELECT id, email, email_verified_at, account_status, updated_at FROM caldav_users ORDER BY id ASC"
+            ).fetchall()
+            conn.close()
+            self._write_json(200, {"data": [dict(r) for r in rows]})
+            return
+
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/events/(\d+)/occurrences", path)
+        if m:
+            auth = self._require_event_read_auth()
+            if not auth:
+                return
+            event_id = int(m.group(1))
+            query = parse_qs(parsed.query)
+            from_s = (query.get("from") or [date.today().isoformat()])[0]
+            months = int((query.get("months") or ["3"])[0])
+            try:
+                from_d = date.fromisoformat(from_s)
+            except ValueError:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid from date"}})
+                return
+            start = datetime.fromisoformat(f"{from_d.isoformat()}T00:00:00+00:00")
+            end = _add_months(start, max(1, min(months, 12)))
+            conn = ensure_db()
+            row = conn.execute("SELECT * FROM events WHERE id = ? AND calendar_id = ?", (event_id, SHARED_CALENDAR_ID)).fetchone()
+            if not row:
+                conn.close()
+                self._write_json(404, {"error": {"code": "not_found", "message": "event not found"}})
+                return
+            ex_rows = conn.execute(
+                "SELECT occurrence_key FROM recurrence_exceptions WHERE event_id = ? AND exception_type = 'deleted_occurrence'",
+                (event_id,),
+            ).fetchall()
+            conn.close()
+            deleted = {r["occurrence_key"] for r in ex_rows}
+            occ = self._expand_event(event_to_dict(row), start, end, deleted)
+            self._write_json(200, {"data": occ})
+            return
+
+        if path == f"{API_BASE}/admin/setup":
+            auth = self._require_api_admin()
+            if not auth:
+                return
+            if auth.role != "wp_admin":
+                self._write_json(403, {"error": {"code": "authorization_error", "message": "admin role required"}})
+                return
+            conn = ensure_db()
+            payload = {
+                "presentation_name": get_setting(conn, "presentation_name", "Calendar"),
+                "caldav_calendar_name": get_setting(conn, "caldav_calendar_name", "Public Calendar"),
+                "url_slug": get_setting(conn, "url_slug", ""),
+                "ics_access_mode": get_setting(conn, "ics_access_mode", "public_read"),
+            }
+            conn.close()
+            self._write_json(200, {"data": payload})
+            return
+
+        if path == f"{API_BASE}/admin/diagnostics":
+            auth = self._require_api_admin()
+            if not auth:
+                return
+            if auth.role != "wp_admin":
+                self._write_json(403, {"error": {"code": "authorization_error", "message": "admin role required"}})
+                return
+            query = parse_qs(parsed.query)
+            try:
+                limit = int((query.get("limit") or ["20"])[0])
+            except ValueError:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid limit"}})
+                return
+            entries = self._read_trace_entries(limit=max(1, min(limit, 200)))
+            self._write_json(200, {"data": entries, "meta": {"count": len(entries)}})
+            return
+
+        if path == f"{API_BASE}/users/me":
+            creds = parse_basic_auth(self.headers.get("Authorization"))
+            email_hint = (creds[0].strip().lower() if creds else "unknown")
+            if _rate_limited("user_login", f"{self.client_address[0]}:{email_hint}", limit=20, window_seconds=300):
+                self._write_json(429, {"error": {"code": "rate_limited", "message": "Too many login attempts. Try again later."}})
+                return
+            auth = self._authenticate_caldav_user()
+            if not auth:
+                self._write_json(401, {"error": {"code": "authentication_error", "message": "Login failure"}})
+                return
+            conn = ensure_db()
+            row = conn.execute(
+                "SELECT id, email, account_status, email_verified_at FROM caldav_users WHERE id = ?",
+                (int(auth.actor_id),),
+            ).fetchone()
+            conn.close()
+            self._write_json(200, {"data": dict(row)})
+            return
+
+        self._not_found()
+
+    def _handle_api_post(self, path: str) -> None:
+        if path == f"{API_BASE}/events":
+            auth = self._require_event_write_auth()
+            if not auth:
+                return
+            payload = self._json_body()
+            normalized = self._validate_event_payload(payload)
+            if normalized is None:
+                return
+            now = utc_now_iso()
+            uid = payload.get("uid") or make_uid(f"{normalized['title']}:{normalized['start_datetime']}:{now}")
+            etag = mk_etag(f"{uid}:{now}:1")
+            conn = ensure_db()
+            cur = conn.execute(
+                """
+                INSERT INTO events
+                  (uid, title, description, location, category, all_day_event, start_datetime, end_datetime,
+                   repeat_type, repeat_interval, repeat_nth_mode, repeat_nth_day, repeat_nth_pos, repeat_nth_weekday,
+                   repeat_range_mode, repeat_count, repeat_until, timezone,
+                   calendar_id, caldav_resource, etag, sync_version, created_at, updated_at)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+                """,
+                (
+                    uid,
+                    normalized["title"],
+                    normalized["description"],
+                    normalized["location"],
+                    normalized["category"],
+                    normalized["all_day_event"],
+                    normalized["start_datetime"],
+                    normalized["end_datetime"],
+                    normalized["repeat_type"],
+                    normalized["repeat_interval"],
+                    normalized["repeat_nth_mode"],
+                    normalized["repeat_nth_day"],
+                    normalized["repeat_nth_pos"],
+                    normalized["repeat_nth_weekday"],
+                    normalized["repeat_range_mode"],
+                    normalized["repeat_count"],
+                    normalized["repeat_until"],
+                    normalized["timezone"],
+                    SHARED_CALENDAR_ID,
+                    None,
+                    etag,
+                    1,
+                    now,
+                    now,
+                ),
+            )
+            event_id = cur.lastrowid
+            conn.execute("UPDATE events SET caldav_resource = ? WHERE id = ?", (f"{event_id}.ics", event_id))
+            conn.commit()
+            row = conn.execute("SELECT * FROM events WHERE id = ?", (event_id,)).fetchone()
+            log_audit(conn, auth.actor_type, auth.actor_id, "event.create", "event", str(event_id), "success", {})
+            conn.close()
+            self._write_json(201, {"data": event_to_dict(row)})
+            return
+
+        if path == f"{API_BASE}/users/register":
+            if _rate_limited("user_register", self.client_address[0], limit=8, window_seconds=300):
+                self._write_json(429, {"error": {"code": "rate_limited", "message": "Too many registration attempts. Try again later."}})
+                return
+            payload = self._json_body()
+            email = (payload.get("email") or "").strip().lower()
+            password = payload.get("password") or ""
+            if not email or len(password) < 8:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "email and strong password required"}})
+                return
+            now = utc_now_iso()
+            conn = ensure_db()
+            try:
+                cur = conn.execute(
+                    """
+                    INSERT INTO caldav_users
+                      (email, password_hash, email_verified_at, account_status, access_level, request_state, created_at, updated_at)
+                    VALUES (?, ?, NULL, 'pending_approval', 'write', 'requested', ?, ?)
+                    """,
+                    (email, hash_password(password), now, now),
+                )
+            except sqlite3.IntegrityError:
+                conn.close()
+                self._write_json(409, {"error": {"code": "conflict_error", "message": "user already exists"}})
+                return
+            user_id = cur.lastrowid
+            token_plain = secrets.token_urlsafe(24)
+            conn.execute(
+                """
+                INSERT INTO user_tokens (user_id, token_type, token_hash, expires_at, used_at, created_at)
+                VALUES (?, 'verify_email', ?, ?, NULL, ?)
+                """,
+                (user_id, hash_password(token_plain), (datetime.now(timezone.utc) + timedelta(seconds=VERIFY_TOKEN_TTL_SECONDS)).replace(microsecond=0).isoformat(), now),
+            )
+            conn.commit()
+            conn.close()
+            sent, send_info = send_fixture_email(
+                email,
+                "Calendar account verification",
+                f"Your verification token is: {token_plain}\nUse this token in the calendar login dialog to verify your email.",
+            )
+            self._write_json(
+                201,
+                {
+                    "data": {
+                        "user_id": user_id,
+                        "status": "pending_approval",
+                        "email_status": send_info,
+                        "email_sent": sent,
+                    }
+                },
+            )
+            return
+
+        if path == f"{API_BASE}/users/verify":
+            if _rate_limited("user_verify", self.client_address[0], limit=20, window_seconds=300):
+                self._write_json(429, {"error": {"code": "rate_limited", "message": "Too many verification attempts. Try again later."}})
+                return
+            payload = self._json_body()
+            token = payload.get("token") or ""
+            conn = ensure_db()
+            now = utc_now_iso()
+            candidates = conn.execute(
+                """
+                SELECT id, user_id, token_hash, expires_at FROM user_tokens
+                WHERE token_type = 'verify_email' AND used_at IS NULL
+                ORDER BY id DESC
+                """
+            ).fetchall()
+            row = None
+            for cand in candidates:
+                if cand["expires_at"] < now:
+                    continue
+                if verify_password(token, cand["token_hash"]):
+                    row = cand
+                    break
+            if not row:
+                conn.close()
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid token"}})
+                return
+            conn.execute("UPDATE user_tokens SET used_at = ? WHERE id = ?", (now, row["id"]))
+            conn.execute(
+                "UPDATE caldav_users SET email_verified_at = ?, account_status = 'pending_approval', request_state = 'requested', updated_at = ? WHERE id = ?",
+                (now, now, row["user_id"]),
+            )
+            conn.commit()
+            requester = conn.execute("SELECT email FROM caldav_users WHERE id = ?", (row["user_id"],)).fetchone()
+            conn.close()
+            cfg = _smtp_settings()
+            admin_to = (cfg.get("SMTP_ADMIN_TO") or "").strip()
+            sent = False
+            send_info = "admin_email_not_configured"
+            if admin_to:
+                req_email = requester["email"] if requester else f"user_id={row['user_id']}"
+                host = (self.headers.get("Host") or "127.0.0.1:8080").strip()
+                proto = (self.headers.get("X-Forwarded-Proto") or "http").strip().lower()
+                scheme = "https" if proto == "https" else "http"
+                approval_path = self._with_slug("/wp-admin/admin.php?page=calendar-users") + "&as=admin"
+                approval_url = f"{scheme}://{host}{approval_path}"
+                sent, send_info = send_fixture_email(
+                    admin_to,
+                    "Calendar registration pending approval",
+                    f"User {req_email} (id {row['user_id']}) verified email and is awaiting approval.\nApprove here: {approval_url}",
+                )
+            self._write_json(200, {"data": {"status": "pending_approval", "email_status": send_info, "email_sent": sent}})
+            return
+
+        if path == f"{API_BASE}/users/forgot-password":
+            if _rate_limited("user_forgot", self.client_address[0], limit=12, window_seconds=300):
+                self._write_json(429, {"error": {"code": "rate_limited", "message": "Too many password reset requests. Try again later."}})
+                return
+            payload = self._json_body()
+            email = (payload.get("email") or "").strip().lower()
+            conn = ensure_db()
+            row = conn.execute("SELECT id FROM caldav_users WHERE email = ?", (email,)).fetchone()
+            token_plain = None
+            if row:
+                now = utc_now_iso()
+                token_plain = secrets.token_urlsafe(24)
+                conn.execute(
+                    """
+                    INSERT INTO user_tokens (user_id, token_type, token_hash, expires_at, used_at, created_at)
+                    VALUES (?, 'reset_password', ?, ?, NULL, ?)
+                    """,
+                    (row["id"], hash_password(token_plain), (datetime.now(timezone.utc) + timedelta(seconds=RESET_TOKEN_TTL_SECONDS)).replace(microsecond=0).isoformat(), now),
+                )
+                conn.commit()
+            conn.close()
+            sent = False
+            send_info = "no_matching_user"
+            if token_plain:
+                sent, send_info = send_fixture_email(
+                    email,
+                    "Calendar password reset",
+                    f"Your password reset token is: {token_plain}\nUse this token in the calendar login dialog to set a new password.",
+                )
+            self._write_json(200, {"data": {"status": "ok", "email_status": send_info, "email_sent": sent}})
+            return
+
+        if path == f"{API_BASE}/users/reset-password":
+            if _rate_limited("user_reset", self.client_address[0], limit=20, window_seconds=300):
+                self._write_json(429, {"error": {"code": "rate_limited", "message": "Too many reset attempts. Try again later."}})
+                return
+            payload = self._json_body()
+            token = payload.get("token") or ""
+            new_password = payload.get("new_password") or ""
+            if len(new_password) < 8:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "weak password"}})
+                return
+            conn = ensure_db()
+            now = utc_now_iso()
+            candidates = conn.execute(
+                """
+                SELECT id, user_id, token_hash, expires_at FROM user_tokens
+                WHERE token_type = 'reset_password' AND used_at IS NULL
+                ORDER BY id DESC
+                """
+            ).fetchall()
+            row = None
+            for cand in candidates:
+                if cand["expires_at"] < now:
+                    continue
+                if verify_password(token, cand["token_hash"]):
+                    row = cand
+                    break
+            if not row:
+                conn.close()
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid token"}})
+                return
+            conn.execute("UPDATE user_tokens SET used_at = ? WHERE id = ?", (now, row["id"]))
+            conn.execute("UPDATE caldav_users SET password_hash = ?, updated_at = ? WHERE id = ?", (hash_password(new_password), now, row["user_id"]))
+            conn.commit()
+            conn.close()
+            self._write_json(200, {"data": {"status": "password_reset"}})
+            return
+
+        self._not_found()
+
+    def _handle_api_put(self, path: str) -> None:
+        self._handle_api_patch(path)
+
+    def _handle_api_patch(self, path: str) -> None:
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/events/(\d+)", path)
+        if m:
+            auth = self._require_event_write_auth()
+            if not auth:
+                return
+            event_id = int(m.group(1))
+            payload = self._json_body()
+            normalized = self._validate_event_payload(payload)
+            if normalized is None:
+                return
+            conn = ensure_db()
+            row = conn.execute("SELECT * FROM events WHERE id = ? AND calendar_id = ?", (event_id, SHARED_CALENDAR_ID)).fetchone()
+            if not row:
+                conn.close()
+                self._write_json(404, {"error": {"code": "not_found", "message": "event not found"}})
+                return
+            if_match = self.headers.get("If-Match")
+            if if_match and if_match != row["etag"]:
+                conn.close()
+                self._write_json(412, {"error": {"code": "precondition_failed", "message": "etag mismatch"}})
+                return
+            now = utc_now_iso()
+            sync_version = int(row["sync_version"]) + 1
+            etag = mk_etag(f"{row['uid']}:{now}:{sync_version}")
+            conn.execute(
+                """
+                UPDATE events
+                SET title = ?, description = ?, location = ?, category = ?, all_day_event = ?,
+                    start_datetime = ?, end_datetime = ?, repeat_type = ?, repeat_interval = ?,
+                    repeat_nth_mode = ?, repeat_nth_day = ?, repeat_nth_pos = ?, repeat_nth_weekday = ?,
+                    repeat_range_mode = ?, repeat_count = ?, repeat_until = ?, timezone = ?,
+                    etag = ?, sync_version = ?, updated_at = ?
+                WHERE id = ?
+                """,
+                (
+                    normalized["title"],
+                    normalized["description"],
+                    normalized["location"],
+                    normalized["category"],
+                    normalized["all_day_event"],
+                    normalized["start_datetime"],
+                    normalized["end_datetime"],
+                    normalized["repeat_type"],
+                    normalized["repeat_interval"],
+                    normalized["repeat_nth_mode"],
+                    normalized["repeat_nth_day"],
+                    normalized["repeat_nth_pos"],
+                    normalized["repeat_nth_weekday"],
+                    normalized["repeat_range_mode"],
+                    normalized["repeat_count"],
+                    normalized["repeat_until"],
+                    normalized["timezone"],
+                    etag,
+                    sync_version,
+                    now,
+                    event_id,
+                ),
+            )
+            conn.commit()
+            updated = conn.execute("SELECT * FROM events WHERE id = ?", (event_id,)).fetchone()
+            log_audit(conn, auth.actor_type, auth.actor_id, "event.update", "event", str(event_id), "success", {})
+            conn.close()
+            self._write_json(200, {"data": event_to_dict(updated)})
+            return
+
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/admin/users/(\d+)", path)
+        if m:
+            auth = self._require_api_admin()
+            if not auth:
+                return
+            if auth.role != "wp_admin":
+                self._write_json(403, {"error": {"code": "authorization_error", "message": "admin role required"}})
+                return
+            user_id = int(m.group(1))
+            payload = self._json_body()
+            account_status = payload.get("account_status")
+            conn = ensure_db()
+            row = conn.execute("SELECT * FROM caldav_users WHERE id = ?", (user_id,)).fetchone()
+            if not row:
+                conn.close()
+                self._write_json(404, {"error": {"code": "not_found", "message": "user not found"}})
+                return
+            updates = []
+            params: List[Any] = []
+            if account_status in {"pending_approval", "active"}:
+                if account_status == "active" and not row["email_verified_at"]:
+                    conn.close()
+                    self._write_json(422, {"error": {"code": "validation_error", "message": "email must be verified before approval"}})
+                    return
+                updates.append("account_status = ?")
+                params.append(account_status)
+                updates.append("request_state = ?")
+                params.append("approved" if account_status == "active" else "requested")
+            if not updates:
+                conn.close()
+                self._write_json(422, {"error": {"code": "validation_error", "message": "valid account_status required"}})
+                return
+            if account_status == "active":
+                updates.append("access_level = ?")
+                params.append("write")
+            updates.append("updated_at = ?")
+            params.append(utc_now_iso())
+            params.append(user_id)
+            conn.execute(f"UPDATE caldav_users SET {', '.join(updates)} WHERE id = ?", params)
+            conn.commit()
+            updated = conn.execute(
+                "SELECT id, email, email_verified_at, account_status, updated_at FROM caldav_users WHERE id = ?",
+                (user_id,),
+            ).fetchone()
+            log_audit(conn, auth.actor_type, auth.actor_id, "user.update", "caldav_user", str(user_id), "success", payload)
+            conn.close()
+            self._write_json(200, {"data": dict(updated)})
+            return
+
+        if path == f"{API_BASE}/admin/setup":
+            auth = self._require_api_admin()
+            if not auth:
+                return
+            if auth.role != "wp_admin":
+                self._write_json(403, {"error": {"code": "authorization_error", "message": "admin role required"}})
+                return
+            payload = self._json_body()
+            presentation_name = (payload.get("presentation_name") or "").strip()
+            caldav_calendar_name = (payload.get("caldav_calendar_name") or "").strip()
+            raw_slug = (payload.get("url_slug") or "").strip().strip("/")
+            ics_access_mode = payload.get("ics_access_mode")
+            if not presentation_name:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "presentation_name required"}})
+                return
+            if not caldav_calendar_name:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "caldav_calendar_name required"}})
+                return
+            if raw_slug and not re.fullmatch(r"[a-z0-9-]+", raw_slug):
+                self._write_json(422, {"error": {"code": "validation_error", "message": "url_slug must match [a-z0-9-]+"}})
+                return
+            if ics_access_mode not in {"public_read", "authenticated_read"}:
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid ics_access_mode"}})
+                return
+            conn = ensure_db()
+            set_setting(conn, "presentation_name", presentation_name)
+            set_setting(conn, "caldav_calendar_name", caldav_calendar_name)
+            set_setting(conn, "url_slug", raw_slug)
+            set_setting(conn, "ics_access_mode", ics_access_mode)
+            log_audit(conn, auth.actor_type, auth.actor_id, "setup.update", "plugin_settings", "global", "success", payload)
+            conn.close()
+            self._write_json(200, {"data": {"presentation_name": presentation_name, "caldav_calendar_name": caldav_calendar_name, "url_slug": raw_slug, "ics_access_mode": ics_access_mode}})
+            return
+
+        self._not_found()
+
+    def _handle_api_delete(self, path: str) -> None:
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/admin/users/(\d+)$", path)
+        if m:
+            auth = self._require_api_admin()
+            if not auth:
+                return
+            if auth.role != "wp_admin":
+                self._write_json(403, {"error": {"code": "authorization_error", "message": "admin role required"}})
+                return
+            user_id = int(m.group(1))
+            conn = ensure_db()
+            row = conn.execute("SELECT id FROM caldav_users WHERE id = ?", (user_id,)).fetchone()
+            if not row:
+                conn.close()
+                self._write_json(404, {"error": {"code": "not_found", "message": "user not found"}})
+                return
+            conn.execute("DELETE FROM user_tokens WHERE user_id = ?", (user_id,))
+            conn.execute("DELETE FROM caldav_users WHERE id = ?", (user_id,))
+            conn.commit()
+            log_audit(conn, auth.actor_type, auth.actor_id, "user.delete", "caldav_user", str(user_id), "success", {})
+            conn.close()
+            self.send_response(204)
+            self.end_headers()
+            return
+
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/events/(\d+)$", path)
+        if m:
+            auth = self._require_event_write_auth()
+            if not auth:
+                return
+            event_id = int(m.group(1))
+            conn = ensure_db()
+            row = conn.execute("SELECT * FROM events WHERE id = ? AND calendar_id = ?", (event_id, SHARED_CALENDAR_ID)).fetchone()
+            if not row:
+                conn.close()
+                self._write_json(404, {"error": {"code": "not_found", "message": "event not found"}})
+                return
+            conn.execute("DELETE FROM recurrence_exceptions WHERE event_id = ?", (event_id,))
+            conn.execute("DELETE FROM events WHERE id = ?", (event_id,))
+            conn.commit()
+            log_audit(conn, auth.actor_type, auth.actor_id, "event.delete", "event", str(event_id), "success", {})
+            conn.close()
+            self.send_response(204)
+            self.end_headers()
+            return
+
+        m = re.fullmatch(rf"{re.escape(API_BASE)}/events/(\d+)/occurrences/(.+)$", path)
+        if m:
+            auth = self._require_event_write_auth()
+            if not auth:
+                return
+            event_id = int(m.group(1))
+            occurrence_key = unquote(m.group(2))
+            conn = ensure_db()
+            row = conn.execute("SELECT * FROM events WHERE id = ? AND calendar_id = ?", (event_id, SHARED_CALENDAR_ID)).fetchone()
+            if not row:
+                conn.close()
+                self._write_json(404, {"error": {"code": "not_found", "message": "event not found"}})
+                return
+            if row["repeat_type"] == "none":
+                conn.close()
+                self._write_json(422, {"error": {"code": "validation_error", "message": "event is not recurring"}})
+                return
+            normalized_occurrence_key = _normalize_occurrence_key_for_event(occurrence_key, row["start_datetime"])
+            if not normalized_occurrence_key:
+                conn.close()
+                self._write_json(422, {"error": {"code": "validation_error", "message": "invalid occurrence key"}})
+                return
+            now = utc_now_iso()
+            try:
+                conn.execute(
+                    """
+                    INSERT INTO recurrence_exceptions (event_id, occurrence_key, exception_type, override_payload, created_at, updated_at)
+                    VALUES (?, ?, 'deleted_occurrence', NULL, ?, ?)
+                    """,
+                    (event_id, normalized_occurrence_key, now, now),
+                )
+                sync_version = int(row["sync_version"]) + 1
+                etag = mk_etag(f"{row['uid']}:{now}:{sync_version}")
+                conn.execute("UPDATE events SET etag = ?, sync_version = ?, updated_at = ? WHERE id = ?", (etag, sync_version, now, event_id))
+                conn.commit()
+                log_audit(
+                    conn,
+                    auth.actor_type,
+                    auth.actor_id,
+                    "event.occurrence.delete",
+                    "event",
+                    str(event_id),
+                    "success",
+                    {"occurrence_key": normalized_occurrence_key},
+                )
+            except sqlite3.IntegrityError:
+                conn.close()
+                # Idempotent behavior: deleting an already-excepted occurrence is still success.
+                self.send_response(204)
+                self.end_headers()
+                return
+            conn.close()
+            self.send_response(204)
+            self.end_headers()
+            return
+
+        self._not_found()
+
+    def _expand_event(
+        self,
+        evt: Dict[str, Any],
+        window_start: datetime,
+        window_end: datetime,
+        deleted_occurrence_keys: set,
+    ) -> List[Dict[str, Any]]:
+        deleted_canonical = set()
+        for key in deleted_occurrence_keys:
+            deleted_canonical.add(str(key))
+            canonical = _canonical_occurrence_key(str(key))
+            if canonical:
+                deleted_canonical.add(canonical)
+
+        def is_deleted(occ_start: datetime) -> bool:
+            raw = occ_start.isoformat()
+            if raw in deleted_canonical:
+                return True
+            canonical = _canonical_occurrence_key(raw)
+            return canonical in deleted_canonical if canonical else False
+
+        start = _parse_dt_maybe_date(evt["start_datetime"])
+        end = _parse_dt_maybe_date(evt["end_datetime"])
+        duration = end - start
+        repeat_type = evt.get("repeat_type", "none")
+        interval = max(1, int(evt.get("repeat_interval") or 1))
+        repeat_mode = evt.get("repeat_range_mode") or "none"
+        max_count: Optional[int] = None
+        until_dt: Optional[datetime] = None
+        if repeat_mode == "count" and evt.get("repeat_count"):
+            max_count = int(evt["repeat_count"])
+        if repeat_mode == "until" and evt.get("repeat_until"):
+            try:
+                until_dt = _parse_dt_maybe_date(f"{evt['repeat_until']}T23:59:59+00:00")
+            except ValueError:
+                until_dt = None
+
+        def make_occurrence(occ_start: datetime) -> Dict[str, Any]:
+            occ_end = occ_start + duration
+            return {
+                "event_id": evt["id"],
+                "uid": evt["uid"],
+                "title": evt["title"],
+                "description": evt["description"],
+                "location": evt["location"],
+                "category": evt["category"],
+                "all_day_event": bool(evt.get("all_day_event")),
+                "occurrence_start": occ_start.isoformat(),
+                "occurrence_end": occ_end.isoformat(),
+                "repeat_type": repeat_type,
+            }
+
+        occurrences: List[Dict[str, Any]] = []
+        if repeat_type == "none":
+            if _occurrence_overlap(start, end, window_start, window_end):
+                if not is_deleted(start):
+                    occurrences.append(make_occurrence(start))
+            return occurrences
+
+        current = start
+        produced = 0
+        for _ in range(0, 512):
+            if max_count is not None and produced >= max_count:
+                break
+            if until_dt is not None and current > until_dt:
+                break
+            occ_end = current + duration
+            if _occurrence_overlap(current, occ_end, window_start, window_end):
+                if not is_deleted(current):
+                    occurrences.append(make_occurrence(current))
+            if current > window_end + timedelta(days=400):
+                break
+            produced += 1
+            if repeat_type == "daily":
+                current = current + timedelta(days=interval)
+            elif repeat_type in {"weekly", "custom"}:
+                current = current + timedelta(weeks=interval)
+            elif repeat_type == "monthly":
+                nth_mode = evt.get("repeat_nth_mode") or ""
+                if nth_mode == "day_of_month" and evt.get("repeat_nth_day"):
+                    next_month = _add_months(current, interval)
+                    day = int(evt["repeat_nth_day"])
+                    day = max(1, min(day, pycalendar.monthrange(next_month.year, next_month.month)[1]))
+                    current = next_month.replace(day=day)
+                elif nth_mode == "weekday_of_month" and evt.get("repeat_nth_pos") and evt.get("repeat_nth_weekday") is not None:
+                    next_month = _add_months(current, interval)
+                    pos = int(evt["repeat_nth_pos"])
+                    wd = int(evt["repeat_nth_weekday"])
+                    nth_day = _nth_weekday_of_month(next_month.year, next_month.month, wd, pos)
+                    if nth_day is None:
+                        current = next_month
+                    else:
+                        current = next_month.replace(day=nth_day)
+                else:
+                    current = _add_months(current, interval)
+            elif repeat_type == "yearly":
+                current = _add_years(current, interval)
+            else:
+                break
+        return occurrences
+
+    def _handle_ics_get(self) -> None:
+        conn = ensure_db()
+        ics_mode = get_setting(conn, "ics_access_mode", "public_read")
+        if ics_mode == "authenticated_read":
+            auth = self._authenticate_caldav_user() or self._authenticate_wp_admin()
+            if not auth:
+                conn.close()
+                self.send_response(401)
+                self.send_header("WWW-Authenticate", 'Basic realm="calendar-ics-fixture"')
+                self.end_headers()
+                return
+        rows = conn.execute(
+            "SELECT * FROM events WHERE calendar_id = ? ORDER BY id ASC", (SHARED_CALENDAR_ID,)
+        ).fetchall()
+        payloads = []
+        for row in rows:
+            evt = event_to_dict(row)
+            ex_rows = conn.execute(
+                "SELECT occurrence_key FROM recurrence_exceptions WHERE event_id = ? AND exception_type = 'deleted_occurrence' ORDER BY occurrence_key",
+                (row["id"],),
+            ).fetchall()
+            exdates = [r["occurrence_key"] for r in ex_rows]
+            payloads.append(create_ics_event(evt, exdates))
+        conn.close()
+        body = (
+            "BEGIN:VCALENDAR\r\n"
+            "VERSION:2.0\r\n"
+            "PRODID:-//Calendar WP Plugin Fixture//EN\r\n"
+            "CALSCALE:GREGORIAN\r\n"
+            f"X-WR-TIMEZONE:{DEFAULT_TIMEZONE}\r\n"
+            + ("\r\n".join(payloads) + "\r\n" if payloads else "")
+            + "END:VCALENDAR\r\n"
+        )
+        encoded = body.encode("utf-8")
+        etag = mk_etag(body)
+        self.send_response(200)
+        self.send_header("Content-Type", "text/calendar; charset=utf-8")
+        self.send_header("ETag", etag)
+        self.send_header("Content-Length", str(len(encoded)))
+        self.end_headers()
+        self.wfile.write(encoded)
+
+    def _event_as_ics(self, evt: Dict[str, Any]) -> str:
+        conn = ensure_db()
+        ex_rows = conn.execute(
+            "SELECT occurrence_key FROM recurrence_exceptions WHERE event_id = ? AND exception_type = 'deleted_occurrence' ORDER BY occurrence_key",
+            (evt["id"],),
+        ).fetchall()
+        conn.close()
+        exdates = [r["occurrence_key"] for r in ex_rows]
+        event_ics = create_ics_event(evt, exdates)
+        return "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Calendar WP Plugin Fixture//EN\r\n" + event_ics + "\r\nEND:VCALENDAR\r\n"
+
+    def _require_caldav_auth(self, write: bool = False) -> Optional[AuthContext]:
+        auth = self._authenticate_caldav_user()
+        if not auth:
+            self.send_response(401)
+            self.send_header("WWW-Authenticate", 'Basic realm="calendar-caldav-fixture"')
+            self.end_headers()
+            return None
+        if write and auth.role != "caldav_write":
+            self.send_response(403)
+            self.end_headers()
+            return None
+        return auth
+
+    def _handle_caldav_get(self, path: str) -> None:
+        auth = self._require_caldav_auth(write=False)
+        if not auth:
+            return
+
+        if path in {"/caldav/", "/caldav/calendars/", "/caldav/calendars/public/"}:
+            self._write_text(200, "caldav shared public calendar\n")
+            return
+
+        filename = _caldav_filename_from_path(path)
+        if not filename:
+            self._not_found()
+            return
+        conn = ensure_db()
+        row = _caldav_lookup_event(conn, filename)
+        conn.close()
+        if not row:
+            self._not_found()
+            return
+        evt = event_to_dict(row)
+        body = self._event_as_ics(evt).encode("utf-8")
+        self.send_response(200)
+        self.send_header("Content-Type", "text/calendar; charset=utf-8")
+        self.send_header("ETag", row["etag"])
+        self.send_header("Content-Length", str(len(body)))
+        self.end_headers()
+        self.wfile.write(body)
+
+    def _handle_caldav_put(self, path: str) -> None:
+        auth = self._require_caldav_auth(write=True)
+        if not auth:
+            return
+        filename = _caldav_filename_from_path(path)
+        if not filename:
+            self._not_found()
+            return
+        raw = self._read_request_body_bytes().decode("utf-8", errors="replace")
+        parsed = self._parse_ics_payload(raw)
+        if parsed is None:
+            self.send_response(415)
+            self.end_headers()
+            return
+        conn = ensure_db()
+        row = _caldav_lookup_event(conn, filename)
+        now = utc_now_iso()
+        status = 200
+        existing_by_uid = None
+        if row is None and parsed.get("uid"):
+            existing_by_uid = conn.execute(
+                "SELECT * FROM events WHERE calendar_id = ? AND uid = ?",
+                (SHARED_CALENDAR_ID, parsed["uid"]),
+            ).fetchone()
+        if row is None:
+            if existing_by_uid is not None:
+                row = existing_by_uid
+            else:
+                uid = parsed["uid"] or make_uid(f"caldav:{filename}:{now}")
+                etag = mk_etag(f"{uid}:{now}:1")
+                conn.execute(
+                    """
+                    INSERT INTO events
+                      (uid, title, description, location, category, all_day_event, start_datetime, end_datetime,
+                       repeat_type, repeat_interval, repeat_nth_mode, repeat_nth_day, repeat_nth_pos, repeat_nth_weekday,
+                       repeat_range_mode, repeat_count, repeat_until, timezone,
+                       calendar_id, caldav_resource, etag, sync_version, last_modified_by_user_id, created_at, updated_at)
+                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+                    """,
+                    (
+                        uid,
+                        parsed["summary"],
+                        parsed["description"],
+                        parsed["location"],
+                        "",
+                        0,
+                        parsed["start_datetime"],
+                        parsed["end_datetime"],
+                        parsed["repeat_type"],
+                        parsed["repeat_interval"],
+                        parsed["repeat_nth_mode"],
+                        parsed["repeat_nth_day"],
+                        parsed["repeat_nth_pos"],
+                        parsed["repeat_nth_weekday"],
+                        parsed["repeat_range_mode"],
+                        parsed["repeat_count"],
+                        parsed["repeat_until"],
+                        DEFAULT_TIMEZONE,
+                        SHARED_CALENDAR_ID,
+                        filename,
+                        etag,
+                        1,
+                        int(auth.actor_id),
+                        now,
+                        now,
+                    ),
+                )
+                status = 201
+                conn.commit()
+                row = conn.execute("SELECT * FROM events WHERE id = last_insert_rowid()").fetchone()
+                if row is not None and parsed.get("exdates_specified"):
+                    _sync_deleted_occurrences(
+                        conn,
+                        int(row["id"]),
+                        list(parsed.get("exdates") or []),
+                        True,
+                        now,
+                    )
+                    conn.commit()
+        if row is not None and status != 201:
+            if_match = self.headers.get("If-Match")
+            if if_match and if_match != row["etag"]:
+                conn.close()
+                self.send_response(412)
+                self.end_headers()
+                return
+            sync_version = int(row["sync_version"]) + 1
+            etag = mk_etag(f"{row['uid']}:{now}:{sync_version}")
+            conn.execute(
+                """
+                UPDATE events SET title = ?, description = ?, location = ?, start_datetime = ?, end_datetime = ?,
+                  repeat_type = ?, repeat_interval = ?, repeat_nth_mode = ?, repeat_nth_day = ?, repeat_nth_pos = ?, repeat_nth_weekday = ?,
+                  repeat_range_mode = ?, repeat_count = ?, repeat_until = ?,
+                  caldav_resource = ?, etag = ?, sync_version = ?, last_modified_by_user_id = ?, updated_at = ?
+                WHERE id = ?
+                """,
+                (
+                    parsed["summary"],
+                    parsed["description"],
+                    parsed["location"],
+                    parsed["start_datetime"],
+                    parsed["end_datetime"],
+                    parsed["repeat_type"],
+                    parsed["repeat_interval"],
+                    parsed["repeat_nth_mode"],
+                    parsed["repeat_nth_day"],
+                    parsed["repeat_nth_pos"],
+                    parsed["repeat_nth_weekday"],
+                    parsed["repeat_range_mode"],
+                    parsed["repeat_count"],
+                    parsed["repeat_until"],
+                    filename,
+                    etag,
+                    sync_version,
+                    int(auth.actor_id),
+                    now,
+                    int(row["id"]),
+                ),
+            )
+            if parsed.get("exdates_specified"):
+                _sync_deleted_occurrences(
+                    conn,
+                    int(row["id"]),
+                    list(parsed.get("exdates") or []),
+                    True,
+                    now,
+                )
+            conn.commit()
+        updated = conn.execute("SELECT id, etag FROM events WHERE id = ?", (int(row["id"]),)).fetchone()
+        log_audit(conn, auth.actor_type, auth.actor_id, "caldav.put", "event", str(updated["id"]), "success", {"resource": filename})
+        conn.close()
+        self.send_response(status)
+        self.send_header("ETag", updated["etag"])
+        self.end_headers()
+        return
+
+    def _handle_caldav_delete(self, path: str) -> None:
+        auth = self._require_caldav_auth(write=True)
+        if not auth:
+            return
+        filename = _caldav_filename_from_path(path)
+        if not filename:
+            self._not_found()
+            return
+        conn = ensure_db()
+        row = _caldav_lookup_event(conn, filename)
+        if not row:
+            conn.close()
+            self._not_found()
+            return
+        event_id = int(row["id"])
+        conn.execute("DELETE FROM recurrence_exceptions WHERE event_id = ?", (event_id,))
+        conn.execute("DELETE FROM events WHERE id = ?", (event_id,))
+        conn.commit()
+        log_audit(conn, auth.actor_type, auth.actor_id, "caldav.delete", "event", str(event_id), "success", {"resource": filename})
+        conn.close()
+        self.send_response(204)
+        self.end_headers()
+
+    def _parse_ics_payload(self, raw: str) -> Optional[Dict[str, Any]]:
+        vevents = _extract_vevents(raw)
+        vevent = _select_master_vevent(vevents)
+        if not vevent:
+            return None
+        summary = _extract_line(vevent, "SUMMARY:") or "Untitled"
+        description = _extract_line(vevent, "DESCRIPTION:") or ""
+        location = _extract_line(vevent, "LOCATION:") or ""
+        uid = _extract_line(vevent, "UID:")
+        dtstart = _extract_dt(vevent, "DTSTART")
+        dtend = _extract_dt(vevent, "DTEND")
+        if not dtstart or not dtend:
+            return None
+        rrule = _extract_line(vevent, "RRULE:")
+        exdates = _extract_exdates(vevent)
+        cancelled_occurrence_exdates = _extract_cancelled_recurrence_ids(vevents)
+        if cancelled_occurrence_exdates:
+            dedup = set(exdates)
+            for ex in cancelled_occurrence_exdates:
+                if ex not in dedup:
+                    exdates.append(ex)
+                    dedup.add(ex)
+        repeat_type = "none"
+        repeat_interval = 1
+        repeat_nth_mode = ""
+        repeat_nth_day = None
+        repeat_nth_pos = None
+        repeat_nth_weekday = None
+        repeat_range_mode = "none"
+        repeat_count = None
+        repeat_until = None
+        if rrule:
+            parts = {p.split("=", 1)[0]: p.split("=", 1)[1] for p in rrule.split(";") if "=" in p}
+            freq = parts.get("FREQ", "")
+            freq_map = {"DAILY": "daily", "WEEKLY": "weekly", "MONTHLY": "monthly", "YEARLY": "yearly"}
+            repeat_type = freq_map.get(freq, "custom")
+            if "INTERVAL" in parts:
+                try:
+                    repeat_interval = max(1, int(parts["INTERVAL"]))
+                except ValueError:
+                    repeat_interval = 1
+            if repeat_type == "monthly":
+                bymonthday = parts.get("BYMONTHDAY")
+                byday = parts.get("BYDAY", "")
+                bysetpos = parts.get("BYSETPOS")
+                if bymonthday:
+                    raw_day = bymonthday.split(",")[0].strip()
+                    try:
+                        parsed_day = int(raw_day)
+                    except ValueError:
+                        parsed_day = None
+                    if parsed_day is not None and 1 <= parsed_day <= 31:
+                        repeat_nth_mode = "day_of_month"
+                        repeat_nth_day = parsed_day
+                elif byday:
+                    day_map = {"SU": 0, "MO": 1, "TU": 2, "WE": 3, "TH": 4, "FR": 5, "SA": 6}
+                    token = byday.split(",")[0].strip().upper()
+                    m = re.fullmatch(r"([+-]?\d+)?(SU|MO|TU|WE|TH|FR|SA)", token)
+                    if m:
+                        wd = day_map[m.group(2)]
+                        pos_raw = m.group(1)
+                        if not pos_raw and bysetpos:
+                            pos_raw = bysetpos.split(",")[0].strip()
+                        try:
+                            pos = int(pos_raw) if pos_raw else None
+                        except (TypeError, ValueError):
+                            pos = None
+                        if pos is not None and (1 <= pos <= 5 or pos == -1):
+                            repeat_nth_mode = "weekday_of_month"
+                            repeat_nth_pos = pos
+                            repeat_nth_weekday = wd
+            if "COUNT" in parts:
+                repeat_range_mode = "count"
+                repeat_count = int(parts["COUNT"])
+            elif "UNTIL" in parts:
+                repeat_range_mode = "until"
+                until_raw = parts["UNTIL"].strip()
+                if len(until_raw) >= 8:
+                    repeat_until = f"{until_raw[0:4]}-{until_raw[4:6]}-{until_raw[6:8]}"
+            else:
+                repeat_range_mode = "no_end"
+        dtstart, dtend = _normalize_monthly_anchor(
+            dtstart,
+            dtend,
+            repeat_type,
+            repeat_nth_mode,
+            repeat_nth_day,
+            repeat_nth_pos,
+            repeat_nth_weekday,
+        )
+        return {
+            "uid": uid,
+            "summary": summary,
+            "description": description,
+            "location": location,
+            "start_datetime": dtstart,
+            "end_datetime": dtend,
+            "repeat_type": repeat_type,
+            "repeat_interval": repeat_interval,
+            "repeat_nth_mode": repeat_nth_mode,
+            "repeat_nth_day": repeat_nth_day,
+            "repeat_nth_pos": repeat_nth_pos,
+            "repeat_nth_weekday": repeat_nth_weekday,
+            "repeat_range_mode": repeat_range_mode,
+            "repeat_count": repeat_count,
+            "repeat_until": repeat_until,
+            "exdates": exdates,
+            "exdates_specified": bool(_extract_prop_values(vevent, "EXDATE") or cancelled_occurrence_exdates),
+        }
+
+    def log_message(self, fmt: str, *args: Any) -> None:
+        # Keep output concise for fixture runs.
+        print(f"[fixture] {self.address_string()} - {fmt % args}")
+
+
+def _extract_line(raw: str, prefix: str) -> Optional[str]:
+    for line in raw.splitlines():
+        if line.startswith(prefix):
+            return line[len(prefix) :].strip()
+    return None
+
+
+def _extract_prop_values(raw: str, name: str) -> List[str]:
+    out: List[str] = []
+    pattern = re.compile(rf"^{re.escape(name)}(?:;[^:]+)?:([^\r\n]+)$", re.IGNORECASE)
+    for line in raw.splitlines():
+        m = pattern.match(line.strip())
+        if m:
+            out.append(m.group(1).strip())
+    return out
+
+
+def _extract_first_vevent(raw: str) -> Optional[str]:
+    vevents = _extract_vevents(raw)
+    return vevents[0] if vevents else None
+
+
+def _extract_vevents(raw: str) -> List[str]:
+    out: List[str] = []
+    for m in re.finditer(r"BEGIN:VEVENT\r?\n(.*?)\r?\nEND:VEVENT", raw, re.DOTALL):
+        out.append("BEGIN:VEVENT\n" + m.group(1) + "\nEND:VEVENT")
+    return out
+
+
+def _is_cancelled_vevent(vevent: str) -> bool:
+    status = (_extract_line(vevent, "STATUS:") or "").strip().upper()
+    return status == "CANCELLED"
+
+
+def _select_master_vevent(vevents: List[str]) -> str:
+    if not vevents:
+        return ""
+    # Prefer the true series master, not detached/cancelled overrides.
+    for v in vevents:
+        if not _extract_prop_values(v, "RECURRENCE-ID") and not _is_cancelled_vevent(v):
+            return v
+    for v in vevents:
+        if not _extract_prop_values(v, "RECURRENCE-ID"):
+            return v
+    for v in vevents:
+        if _extract_line(v, "RRULE:") and not _is_cancelled_vevent(v):
+            return v
+    for v in vevents:
+        if not _is_cancelled_vevent(v):
+            return v
+    return vevents[0]
+
+
+def _parse_ics_token_to_iso(value: str) -> Optional[str]:
+    token = (value or "").strip()
+    if not token:
+        return None
+    # DATE value
+    if "T" not in token:
+        if re.fullmatch(r"\d{8}", token):
+            return f"{token[0:4]}-{token[4:6]}-{token[6:8]}T00:00:00+00:00"
+        return None
+    # DATE-TIME value
+    if token.endswith("Z"):
+        core = token[:-1]
+        try:
+            dt = datetime.strptime(core, "%Y%m%dT%H%M%S").replace(tzinfo=timezone.utc)
+            return dt.isoformat()
+        except ValueError:
+            return None
+    try:
+        dt = datetime.strptime(token[:15], "%Y%m%dT%H%M%S")
+        return dt.strftime("%Y-%m-%dT%H:%M:%S+01:00")
+    except ValueError:
+        return None
+
+
+def _extract_exdates(raw: str) -> List[str]:
+    values = _extract_prop_values(raw, "EXDATE")
+    out: List[str] = []
+    for value in values:
+        for token in value.split(","):
+            iso = _parse_ics_token_to_iso(token)
+            if iso:
+                out.append(iso)
+    return out
+
+
+def _extract_cancelled_recurrence_ids(vevents: List[str]) -> List[str]:
+    out: List[str] = []
+    for vevent in vevents:
+        rec_values = _extract_prop_values(vevent, "RECURRENCE-ID")
+        if not rec_values:
+            continue
+        if not _is_cancelled_vevent(vevent):
+            continue
+        for value in rec_values:
+            for token in value.split(","):
+                iso = _parse_ics_token_to_iso(token)
+                if iso:
+                    out.append(iso)
+    return out
+
+
+def _extract_dt(raw: str, field: str) -> Optional[str]:
+    pattern = re.compile(rf"^{field}(?:;[^:]+)?:([0-9TzZ]+)$")
+    for line in raw.splitlines():
+        m = pattern.match(line.strip())
+        if m:
+            value = m.group(1)
+            if "T" in value:
+                # Interpret as local naive fixture time and inject +01:00 offset for test determinism.
+                try:
+                    dt = datetime.strptime(value[:15], "%Y%m%dT%H%M%S")
+                    return dt.strftime("%Y-%m-%dT%H:%M:%S+01:00")
+                except ValueError:
+                    return None
+    return None
+
+
+def _sync_deleted_occurrences(
+    conn: sqlite3.Connection,
+    event_id: int,
+    exdates: List[str],
+    replace: bool,
+    now: str,
+) -> None:
+    if replace:
+        conn.execute(
+            "DELETE FROM recurrence_exceptions WHERE event_id = ? AND exception_type = 'deleted_occurrence'",
+            (event_id,),
+        )
+    if not exdates:
+        return
+    for ex in exdates:
+        canonical = _canonical_occurrence_key(ex)
+        if not canonical:
+            continue
+        conn.execute(
+            """
+            INSERT OR IGNORE INTO recurrence_exceptions
+              (event_id, occurrence_key, exception_type, override_payload, created_at, updated_at)
+            VALUES (?, ?, 'deleted_occurrence', NULL, ?, ?)
+            """,
+            (event_id, canonical, now, now),
+        )
+
+
+def _xml_escape(value: str) -> str:
+    return (
+        value.replace("&", "&")
+        .replace("<", "<")
+        .replace(">", ">")
+        .replace('"', """)
+        .replace("'", "'")
+    )
+
+
+def _html_escape(value: str) -> str:
+    return _xml_escape(value)
+
+
+def _caldav_filename_from_path(path: str) -> Optional[str]:
+    m = re.fullmatch(r"/caldav/calendars/public/([^/]+\.ics)", path)
+    if not m:
+        return None
+    return unquote(m.group(1))
+
+
+def _caldav_lookup_event(conn: sqlite3.Connection, filename: str) -> Optional[sqlite3.Row]:
+    return conn.execute(
+        "SELECT * FROM events WHERE calendar_id = ? AND caldav_resource = ?",
+        (SHARED_CALENDAR_ID, filename),
+    ).fetchone()
+
+
+def cmd_init(_: argparse.Namespace) -> int:
+    conn = ensure_db()
+    create_schema(conn)
+    conn.close()
+    print(f"initialized fixture db at {DB_PATH}")
+    return 0
+
+
+def cmd_reset(_: argparse.Namespace) -> int:
+    conn = ensure_db()
+    drop_all(conn)
+    create_schema(conn)
+    conn.close()
+    print(f"reset fixture db at {DB_PATH}")
+    return 0
+
+
+def cmd_seed(_: argparse.Namespace) -> int:
+    conn = ensure_db()
+    create_schema(conn)
+    seed_data(conn)
+    conn.close()
+    print("seeded fixture data")
+    return 0
+
+
+def cmd_run(args: argparse.Namespace) -> int:
+    conn = ensure_db()
+    create_schema(conn)
+    conn.close()
+    server = ThreadingHTTPServer((args.host, args.port), FixtureHandler)
+    trace_log = fixture_trace_log_path()
+    trace_log.parent.mkdir(parents=True, exist_ok=True)
+    print(f"fixture server listening at http://{args.host}:{args.port}")
+    print(f"http trace log: {trace_log}")
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        pass
+    finally:
+        server.server_close()
+    return 0
+
+
+def build_parser() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(description="Calendar plugin local fixture server")
+    sub = parser.add_subparsers(dest="command", required=True)
+
+    p_init = sub.add_parser("init", help="initialize fixture db schema")
+    p_init.set_defaults(func=cmd_init)
+
+    p_reset = sub.add_parser("reset", help="reset fixture db schema")
+    p_reset.set_defaults(func=cmd_reset)
+
+    p_seed = sub.add_parser("seed", help="seed fixture db data")
+    p_seed.set_defaults(func=cmd_seed)
+
+    p_run = sub.add_parser("run", help="run fixture server")
+    p_run.add_argument("--host", default="127.0.0.1")
+    p_run.add_argument("--port", type=int, default=8080)
+    p_run.set_defaults(func=cmd_run)
+
+    return parser
+
+
+def main(argv: Optional[List[str]] = None) -> int:
+    parser = build_parser()
+    args = parser.parse_args(argv)
+    return args.func(args)
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/package/calendar-plugin-0.1.0.manifest.sha256 b/package/calendar-plugin-0.1.0.manifest.sha256
new file mode 100644
index 0000000..350033f
--- /dev/null
+++ b/package/calendar-plugin-0.1.0.manifest.sha256
@@ -0,0 +1,19 @@
+e44decf1154a146e77f0c759c26455ae22ad54bff7ea175dea1c76dd95257c7b  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+58038a39389008eb1b3c4bc57a9daf6e672bf3d7ec216d16a8e2f1429ea5054e  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+a107073f4d114daf22b6bdd79b1347634f102116b046e9d60316e69b30fde805  src/Domain/SettingsService.php
+232961b0e6a0087f0b0069f52ec3db156e19b04004c0b13d4397bc1635c5ac14  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+7686d0778fc27a24ef6e6683e79dfed9d61bf40c64fab8a2793f291bdcf2935b  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+faa235d11b0fcb3a197091bf79ce3184132427d6ec7e732e5f4f5a3abb379c50  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
diff --git a/package/calendar-plugin-0.1.0.zip b/package/calendar-plugin-0.1.0.zip
new file mode 100644
index 0000000..e1f72fa
Binary files /dev/null and b/package/calendar-plugin-0.1.0.zip differ
diff --git a/package/calendar-plugin-0.1.1.manifest.sha256 b/package/calendar-plugin-0.1.1.manifest.sha256
new file mode 100644
index 0000000..8f5d294
--- /dev/null
+++ b/package/calendar-plugin-0.1.1.manifest.sha256
@@ -0,0 +1,19 @@
+af462e458d03144996d4fc18da700e49b9285a2f03941a30df4f5b6775edd1e6  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+58038a39389008eb1b3c4bc57a9daf6e672bf3d7ec216d16a8e2f1429ea5054e  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+a107073f4d114daf22b6bdd79b1347634f102116b046e9d60316e69b30fde805  src/Domain/SettingsService.php
+232961b0e6a0087f0b0069f52ec3db156e19b04004c0b13d4397bc1635c5ac14  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+7686d0778fc27a24ef6e6683e79dfed9d61bf40c64fab8a2793f291bdcf2935b  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+3cf15101dab7b51195075ae0b45930576fd76d4fe0111b6d8d7db8df444b5b7b  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
diff --git a/package/calendar-plugin-0.1.1.zip b/package/calendar-plugin-0.1.1.zip
new file mode 100644
index 0000000..d05e480
Binary files /dev/null and b/package/calendar-plugin-0.1.1.zip differ
diff --git a/package/calendar-plugin-0.1.2.manifest.sha256 b/package/calendar-plugin-0.1.2.manifest.sha256
new file mode 100644
index 0000000..9875b22
--- /dev/null
+++ b/package/calendar-plugin-0.1.2.manifest.sha256
@@ -0,0 +1,19 @@
+af462e458d03144996d4fc18da700e49b9285a2f03941a30df4f5b6775edd1e6  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+58038a39389008eb1b3c4bc57a9daf6e672bf3d7ec216d16a8e2f1429ea5054e  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+633a781fff77c39c4e06b1a04f9972c2b8bcfb21095031c5b4f6f4de65cdee9a  src/Domain/SettingsService.php
+232961b0e6a0087f0b0069f52ec3db156e19b04004c0b13d4397bc1635c5ac14  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+7686d0778fc27a24ef6e6683e79dfed9d61bf40c64fab8a2793f291bdcf2935b  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+0a10c002ffabbe349bc706f9c122d6d83c6a6a4ea6edf5fe892657ba64135c63  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
diff --git a/package/calendar-plugin-0.1.2.zip b/package/calendar-plugin-0.1.2.zip
new file mode 100644
index 0000000..9e27d19
Binary files /dev/null and b/package/calendar-plugin-0.1.2.zip differ
diff --git a/package/calendar-plugin-0.1.3.manifest.sha256 b/package/calendar-plugin-0.1.3.manifest.sha256
new file mode 100644
index 0000000..3baaf23
--- /dev/null
+++ b/package/calendar-plugin-0.1.3.manifest.sha256
@@ -0,0 +1,20 @@
+10c82cc04fa26fdea842d59090dccf5adb452705792b045108749a6d64d2d710  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+6e18669ebbf7dad5fc6a402954428283ada4f2def369206e5065f4a37554437d  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+633a781fff77c39c4e06b1a04f9972c2b8bcfb21095031c5b4f6f4de65cdee9a  src/Domain/SettingsService.php
+c870fc51a6ab8d1b2306ce61e2c94c443a954204305b14743cb913ce243a90f6  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+773eb382da988a3fa283c0041d91b008497e3157bbe5171be5a4622239c05074  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
+4217aa37b3f979f9d16b5a821c9b7d9e7d30b4dc615389495ff5f380c3080c75  uninstall.php
diff --git a/package/calendar-plugin-0.1.3.zip b/package/calendar-plugin-0.1.3.zip
new file mode 100644
index 0000000..63d9288
Binary files /dev/null and b/package/calendar-plugin-0.1.3.zip differ
diff --git a/package/calendar-plugin-0.1.4.manifest.sha256 b/package/calendar-plugin-0.1.4.manifest.sha256
new file mode 100644
index 0000000..d078325
--- /dev/null
+++ b/package/calendar-plugin-0.1.4.manifest.sha256
@@ -0,0 +1,20 @@
+21c6e7f75de826055d42ff3a530770266870652c47537002c2a78a18f1536176  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+6e18669ebbf7dad5fc6a402954428283ada4f2def369206e5065f4a37554437d  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+633a781fff77c39c4e06b1a04f9972c2b8bcfb21095031c5b4f6f4de65cdee9a  src/Domain/SettingsService.php
+c870fc51a6ab8d1b2306ce61e2c94c443a954204305b14743cb913ce243a90f6  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+8d8eb0518838370e636a1e32b82e61d5c1ece6875f85c333a6df7f8c7d46b60a  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
+bfecf2aa282942bbf143725152d3c7f682927c40c9febd19f2925b42f32c2c6d  uninstall.php
diff --git a/package/calendar-plugin-0.1.4.zip b/package/calendar-plugin-0.1.4.zip
new file mode 100644
index 0000000..d45909d
Binary files /dev/null and b/package/calendar-plugin-0.1.4.zip differ
diff --git a/package/calendar-plugin-0.1.5.manifest.sha256 b/package/calendar-plugin-0.1.5.manifest.sha256
new file mode 100644
index 0000000..05ce78e
--- /dev/null
+++ b/package/calendar-plugin-0.1.5.manifest.sha256
@@ -0,0 +1,20 @@
+21c6e7f75de826055d42ff3a530770266870652c47537002c2a78a18f1536176  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+6e18669ebbf7dad5fc6a402954428283ada4f2def369206e5065f4a37554437d  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+a6ecfcb4894883ea62daf4547b54456b10b69f0c82d92c73d8c26472476a0aff  src/Domain/SettingsService.php
+c870fc51a6ab8d1b2306ce61e2c94c443a954204305b14743cb913ce243a90f6  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+11a04e9b063188e4a4a39dae0c36d35206d0cee31e73a18baada591b58847650  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
+bfecf2aa282942bbf143725152d3c7f682927c40c9febd19f2925b42f32c2c6d  uninstall.php
diff --git a/package/calendar-plugin-0.1.5.zip b/package/calendar-plugin-0.1.5.zip
new file mode 100644
index 0000000..f92a657
Binary files /dev/null and b/package/calendar-plugin-0.1.5.zip differ
diff --git a/package/calendar-plugin-0.1.8.manifest.sha256 b/package/calendar-plugin-0.1.8.manifest.sha256
new file mode 100644
index 0000000..0dc6263
--- /dev/null
+++ b/package/calendar-plugin-0.1.8.manifest.sha256
@@ -0,0 +1,20 @@
+1bf98ffa4bf22472be330b49aeee42507aef988cb56f1dc15415e903b4e74b6a  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+37a811d322ef40dc0c3af0b62cb5ad8e19c004e0fbf76d5b46fc46f4a1c47398  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+1f337ca1d51e39bbf16e2e5d17ebb232a9a1a5b5fd1924f3800c8b6fc12c1760  src/Domain/SettingsService.php
+c870fc51a6ab8d1b2306ce61e2c94c443a954204305b14743cb913ce243a90f6  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+a1c7f4a793d3647cb4c24f3f3cef61e6a11eb45cc559ab528ebfd15cddf005da  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
+893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c  uninstall.php
diff --git a/package/calendar-plugin-0.1.8.zip b/package/calendar-plugin-0.1.8.zip
new file mode 100644
index 0000000..5a12fd2
Binary files /dev/null and b/package/calendar-plugin-0.1.8.zip differ
diff --git a/package/calendar-plugin-0.1.9.manifest.sha256 b/package/calendar-plugin-0.1.9.manifest.sha256
new file mode 100644
index 0000000..fcc9674
--- /dev/null
+++ b/package/calendar-plugin-0.1.9.manifest.sha256
@@ -0,0 +1,20 @@
+6735506aef88825a3d66d53d8a90ac75aaf57331e09aab685db2de993e92da96  calendar-plugin.php
+1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb  src/Contracts/AuthAdapterInterface.php
+25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d  src/Contracts/DatabaseAdapterInterface.php
+4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba  src/Contracts/HttpAdapterInterface.php
+15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563  src/Contracts/OptionsAdapterInterface.php
+47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8  src/Domain/CalDavService.php
+37a811d322ef40dc0c3af0b62cb5ad8e19c004e0fbf76d5b46fc46f4a1c47398  src/Domain/EventService.php
+412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f  src/Domain/IcsService.php
+a6bc2e7b2d1862603d5709ee2860ff3a28cd128b6eb15ee2e56b6bbd2ae806eb  src/Domain/RecurrenceExpander.php
+1f337ca1d51e39bbf16e2e5d17ebb232a9a1a5b5fd1924f3800c8b6fc12c1760  src/Domain/SettingsService.php
+c870fc51a6ab8d1b2306ce61e2c94c443a954204305b14743cb913ce243a90f6  src/Domain/UserService.php
+5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b  src/Infrastructure/ServiceContainer.php
+70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8  src/Infrastructure/WordPress/MigrationManager.php
+8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81  src/Infrastructure/WordPress/WordPressAuthAdapter.php
+68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c  src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
+8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b  src/Infrastructure/WordPress/WordPressHttpAdapter.php
+cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea  src/Infrastructure/WordPress/WordPressOptionsAdapter.php
+7dd9f30ce2fd45acab665e1c2a5da34ac05ad6dff9695c1da438c8a4afd7925d  src/Plugin.php
+4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0  src/bootstrap.php
+893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c  uninstall.php
diff --git a/package/calendar-plugin-0.1.9.zip b/package/calendar-plugin-0.1.9.zip
new file mode 100644
index 0000000..81cdcfb
Binary files /dev/null and b/package/calendar-plugin-0.1.9.zip differ
diff --git a/package/deployment-record-2026-03-30.md b/package/deployment-record-2026-03-30.md
new file mode 100644
index 0000000..109e603
--- /dev/null
+++ b/package/deployment-record-2026-03-30.md
@@ -0,0 +1,56 @@
+# Deployment Record - 2026-03-30
+
+- Artifact: `package/calendar-plugin-0.1.0.zip`
+- Manifest: `package/calendar-plugin-0.1.0.manifest.sha256`
+- Source revision: SVN r405 (`https://svn.chezstephens.org.uk/adrian/tools/calendar_wp_plugin`)
+- Deploy target host: `cs.chezstephens.org.uk`
+- Deploy target path: `/var/www/wordpress/wp-content/plugins/calendar-plugin`
+- Operator: Codex (with user-approved escalations)
+- Timestamp (UTC): 2026-03-30
+
+## Pre-deploy Gates (Local)
+- `compatibility-layer/e2e_wp_emulation.php`: PASS
+- `compatibility-layer/ui_e2e.sh`: PASS
+- `compatibility-layer/smoke.sh`: PASS
+- `fixture/security_smoke.sh`: PASS
+
+## Packaging Validation
+- Archive root contains single plugin folder: PASS (`calendar-plugin/`)
+- Main entry present: PASS (`calendar-plugin/calendar-plugin.php`)
+- Runtime-only source (`code/`) packaged: PASS
+
+## Deployment Procedure
+1. Uploaded artifact+manifest to remote staging `/tmp/calendar-plugin-0.1.0`.
+2. Extracted and synced to plugin path with `rsync -a --delete`.
+3. Activated plugin via WP-CLI (`wp plugin activate calendar-plugin --allow-root`).
+
+## Exact-Match Validation
+- Compared artifact and deployed SHA256 manifests (relative paths normalized).
+- `diff -u` returned no differences.
+- Result: PASS (`REDEPLOY_HASH_MATCH_OK`).
+
+## Post-deploy Checks
+- `/calendar.ics` -> `200`
+- `/caldav/` (unauthenticated) -> `401`
+- `/wp-json/calendar/v1/health` -> `200`
+- `/wp-json/calendar/v1/public/ics` -> `200`
+- Authenticated CalDAV checks:
+  - `/caldav/` -> `405`
+  - `PROPFIND /caldav/calendars/public/` -> `207`
+  - `GET /caldav/calendars/public/deploy-smoke.ics` -> `200`
+
+## Backup/Rollback
+- Previous deployment backup path (if existed):
+  - `/var/www/wordpress/wp-content/plugins/calendar-plugin.backup.prev`
+
+## Redeploy Update (Same Day)
+- Redeployed latest local UI/auth shortcode fixes from current workspace artifact.
+- Remote sync to `/var/www/wordpress/wp-content/plugins/calendar-plugin` completed.
+- Exact-match manifest validation re-run: PASS (`DEPLOY_AND_HASH_MATCH_OK`).
+- Stale duplicate plugin directory removed:
+  - `/var/www/wordpress/wp-content/plugins/calendar-plugin.backup.prev`
+- Remote validation after cleanup:
+  - only `calendar-plugin` present under plugins directory
+  - `wp plugin list` shows `calendar-plugin,active,0.1.0`
+  - `GET /calendar.ics` -> `200`
+  - `GET /wp-json/calendar/v1/health` -> `200`
diff --git a/package/staging/calendar-plugin/calendar-plugin.php b/package/staging/calendar-plugin/calendar-plugin.php
new file mode 100644
index 0000000..8abffcf
--- /dev/null
+++ b/package/staging/calendar-plugin/calendar-plugin.php
@@ -0,0 +1,22 @@
+events->listEvents() as $event) {
+            $resource = $this->resourceForEvent($event);
+            $items[] = [
+                'resource' => $resource,
+                'href' => '/caldav/calendars/public/' . $resource,
+                'uid' => (string) ($event['uid'] ?? ''),
+                'etag' => (string) ($event['etag'] ?? ''),
+                'updated_at' => (string) ($event['updated_at'] ?? ''),
+                'sync_version' => (int) ($event['sync_version'] ?? 1),
+            ];
+        }
+        return $items;
+    }
+
+    public function getObject(string $resource): ?array
+    {
+        $event = $this->findEventByResource($resource);
+        if (!$event) {
+            return null;
+        }
+        $ics = $this->ics->buildCalendar(
+            [$event],
+            fn(int $eventId): array => $this->events->getDeletedOccurrenceKeys($eventId)
+        );
+
+        return [
+            'resource' => $resource,
+            'etag' => (string) ($event['etag'] ?? ''),
+            'event' => $event,
+            'ics' => $ics,
+        ];
+    }
+
+    public function putObject(string $resource, string $icsPayload, ?string $ifMatch = null, ?string $ifNoneMatch = null, ?int $userId = null): array
+    {
+        $payload = $this->ics->parseEventFromIcs($icsPayload);
+        if ($payload === null) {
+            return ['error' => ['code' => 'invalid_ics', 'message' => 'invalid iCalendar payload', 'status' => 422]];
+        }
+
+        $existing = $this->events->getEventByResource($resource);
+        if (!$existing) {
+            $existing = $this->findEventByResource($resource);
+        }
+        if ($ifNoneMatch === '*' && $existing) {
+            return ['error' => ['code' => 'precondition_failed', 'message' => 'resource already exists', 'status' => 412]];
+        }
+        if ($ifMatch !== null) {
+            if (!$existing) {
+                return ['error' => ['code' => 'precondition_failed', 'message' => 'resource does not exist', 'status' => 412]];
+            }
+            if ((string) ($existing['etag'] ?? '') !== trim($ifMatch)) {
+                return ['error' => ['code' => 'precondition_failed', 'message' => 'etag mismatch', 'status' => 412]];
+            }
+        }
+
+        $payload['caldav_resource'] = $resource;
+        if ($userId !== null) {
+            $payload['last_modified_by_user_id'] = $userId;
+        }
+
+        $deleted = (array) ($payload['deleted_occurrence_keys'] ?? []);
+        unset($payload['deleted_occurrence_keys']);
+
+        if ($existing) {
+            $nextSyncVersion = ((int) ($existing['sync_version'] ?? 1)) + 1;
+            $payload['sync_version'] = $nextSyncVersion;
+            $payload['etag'] = $this->etagFor((string) ($payload['uid'] ?? $existing['uid'] ?? ''), $nextSyncVersion);
+            $event = $this->events->updateEvent((int) $existing['id'], $payload);
+            if (!$event) {
+                return ['error' => ['code' => 'update_failed', 'message' => 'failed to update object', 'status' => 500]];
+            }
+            $this->events->syncDeletedOccurrenceKeys((int) $event['id'], $deleted, true);
+            $event = $this->events->getEvent((int) $event['id']) ?? $event;
+
+            return ['status' => 204, 'created' => false, 'event' => $event];
+        }
+
+        $payload['sync_version'] = 1;
+        $payload['etag'] = $this->etagFor((string) ($payload['uid'] ?? ''), 1);
+        $event = $this->events->createEvent($payload);
+        $this->events->syncDeletedOccurrenceKeys((int) $event['id'], $deleted, true);
+        $event = $this->events->getEvent((int) $event['id']) ?? $event;
+
+        return ['status' => 201, 'created' => true, 'event' => $event];
+    }
+
+    public function deleteObject(string $resource): array
+    {
+        $existing = $this->findEventByResource($resource);
+        if (!$existing) {
+            return ['error' => ['code' => 'not_found', 'message' => 'resource not found', 'status' => 404]];
+        }
+
+        $ok = $this->events->deleteEvent((int) $existing['id']);
+        if (!$ok) {
+            return ['error' => ['code' => 'delete_failed', 'message' => 'failed to delete resource', 'status' => 500]];
+        }
+
+        return ['status' => 204, 'deleted' => true];
+    }
+
+    public function multiget(array $resources): array
+    {
+        $out = [];
+        foreach ($resources as $resource) {
+            $resource = basename((string) $resource);
+            if ($resource === '') {
+                continue;
+            }
+            $object = $this->getObject($resource);
+            if ($object === null) {
+                $out[] = ['resource' => $resource, 'status' => 404];
+                continue;
+            }
+            $out[] = [
+                'resource' => $resource,
+                'status' => 200,
+                'etag' => $object['etag'],
+                'ics' => $object['ics'],
+            ];
+        }
+        return $out;
+    }
+
+    public function resourceForEvent(array $event): string
+    {
+        $resource = trim((string) ($event['caldav_resource'] ?? ''));
+        if ($resource !== '') {
+            return $resource;
+        }
+        return (string) ($event['uid'] ?? 'event-' . (string) ($event['id'] ?? 0)) . '.ics';
+    }
+
+    private function etagFor(string $uid, int $version): string
+    {
+        return '"' . substr(sha1($uid . ':' . $version . ':' . gmdate('c')), 0, 16) . '"';
+    }
+
+    private function findEventByResource(string $resource): ?array
+    {
+        foreach ($this->events->listEvents() as $event) {
+            if ($this->resourceForEvent($event) === $resource) {
+                return $event;
+            }
+        }
+        return null;
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Domain/EventService.php b/package/staging/calendar-plugin/src/Domain/EventService.php
new file mode 100644
index 0000000..7e99a9b
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Domain/EventService.php
@@ -0,0 +1,709 @@
+getPrefix();
+        $stem = trim($tableStem, '_');
+        $this->eventsTable = $prefix . $stem . '_events';
+        $this->exceptionsTable = $prefix . $stem . '_recurrence_exceptions';
+    }
+
+    public function listEvents(): array
+    {
+        $rows = $this->db->getResults("SELECT * FROM {$this->eventsTable} ORDER BY id ASC");
+        return array_map([$this, 'normalizeRow'], $rows);
+    }
+
+    public function getEvent(int $id): ?array
+    {
+        $sql = $this->db->prepare("SELECT * FROM {$this->eventsTable} WHERE id = %d", $id);
+        $row = $this->db->getRow($sql);
+        return $row ? $this->normalizeRow($row) : null;
+    }
+
+    public function createEvent(array $payload): array
+    {
+        $now = gmdate('c');
+        $uid = (string) ($payload['uid'] ?? bin2hex(random_bytes(10)) . '@calendar-plugin');
+        $resource = $this->resourceFromUid($uid);
+        $title = trim((string) ($payload['title'] ?? 'Untitled'));
+        $startRaw = (string) ($payload['start_datetime'] ?? '');
+        $endRaw = (string) ($payload['end_datetime'] ?? '');
+        $start = $this->toLondonDateTimeString($startRaw);
+        $end = $this->toLondonDateTimeString($endRaw);
+        if ($start === '' || $end === '') {
+            throw new \InvalidArgumentException('start_datetime and end_datetime are required');
+        }
+        if (new DateTimeImmutable($end) < new DateTimeImmutable($start)) {
+            throw new \InvalidArgumentException('end_datetime must be at or after start_datetime');
+        }
+        $repeatType = (string) ($payload['repeat_type'] ?? 'none');
+        $repeatInterval = max(1, (int) ($payload['repeat_interval'] ?? 1));
+        $repeatNthMode = (string) ($payload['repeat_nth_mode'] ?? '');
+        $repeatNthDay = array_key_exists('repeat_nth_day', $payload) && $payload['repeat_nth_day'] !== null && $payload['repeat_nth_day'] !== ''
+            ? (int) $payload['repeat_nth_day']
+            : null;
+        $repeatNthPos = array_key_exists('repeat_nth_pos', $payload) && $payload['repeat_nth_pos'] !== null && $payload['repeat_nth_pos'] !== ''
+            ? (int) $payload['repeat_nth_pos']
+            : null;
+        $repeatNthWeekday = array_key_exists('repeat_nth_weekday', $payload) && $payload['repeat_nth_weekday'] !== null && $payload['repeat_nth_weekday'] !== ''
+            ? (int) $payload['repeat_nth_weekday']
+            : null;
+        [$start, $end] = $this->normalizeMonthlyAnchor(
+            $start,
+            $end,
+            $repeatType,
+            $repeatInterval,
+            $repeatNthMode,
+            $repeatNthDay,
+            $repeatNthPos,
+            $repeatNthWeekday
+        );
+
+        $data = [
+            'uid' => $uid,
+            'title' => $title,
+            'description' => (string) ($payload['description'] ?? ''),
+            'location' => (string) ($payload['location'] ?? ''),
+            'category' => (string) ($payload['category'] ?? ''),
+            'all_day_event' => !empty($payload['all_day_event']) ? 1 : 0,
+            'start_datetime' => $start,
+            'end_datetime' => $end,
+            'repeat_type' => $repeatType,
+            'repeat_interval' => $repeatInterval,
+            'repeat_nth_mode' => $repeatNthMode,
+            'repeat_nth_day' => $repeatNthDay,
+            'repeat_nth_pos' => $repeatNthPos,
+            'repeat_nth_weekday' => $repeatNthWeekday,
+            'repeat_range_mode' => $this->canonicalRangeMode((string) ($payload['repeat_range_mode'] ?? 'none')),
+            'repeat_count' => isset($payload['repeat_count']) ? (int) $payload['repeat_count'] : null,
+            'repeat_until' => !empty($payload['repeat_until']) ? (string) $payload['repeat_until'] : null,
+            'timezone' => (string) ($payload['timezone'] ?? 'Europe/London'),
+            'caldav_resource' => !empty($payload['caldav_resource']) ? (string) $payload['caldav_resource'] : $resource,
+            'etag' => (string) ($payload['etag'] ?? $this->makeEtag($uid, 1, $now)),
+            'sync_version' => (int) ($payload['sync_version'] ?? 1),
+            'last_modified_by_user_id' => isset($payload['last_modified_by_user_id']) ? (int) $payload['last_modified_by_user_id'] : null,
+            'created_at' => $now,
+            'updated_at' => $now,
+        ];
+
+        $inserted = $this->db->insert($this->eventsTable, $data);
+        if ($inserted === false) {
+            throw new \RuntimeException('failed to create event');
+        }
+        return (array) $this->getEvent($this->db->insertId());
+    }
+
+    public function updateEvent(int $id, array $payload): ?array
+    {
+        $existing = $this->getEvent($id);
+        if (!$existing) {
+            return null;
+        }
+        $now = gmdate('c');
+        $currentResource = trim((string) ($existing['caldav_resource'] ?? ''));
+        $fallbackResource = $this->resourceFromUid((string) ($existing['uid'] ?? ''));
+        $start = array_key_exists('start_datetime', $payload)
+            ? $this->toLondonDateTimeString((string) $payload['start_datetime'])
+            : (string) $existing['start_datetime'];
+        $end = array_key_exists('end_datetime', $payload)
+            ? $this->toLondonDateTimeString((string) $payload['end_datetime'])
+            : (string) $existing['end_datetime'];
+        if ($start !== '' && $end !== '' && new DateTimeImmutable($end) < new DateTimeImmutable($start)) {
+            throw new \InvalidArgumentException('end_datetime must be at or after start_datetime');
+        }
+        $repeatType = (string) ($payload['repeat_type'] ?? $existing['repeat_type']);
+        $repeatInterval = max(1, (int) ($payload['repeat_interval'] ?? $existing['repeat_interval']));
+        $repeatNthMode = (string) ($payload['repeat_nth_mode'] ?? ($existing['repeat_nth_mode'] ?? ''));
+        $repeatNthDay = array_key_exists('repeat_nth_day', $payload)
+            ? ($payload['repeat_nth_day'] === null || $payload['repeat_nth_day'] === '' ? null : (int) $payload['repeat_nth_day'])
+            : ($existing['repeat_nth_day'] ?? null);
+        $repeatNthPos = array_key_exists('repeat_nth_pos', $payload)
+            ? ($payload['repeat_nth_pos'] === null || $payload['repeat_nth_pos'] === '' ? null : (int) $payload['repeat_nth_pos'])
+            : ($existing['repeat_nth_pos'] ?? null);
+        $repeatNthWeekday = array_key_exists('repeat_nth_weekday', $payload)
+            ? ($payload['repeat_nth_weekday'] === null || $payload['repeat_nth_weekday'] === '' ? null : (int) $payload['repeat_nth_weekday'])
+            : ($existing['repeat_nth_weekday'] ?? null);
+        [$start, $end] = $this->normalizeMonthlyAnchor(
+            $start,
+            $end,
+            $repeatType,
+            $repeatInterval,
+            $repeatNthMode,
+            $repeatNthDay,
+            $repeatNthPos,
+            $repeatNthWeekday
+        );
+        $data = [
+            'title' => trim((string) ($payload['title'] ?? $existing['title'])),
+            'description' => (string) ($payload['description'] ?? $existing['description']),
+            'location' => (string) ($payload['location'] ?? $existing['location']),
+            'category' => (string) ($payload['category'] ?? $existing['category']),
+            'all_day_event' => array_key_exists('all_day_event', $payload)
+                ? (!empty($payload['all_day_event']) ? 1 : 0)
+                : ((bool) $existing['all_day_event'] ? 1 : 0),
+            'start_datetime' => $start,
+            'end_datetime' => $end,
+            'repeat_type' => $repeatType,
+            'repeat_interval' => $repeatInterval,
+            'repeat_nth_mode' => $repeatNthMode,
+            'repeat_nth_day' => $repeatNthDay,
+            'repeat_nth_pos' => $repeatNthPos,
+            'repeat_nth_weekday' => $repeatNthWeekday,
+            'repeat_range_mode' => $this->canonicalRangeMode((string) ($payload['repeat_range_mode'] ?? $existing['repeat_range_mode'])),
+            'repeat_count' => array_key_exists('repeat_count', $payload) ? (is_null($payload['repeat_count']) ? null : (int) $payload['repeat_count']) : $existing['repeat_count'],
+            'repeat_until' => array_key_exists('repeat_until', $payload) ? (empty($payload['repeat_until']) ? null : (string) $payload['repeat_until']) : $existing['repeat_until'],
+            'timezone' => (string) ($payload['timezone'] ?? $existing['timezone']),
+            'caldav_resource' => !empty($payload['caldav_resource'])
+                ? (string) $payload['caldav_resource']
+                : ($currentResource !== '' ? $currentResource : $fallbackResource),
+            'etag' => (string) ($payload['etag'] ?? $existing['etag'] ?? $this->makeEtag((string) $existing['uid'], (int) ($existing['sync_version'] ?? 1), $now)),
+            'sync_version' => (int) ($payload['sync_version'] ?? (($existing['sync_version'] ?? 1) + 1)),
+            'last_modified_by_user_id' => array_key_exists('last_modified_by_user_id', $payload)
+                ? (is_null($payload['last_modified_by_user_id']) ? null : (int) $payload['last_modified_by_user_id'])
+                : ($existing['last_modified_by_user_id'] ?? null),
+            'updated_at' => $now,
+        ];
+
+        $this->db->update($this->eventsTable, $data, ['id' => $id]);
+        return $this->getEvent($id);
+    }
+
+    public function deleteEvent(int $id): bool
+    {
+        $this->db->delete($this->exceptionsTable, ['event_id' => $id]);
+        $deleted = $this->db->delete($this->eventsTable, ['id' => $id]);
+        return $deleted !== false;
+    }
+
+    public function deleteOccurrence(int $eventId, string $occurrenceKey): bool
+    {
+        $event = $this->getEvent($eventId);
+        if (!$event) {
+            return false;
+        }
+        $canonical = $this->canonicalOccurrenceKey($occurrenceKey);
+        if ($canonical === null) {
+            return false;
+        }
+        if (in_array($canonical, $this->deletedKeysForEvent($eventId), true)) {
+            return true;
+        }
+        $now = gmdate('c');
+        $inserted = $this->db->insert(
+            $this->exceptionsTable,
+            [
+                'event_id' => $eventId,
+                'occurrence_key' => $canonical,
+                'exception_type' => 'deleted_occurrence',
+                'created_at' => $now,
+                'updated_at' => $now,
+            ]
+        );
+        return $inserted !== false;
+    }
+
+    public function listEventOccurrences(int $eventId, string $fromDate, int $months = 3): ?array
+    {
+        $event = $this->getEvent($eventId);
+        if (!$event) {
+            return null;
+        }
+
+        $tz = new DateTimeZone('Europe/London');
+        $start = $this->safeDate($fromDate, $tz)->setTime(0, 0, 0);
+        $months = max(1, min($months, 24));
+        $end = $start->modify('+' . $months . ' month');
+        $deleted = $this->deletedKeysForEvent($eventId);
+
+        $items = RecurrenceExpander::expand($event, $start, $end, $deleted);
+        usort(
+            $items,
+            static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start'])
+        );
+        return $items;
+    }
+
+    public function previewOccurrences(array $payload, string $fromDate, int $months = 3): array
+    {
+        $startRaw = (string) ($payload['start_datetime'] ?? '');
+        $endRaw = (string) ($payload['end_datetime'] ?? '');
+        $start = $this->toLondonDateTimeString($startRaw);
+        $end = $this->toLondonDateTimeString($endRaw);
+        if ($start === '' || $end === '') {
+            throw new \InvalidArgumentException('start_datetime and end_datetime are required');
+        }
+        if (new DateTimeImmutable($end) < new DateTimeImmutable($start)) {
+            throw new \InvalidArgumentException('end_datetime must be at or after start_datetime');
+        }
+
+        $repeatType = (string) ($payload['repeat_type'] ?? 'none');
+        if ($repeatType === 'none') {
+            return [];
+        }
+        $repeatInterval = max(1, (int) ($payload['repeat_interval'] ?? 1));
+        $repeatNthMode = (string) ($payload['repeat_nth_mode'] ?? '');
+        $repeatNthDay = array_key_exists('repeat_nth_day', $payload) && $payload['repeat_nth_day'] !== null && $payload['repeat_nth_day'] !== ''
+            ? (int) $payload['repeat_nth_day']
+            : null;
+        $repeatNthPos = array_key_exists('repeat_nth_pos', $payload) && $payload['repeat_nth_pos'] !== null && $payload['repeat_nth_pos'] !== ''
+            ? (int) $payload['repeat_nth_pos']
+            : null;
+        $repeatNthWeekday = array_key_exists('repeat_nth_weekday', $payload) && $payload['repeat_nth_weekday'] !== null && $payload['repeat_nth_weekday'] !== ''
+            ? (int) $payload['repeat_nth_weekday']
+            : null;
+        [$start, $end] = $this->normalizeMonthlyAnchor(
+            $start,
+            $end,
+            $repeatType,
+            $repeatInterval,
+            $repeatNthMode,
+            $repeatNthDay,
+            $repeatNthPos,
+            $repeatNthWeekday
+        );
+        $event = [
+            'id' => 0,
+            'uid' => 'preview@calendar-plugin',
+            'title' => (string) ($payload['title'] ?? ''),
+            'description' => (string) ($payload['description'] ?? ''),
+            'location' => (string) ($payload['location'] ?? ''),
+            'category' => (string) ($payload['category'] ?? ''),
+            'all_day_event' => !empty($payload['all_day_event']),
+            'start_datetime' => $start,
+            'end_datetime' => $end,
+            'repeat_type' => $repeatType,
+            'repeat_interval' => $repeatInterval,
+            'repeat_nth_mode' => $repeatNthMode,
+            'repeat_nth_day' => $repeatNthDay,
+            'repeat_nth_pos' => $repeatNthPos,
+            'repeat_nth_weekday' => $repeatNthWeekday,
+            'repeat_range_mode' => $this->canonicalRangeMode((string) ($payload['repeat_range_mode'] ?? 'none')),
+            'repeat_count' => isset($payload['repeat_count']) ? (int) $payload['repeat_count'] : null,
+            'repeat_until' => !empty($payload['repeat_until']) ? (string) $payload['repeat_until'] : null,
+            'timezone' => 'Europe/London',
+        ];
+
+        $tz = new DateTimeZone('Europe/London');
+        $startWindow = $this->safeDate($fromDate, $tz)->setTime(0, 0, 0);
+        $months = max(1, min($months, 24));
+        $endWindow = $startWindow->modify('+' . $months . ' month');
+        $deleted = [];
+        foreach ((array) ($payload['deleted_occurrence_keys'] ?? []) as $key) {
+            $canonical = $this->canonicalOccurrenceKey((string) $key);
+            if ($canonical !== null) {
+                $deleted[] = $canonical;
+            }
+        }
+        $items = RecurrenceExpander::expand($event, $startWindow, $endWindow, $deleted);
+        usort(
+            $items,
+            static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start'])
+        );
+        return $items;
+    }
+
+    public function getEventByResource(string $resource): ?array
+    {
+        $sql = $this->db->prepare("SELECT * FROM {$this->eventsTable} WHERE caldav_resource = %s", $resource);
+        $row = $this->db->getRow($sql);
+        return $row ? $this->normalizeRow($row) : null;
+    }
+
+    public function getDeletedOccurrenceKeys(int $eventId): array
+    {
+        return $this->deletedKeysForEvent($eventId);
+    }
+
+    public function syncDeletedOccurrenceKeys(int $eventId, array $keys, bool $replace = true): void
+    {
+        if ($replace) {
+            $this->db->delete($this->exceptionsTable, ['event_id' => $eventId, 'exception_type' => 'deleted_occurrence']);
+        }
+        $now = gmdate('c');
+        foreach ($keys as $key) {
+            $canonical = $this->canonicalOccurrenceKey((string) $key);
+            if ($canonical === null) {
+                continue;
+            }
+            $this->db->insert(
+                $this->exceptionsTable,
+                [
+                    'event_id' => $eventId,
+                    'occurrence_key' => $canonical,
+                    'exception_type' => 'deleted_occurrence',
+                    'created_at' => $now,
+                    'updated_at' => $now,
+                ]
+            );
+        }
+    }
+
+    public function listPublicOccurrences(string $view, string $dateAnchor, bool $futureOnly = false): array
+    {
+        $tz = new DateTimeZone('Europe/London');
+        $anchor = $this->safeDate($dateAnchor, $tz);
+        if (strtolower($view) === 'list') {
+            $windowStart = $anchor->setTime(0, 0, 0);
+            if ($futureOnly) {
+                $today = new DateTimeImmutable('today', $tz);
+                if ($today > $windowStart) {
+                    $windowStart = $today;
+                }
+            }
+            $windowEnd = $windowStart->modify('+18 months');
+        } else {
+            [$windowStart, $windowEnd] = $this->windowForView($view, $anchor);
+        }
+        $events = $this->listEvents();
+
+        $out = [];
+        foreach ($events as $event) {
+            $deleted = $this->deletedKeysForEvent((int) $event['id']);
+            $items = RecurrenceExpander::expand($event, $windowStart, $windowEnd, $deleted);
+            array_push($out, ...$items);
+        }
+
+        usort(
+            $out,
+            static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start'])
+        );
+
+        return $out;
+    }
+
+    public function listSidebarUpcoming(int $days = 14): array
+    {
+        $tz = new DateTimeZone('Europe/London');
+        $start = new DateTimeImmutable('today', $tz);
+        $end = $start->modify('+' . max(1, $days) . ' days');
+
+        $events = $this->listEvents();
+        $out = [];
+        foreach ($events as $event) {
+            $deleted = $this->deletedKeysForEvent((int) $event['id']);
+            $items = RecurrenceExpander::expand($event, $start, $end, $deleted);
+            array_push($out, ...$items);
+        }
+
+        usort(
+            $out,
+            static fn(array $a, array $b): int => strcmp((string) $a['occurrence_start'], (string) $b['occurrence_start'])
+        );
+
+        return $out;
+    }
+
+    public function deleteAllEventsData(): int
+    {
+        $events = $this->listEvents();
+        $count = count($events);
+        $this->db->query("DELETE FROM {$this->exceptionsTable}");
+        $this->db->query("DELETE FROM {$this->eventsTable}");
+        return $count;
+    }
+
+    public function seedDefaultEvents(): int
+    {
+        $seed = [
+            [
+                'uid' => 'seed-ce-001@calendar-plugin',
+                'title' => 'Board Meeting',
+                'description' => 'Quarterly board review.',
+                'location' => 'Room A',
+                'category' => 'Governance',
+                'start_datetime' => '2026-04-01T10:00:00+01:00',
+                'end_datetime' => '2026-04-01T11:30:00+01:00',
+                'repeat_type' => 'none',
+            ],
+            [
+                'uid' => 'seed-ce-002@calendar-plugin',
+                'title' => 'Office Closed',
+                'description' => 'Public holiday closure.',
+                'location' => 'HQ',
+                'category' => 'Operations',
+                'all_day_event' => true,
+                'start_datetime' => '2026-05-04T00:00:00+01:00',
+                'end_datetime' => '2026-05-05T00:00:00+01:00',
+                'repeat_type' => 'none',
+            ],
+            [
+                'uid' => 'seed-ce-003@calendar-plugin',
+                'title' => 'Daily Standup',
+                'description' => '15 minute sync.',
+                'location' => 'Online',
+                'category' => 'Team',
+                'start_datetime' => '2026-04-06T09:00:00+01:00',
+                'end_datetime' => '2026-04-06T09:15:00+01:00',
+                'repeat_type' => 'daily',
+                'repeat_interval' => 1,
+                'repeat_range_mode' => 'until',
+                'repeat_until' => '2026-04-15',
+            ],
+            [
+                'uid' => 'seed-ce-004@calendar-plugin',
+                'title' => 'Community Lunch',
+                'description' => 'Weekly community lunch.',
+                'location' => 'Cafeteria',
+                'category' => 'Community',
+                'start_datetime' => '2026-04-08T12:30:00+01:00',
+                'end_datetime' => '2026-04-08T13:30:00+01:00',
+                'repeat_type' => 'weekly',
+                'repeat_interval' => 1,
+                'repeat_range_mode' => 'until',
+                'repeat_until' => '2026-05-06',
+            ],
+            [
+                'uid' => 'seed-ce-005@calendar-plugin',
+                'title' => 'Finance Close',
+                'description' => 'Month-end close process.',
+                'location' => 'Finance Office',
+                'category' => 'Finance',
+                'start_datetime' => '2026-03-31T17:00:00+01:00',
+                'end_datetime' => '2026-03-31T18:00:00+01:00',
+                'repeat_type' => 'monthly',
+                'repeat_interval' => 1,
+                'repeat_nth_mode' => 'day_of_month',
+                'repeat_nth_day' => 30,
+                'repeat_range_mode' => 'until',
+                'repeat_until' => '2026-06-30',
+            ],
+        ];
+
+        foreach ($seed as $event) {
+            $this->createEvent($event);
+        }
+        return count($seed);
+    }
+
+    private function normalizeRow(object $row): array
+    {
+        return [
+            'id' => (int) $row->id,
+            'uid' => (string) $row->uid,
+            'title' => (string) $row->title,
+            'description' => (string) $row->description,
+            'location' => (string) $row->location,
+            'category' => (string) $row->category,
+            'all_day_event' => (bool) $row->all_day_event,
+            'start_datetime' => (string) $row->start_datetime,
+            'end_datetime' => (string) $row->end_datetime,
+            'repeat_type' => (string) $row->repeat_type,
+            'repeat_interval' => (int) $row->repeat_interval,
+            'repeat_nth_mode' => property_exists($row, 'repeat_nth_mode') ? (string) ($row->repeat_nth_mode ?? '') : '',
+            'repeat_nth_day' => property_exists($row, 'repeat_nth_day') && $row->repeat_nth_day !== null ? (int) $row->repeat_nth_day : null,
+            'repeat_nth_pos' => property_exists($row, 'repeat_nth_pos') && $row->repeat_nth_pos !== null ? (int) $row->repeat_nth_pos : null,
+            'repeat_nth_weekday' => property_exists($row, 'repeat_nth_weekday') && $row->repeat_nth_weekday !== null ? (int) $row->repeat_nth_weekday : null,
+            'repeat_range_mode' => (string) $row->repeat_range_mode,
+            'repeat_count' => is_null($row->repeat_count) ? null : (int) $row->repeat_count,
+            'repeat_until' => $row->repeat_until === null ? null : (string) $row->repeat_until,
+            'timezone' => (string) $row->timezone,
+            'caldav_resource' => property_exists($row, 'caldav_resource') ? (string) ($row->caldav_resource ?? '') : '',
+            'etag' => property_exists($row, 'etag') ? (string) ($row->etag ?? '') : '',
+            'sync_version' => property_exists($row, 'sync_version') ? (int) ($row->sync_version ?? 1) : 1,
+            'last_modified_by_user_id' => property_exists($row, 'last_modified_by_user_id') && $row->last_modified_by_user_id !== null
+                ? (int) $row->last_modified_by_user_id
+                : null,
+            'created_at' => (string) $row->created_at,
+            'updated_at' => (string) $row->updated_at,
+        ];
+    }
+
+    private function deletedKeysForEvent(int $eventId): array
+    {
+        $sql = $this->db->prepare(
+            "SELECT occurrence_key FROM {$this->exceptionsTable} WHERE event_id = %d AND exception_type = 'deleted_occurrence'",
+            $eventId
+        );
+        $rows = $this->db->getResults($sql);
+        return array_map(static fn(object $r): string => (string) $r->occurrence_key, $rows);
+    }
+
+    private function safeDate(string $dateAnchor, DateTimeZone $tz): DateTimeImmutable
+    {
+        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateAnchor)) {
+            return new DateTimeImmutable($dateAnchor . 'T00:00:00', $tz);
+        }
+        return new DateTimeImmutable('today', $tz);
+    }
+
+    private function windowForView(string $view, DateTimeImmutable $anchor): array
+    {
+        $view = strtolower($view);
+        if ($view === 'day') {
+            $start = $anchor->setTime(0, 0, 0);
+            return [$start, $start->modify('+1 day')];
+        }
+        if ($view === 'week') {
+            $weekday = (int) $anchor->format('w');
+            $start = $anchor->modify('-' . $weekday . ' day')->setTime(0, 0, 0);
+            return [$start, $start->modify('+7 days')];
+        }
+        if ($view === 'year') {
+            $start = $anchor->setDate((int) $anchor->format('Y'), 1, 1)->setTime(0, 0, 0);
+            return [$start, $start->modify('+1 year')];
+        }
+        $monthStart = $anchor->setDate((int) $anchor->format('Y'), (int) $anchor->format('m'), 1)->setTime(0, 0, 0);
+        $startWeekday = (int) $monthStart->format('w');
+        $gridStart = $monthStart->modify('-' . $startWeekday . ' day');
+        $gridEnd = $gridStart->modify('+42 days');
+        return [$gridStart, $gridEnd];
+    }
+
+    private function canonicalOccurrenceKey(string $value): ?string
+    {
+        try {
+            if (!str_contains($value, 'T') && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
+                $dt = new DateTimeImmutable($value . 'T00:00:00', new DateTimeZone('Europe/London'));
+                return $dt->format('c');
+            }
+            $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London'));
+            return $dt->format('c');
+        } catch (\Throwable) {
+            return null;
+        }
+    }
+
+    private function makeEtag(string $uid, int $syncVersion, string $stamp): string
+    {
+        return '"' . substr(sha1($uid . ':' . $syncVersion . ':' . $stamp), 0, 16) . '"';
+    }
+
+    private function resourceFromUid(string $uid): string
+    {
+        $uid = trim($uid);
+        if ($uid === '') {
+            $uid = bin2hex(random_bytes(10)) . '@calendar-plugin';
+        }
+        return $uid . '.ics';
+    }
+
+    private function toLondonDateTimeString(string $value): string
+    {
+        $value = trim($value);
+        if ($value === '') {
+            return '';
+        }
+        try {
+            $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London'));
+            return $dt->setTimezone(new DateTimeZone('Europe/London'))->format('c');
+        } catch (\Throwable) {
+            throw new \InvalidArgumentException('invalid datetime value');
+        }
+    }
+
+    private function canonicalRangeMode(string $value): string
+    {
+        $v = strtolower(trim($value));
+        if ($v === 'no_end' || $v === '') {
+            return 'none';
+        }
+        return in_array($v, ['none', 'count', 'until'], true) ? $v : 'none';
+    }
+
+    private function normalizeMonthlyAnchor(
+        string $startIso,
+        string $endIso,
+        string $repeatType,
+        int $repeatInterval,
+        string $repeatNthMode,
+        ?int $repeatNthDay,
+        ?int $repeatNthPos,
+        ?int $repeatNthWeekday
+    ): array {
+        if ($repeatType !== 'monthly') {
+            return [$startIso, $endIso];
+        }
+        try {
+            $tz = new DateTimeZone('Europe/London');
+            $start = new DateTimeImmutable($startIso, $tz);
+            $end = new DateTimeImmutable($endIso, $tz);
+            $duration = $start->diff($end);
+            $targetDay = (int) $start->format('j');
+            if ($repeatNthMode === 'day_of_month' && $repeatNthDay !== null) {
+                $daysInMonth = (int) $start->format('t');
+                $targetDay = max(1, min($repeatNthDay, $daysInMonth));
+            } elseif ($repeatNthMode === 'weekday_of_month' && $repeatNthPos !== null && $repeatNthWeekday !== null) {
+                $nthDay = $this->nthWeekdayOfMonth((int) $start->format('Y'), (int) $start->format('n'), $repeatNthWeekday, $repeatNthPos);
+                if ($nthDay === null) {
+                    $year = (int) $start->format('Y');
+                    $month = (int) $start->format('n');
+                    $step = max(1, $repeatInterval);
+                    for ($i = 0; $i < 120; $i++) {
+                        [$year, $month] = $this->addMonths($year, $month, $step);
+                        $nthDay = $this->nthWeekdayOfMonth($year, $month, $repeatNthWeekday, $repeatNthPos);
+                        if ($nthDay !== null) {
+                            $start = $start->setDate($year, $month, $nthDay);
+                            $targetDay = $nthDay;
+                            break;
+                        }
+                    }
+                } else {
+                    $targetDay = $nthDay;
+                }
+            }
+            $anchoredStart = $start->setDate((int) $start->format('Y'), (int) $start->format('n'), $targetDay);
+            $anchoredEnd = $anchoredStart->add($duration);
+            return [$anchoredStart->format('c'), $anchoredEnd->format('c')];
+        } catch (\Throwable) {
+            return [$startIso, $endIso];
+        }
+    }
+
+    private function nthWeekdayOfMonth(int $year, int $month, int $weekday, int $pos): ?int
+    {
+        $weekday = max(0, min(6, $weekday));
+        $tz = new DateTimeZone('Europe/London');
+        if ($pos === -1) {
+            $last = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz);
+            $last = $last->modify('last day of this month');
+            for ($day = (int) $last->format('j'); $day >= 1; $day--) {
+                $d = $last->setDate($year, $month, $day);
+                if ((int) $d->format('w') === $weekday) {
+                    return $day;
+                }
+            }
+            return null;
+        }
+
+        $first = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz);
+        $daysInMonth = (int) $first->format('t');
+        $seen = 0;
+        for ($day = 1; $day <= $daysInMonth; $day++) {
+            $d = $first->setDate($year, $month, $day);
+            if ((int) $d->format('w') !== $weekday) {
+                continue;
+            }
+            $seen++;
+            if ($seen === $pos) {
+                return $day;
+            }
+        }
+        return null;
+    }
+
+    private function addMonths(int $year, int $month, int $delta): array
+    {
+        $index = ($year * 12) + ($month - 1) + $delta;
+        $newYear = (int) floor($index / 12);
+        $newMonth = ($index % 12) + 1;
+        if ($newMonth <= 0) {
+            $newMonth += 12;
+            $newYear -= 1;
+        }
+        return [$newYear, $newMonth];
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Domain/IcsService.php b/package/staging/calendar-plugin/src/Domain/IcsService.php
new file mode 100644
index 0000000..c974626
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Domain/IcsService.php
@@ -0,0 +1,439 @@
+escapeText($calendarName),
+            'X-WR-TIMEZONE:Europe/London',
+        ];
+
+        foreach ($events as $event) {
+            $lines = array_merge($lines, $this->eventToLines($event, (array) $deletedKeysProvider((int) ($event['id'] ?? 0))));
+        }
+
+        $lines[] = 'END:VCALENDAR';
+
+        return implode("\r\n", $this->foldLines($lines)) . "\r\n";
+    }
+
+    public function parseEventFromIcs(string $ics): ?array
+    {
+        $props = $this->extractVeventProperties($ics);
+        if ($props === null) {
+            return null;
+        }
+
+        $uid = (string) ($props['UID'][0] ?? '');
+        $summary = (string) ($props['SUMMARY'][0] ?? 'Untitled');
+        $description = (string) ($props['DESCRIPTION'][0] ?? '');
+        $location = (string) ($props['LOCATION'][0] ?? '');
+        $category = (string) ($props['CATEGORIES'][0] ?? '');
+        $dtstartRaw = (string) ($props['DTSTART'][0] ?? '');
+        $dtendRaw = (string) ($props['DTEND'][0] ?? '');
+        if ($dtstartRaw === '' || $dtendRaw === '') {
+            return null;
+        }
+
+        $allDay = str_contains((string) ($props['_DTSTART_PARAMS'][0] ?? ''), 'VALUE=DATE');
+        $start = $this->parseIcsDateTime($dtstartRaw, $allDay);
+        $end = $this->parseIcsDateTime($dtendRaw, $allDay);
+        if ($start === null || $end === null) {
+            return null;
+        }
+
+        $payload = [
+            'uid' => $uid !== '' ? $uid : bin2hex(random_bytes(10)) . '@calendar-plugin',
+            'title' => $summary,
+            'description' => $description,
+            'location' => $location,
+            'category' => $category,
+            'all_day_event' => $allDay,
+            'start_datetime' => $start,
+            'end_datetime' => $end,
+            'repeat_type' => 'none',
+            'repeat_interval' => 1,
+            'repeat_nth_mode' => '',
+            'repeat_nth_day' => null,
+            'repeat_nth_pos' => null,
+            'repeat_nth_weekday' => null,
+            'repeat_range_mode' => 'none',
+            'repeat_count' => null,
+            'repeat_until' => null,
+            'timezone' => 'Europe/London',
+        ];
+
+        $rrule = (string) ($props['RRULE'][0] ?? '');
+        if ($rrule !== '') {
+            $payload = array_merge($payload, $this->parseRrule($rrule));
+        }
+
+        $exdates = [];
+        foreach (($props['EXDATE'] ?? []) as $exdateRaw) {
+            $chunks = array_filter(array_map('trim', explode(',', (string) $exdateRaw)));
+            foreach ($chunks as $chunk) {
+                $asDate = $this->parseIcsDateTime($chunk, false);
+                if ($asDate !== null) {
+                    $exdates[] = $asDate;
+                }
+            }
+        }
+        $payload['deleted_occurrence_keys'] = $exdates;
+
+        return $payload;
+    }
+
+    private function eventToLines(array $event, array $deletedKeys): array
+    {
+        $uid = (string) ($event['uid'] ?? '');
+        $uid = $uid !== '' ? $uid : ('event-' . (string) ($event['id'] ?? 0) . '@calendar-plugin');
+
+        $start = $this->toDateTime((string) ($event['start_datetime'] ?? ''));
+        $end = $this->toDateTime((string) ($event['end_datetime'] ?? ''));
+        if ($start === null || $end === null) {
+            return [];
+        }
+
+        $allDay = (bool) ($event['all_day_event'] ?? false);
+        $updated = $this->toDateTime((string) ($event['updated_at'] ?? '')) ?? new DateTimeImmutable('now', new DateTimeZone('UTC'));
+
+        $lines = [
+            'BEGIN:VEVENT',
+            'UID:' . $this->escapeText($uid),
+            'SUMMARY:' . $this->escapeText((string) ($event['title'] ?? 'Untitled')),
+            'DESCRIPTION:' . $this->escapeText((string) ($event['description'] ?? '')),
+            'LOCATION:' . $this->escapeText((string) ($event['location'] ?? '')),
+            'CATEGORIES:' . $this->escapeText((string) ($event['category'] ?? '')),
+            'DTSTAMP:' . $this->toUtcIcs($updated),
+            'LAST-MODIFIED:' . $this->toUtcIcs($updated),
+        ];
+
+        if ($allDay) {
+            $lines[] = 'DTSTART;VALUE=DATE:' . $start->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd');
+            $lines[] = 'DTEND;VALUE=DATE:' . $end->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd');
+        } else {
+            $lines[] = 'DTSTART;TZID=Europe/London:' . $start->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis');
+            $lines[] = 'DTEND;TZID=Europe/London:' . $end->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis');
+        }
+
+        $rrule = $this->eventToRrule($event);
+        if ($rrule !== null) {
+            $lines[] = 'RRULE:' . $rrule;
+        }
+
+        if ($deletedKeys) {
+            $parts = [];
+            foreach ($deletedKeys as $key) {
+                $dt = $this->toDateTime((string) $key);
+                if ($dt === null) {
+                    continue;
+                }
+                $parts[] = $dt->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis');
+            }
+            if ($parts) {
+                $lines[] = 'EXDATE;TZID=Europe/London:' . implode(',', $parts);
+            }
+        }
+
+        $lines[] = 'END:VEVENT';
+        return $lines;
+    }
+
+    private function eventToRrule(array $event): ?string
+    {
+        $type = strtolower((string) ($event['repeat_type'] ?? 'none'));
+        if ($type === 'none') {
+            return null;
+        }
+
+        $freq = match ($type) {
+            'daily' => 'DAILY',
+            'weekly', 'custom' => 'WEEKLY',
+            'monthly' => 'MONTHLY',
+            'yearly' => 'YEARLY',
+            default => null,
+        };
+        if ($freq === null) {
+            return null;
+        }
+
+        $interval = max(1, (int) ($event['repeat_interval'] ?? 1));
+        $parts = ['FREQ=' . $freq, 'INTERVAL=' . $interval];
+        if ($type === 'monthly') {
+            $nthMode = (string) ($event['repeat_nth_mode'] ?? '');
+            $nthDay = isset($event['repeat_nth_day']) && $event['repeat_nth_day'] !== null ? (int) $event['repeat_nth_day'] : null;
+            $nthPos = isset($event['repeat_nth_pos']) && $event['repeat_nth_pos'] !== null ? (int) $event['repeat_nth_pos'] : null;
+            $nthWeekday = isset($event['repeat_nth_weekday']) && $event['repeat_nth_weekday'] !== null ? (int) $event['repeat_nth_weekday'] : null;
+            if ($nthMode === 'day_of_month' && $nthDay !== null) {
+                $parts[] = 'BYMONTHDAY=' . max(1, min(31, $nthDay));
+            } elseif ($nthMode === 'weekday_of_month' && $nthPos !== null && $nthWeekday !== null) {
+                $byDay = $this->weekdayNumToToken($nthWeekday);
+                if ($byDay !== null) {
+                    $parts[] = 'BYDAY=' . $byDay;
+                    $parts[] = 'BYSETPOS=' . ($nthPos < 0 ? -1 : max(1, min(5, $nthPos)));
+                }
+            }
+        }
+
+        $rangeMode = strtolower((string) ($event['repeat_range_mode'] ?? 'none'));
+        if ($rangeMode === 'count' && !empty($event['repeat_count'])) {
+            $parts[] = 'COUNT=' . max(1, (int) $event['repeat_count']);
+        }
+        if ($rangeMode === 'until' && !empty($event['repeat_until'])) {
+            $until = $this->toDateTime((string) $event['repeat_until'] . 'T23:59:59');
+            if ($until !== null) {
+                $parts[] = 'UNTIL=' . $this->toUtcIcs($until);
+            }
+        }
+
+        return implode(';', $parts);
+    }
+
+    private function parseRrule(string $rrule): array
+    {
+        $parts = [];
+        foreach (explode(';', strtoupper(trim($rrule))) as $chunk) {
+            [$k, $v] = array_pad(explode('=', $chunk, 2), 2, '');
+            if ($k !== '') {
+                $parts[$k] = $v;
+            }
+        }
+
+        $repeatType = match ($parts['FREQ'] ?? '') {
+            'DAILY' => 'daily',
+            'WEEKLY' => 'weekly',
+            'MONTHLY' => 'monthly',
+            'YEARLY' => 'yearly',
+            default => 'none',
+        };
+
+        $payload = [
+            'repeat_type' => $repeatType,
+            'repeat_interval' => max(1, (int) ($parts['INTERVAL'] ?? 1)),
+            'repeat_nth_mode' => '',
+            'repeat_nth_day' => null,
+            'repeat_nth_pos' => null,
+            'repeat_nth_weekday' => null,
+            'repeat_range_mode' => 'none',
+            'repeat_count' => null,
+            'repeat_until' => null,
+        ];
+
+        if ($repeatType === 'monthly') {
+            if (!empty($parts['BYMONTHDAY'])) {
+                $raw = trim(explode(',', (string) $parts['BYMONTHDAY'])[0]);
+                if (preg_match('/^-?\d+$/', $raw)) {
+                    $payload['repeat_nth_mode'] = 'day_of_month';
+                    $payload['repeat_nth_day'] = max(1, min(31, (int) $raw));
+                }
+            } elseif (!empty($parts['BYDAY'])) {
+                $byDayRaw = trim(explode(',', (string) $parts['BYDAY'])[0]);
+                $pos = null;
+                $token = $byDayRaw;
+                if (preg_match('/^(-?\d+)([A-Z]{2})$/', $byDayRaw, $m)) {
+                    $pos = (int) $m[1];
+                    $token = $m[2];
+                }
+                $weekday = $this->weekdayTokenToNum($token);
+                if ($weekday !== null) {
+                    $payload['repeat_nth_mode'] = 'weekday_of_month';
+                    $payload['repeat_nth_weekday'] = $weekday;
+                    if (isset($parts['BYSETPOS']) && preg_match('/^-?\d+$/', (string) $parts['BYSETPOS'])) {
+                        $pos = (int) $parts['BYSETPOS'];
+                    }
+                    $payload['repeat_nth_pos'] = $pos === null ? 1 : ($pos < 0 ? -1 : max(1, min(5, $pos)));
+                }
+            }
+        }
+
+        if (isset($parts['COUNT'])) {
+            $payload['repeat_range_mode'] = 'count';
+            $payload['repeat_count'] = max(1, (int) $parts['COUNT']);
+        } elseif (isset($parts['UNTIL'])) {
+            $until = $this->parseIcsDateTime($parts['UNTIL'], false);
+            if ($until !== null) {
+                $payload['repeat_range_mode'] = 'until';
+                $payload['repeat_until'] = substr($until, 0, 10);
+            }
+        }
+
+        return $payload;
+    }
+
+    private function extractVeventProperties(string $ics): ?array
+    {
+        $lines = preg_split('/\r\n|\n|\r/', $ics) ?: [];
+        $unfolded = [];
+        foreach ($lines as $line) {
+            if ($line === '') {
+                continue;
+            }
+            if (($line[0] ?? '') === ' ' && $unfolded) {
+                $unfolded[count($unfolded) - 1] .= substr($line, 1);
+                continue;
+            }
+            $unfolded[] = $line;
+        }
+
+        $in = false;
+        $props = [];
+        foreach ($unfolded as $line) {
+            $upper = strtoupper($line);
+            if ($upper === 'BEGIN:VEVENT') {
+                $in = true;
+                continue;
+            }
+            if ($upper === 'END:VEVENT') {
+                break;
+            }
+            if (!$in) {
+                continue;
+            }
+            [$left, $value] = array_pad(explode(':', $line, 2), 2, '');
+            if ($left === '') {
+                continue;
+            }
+            [$name, $params] = array_pad(explode(';', $left, 2), 2, '');
+            $name = strtoupper(trim($name));
+            if ($name === '') {
+                continue;
+            }
+            $props[$name][] = $this->unescapeText(trim($value));
+            if ($name === 'DTSTART') {
+                $props['_DTSTART_PARAMS'][] = strtoupper(trim($params));
+            }
+        }
+
+        return $in ? $props : null;
+    }
+
+    private function parseIcsDateTime(string $value, bool $dateOnly): ?string
+    {
+        $value = trim($value);
+        if ($value === '') {
+            return null;
+        }
+
+        try {
+            if ($dateOnly && preg_match('/^\d{8}$/', $value)) {
+                $dt = DateTimeImmutable::createFromFormat('Ymd H:i:s', $value . ' 00:00:00', new DateTimeZone('Europe/London'));
+                if ($dt instanceof DateTimeImmutable) {
+                    return $dt->format('Y-m-d\\T00:00:00P');
+                }
+            }
+
+            if (preg_match('/^\d{8}T\d{6}Z$/', $value)) {
+                $dt = DateTimeImmutable::createFromFormat('Ymd\\THis\\Z', $value, new DateTimeZone('UTC'));
+                if ($dt instanceof DateTimeImmutable) {
+                    return $dt->setTimezone(new DateTimeZone('Europe/London'))->format('c');
+                }
+            }
+
+            if (preg_match('/^\d{8}T\d{6}$/', $value)) {
+                $dt = DateTimeImmutable::createFromFormat('Ymd\\THis', $value, new DateTimeZone('Europe/London'));
+                if ($dt instanceof DateTimeImmutable) {
+                    return $dt->format('c');
+                }
+            }
+
+            $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London'));
+            return $dt->format('c');
+        } catch (\Throwable) {
+            return null;
+        }
+    }
+
+    private function toDateTime(string $value): ?DateTimeImmutable
+    {
+        if ($value === '') {
+            return null;
+        }
+        try {
+            return new DateTimeImmutable($value, new DateTimeZone('Europe/London'));
+        } catch (\Throwable) {
+            return null;
+        }
+    }
+
+    private function toUtcIcs(DateTimeImmutable $dt): string
+    {
+        return $dt->setTimezone(new DateTimeZone('UTC'))->format('Ymd\\THis\\Z');
+    }
+
+    private function escapeText(string $value): string
+    {
+        return str_replace(
+            ["\\", ";", ",", "\r\n", "\n", "\r"],
+            ["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"],
+            $value
+        );
+    }
+
+    private function unescapeText(string $value): string
+    {
+        return str_replace(
+            ["\\n", "\\N", "\\,", "\\;", "\\\\"],
+            ["\n", "\n", ",", ";", "\\"],
+            $value
+        );
+    }
+
+    private function weekdayNumToToken(int $weekday): ?string
+    {
+        return match ($weekday) {
+            0 => 'SU',
+            1 => 'MO',
+            2 => 'TU',
+            3 => 'WE',
+            4 => 'TH',
+            5 => 'FR',
+            6 => 'SA',
+            default => null,
+        };
+    }
+
+    private function weekdayTokenToNum(string $token): ?int
+    {
+        return match (strtoupper(trim($token))) {
+            'SU' => 0,
+            'MO' => 1,
+            'TU' => 2,
+            'WE' => 3,
+            'TH' => 4,
+            'FR' => 5,
+            'SA' => 6,
+            default => null,
+        };
+    }
+
+    private function foldLines(array $lines): array
+    {
+        $out = [];
+        foreach ($lines as $line) {
+            if ($line === '') {
+                $out[] = $line;
+                continue;
+            }
+            while (strlen($line) > 73) {
+                $out[] = substr($line, 0, 73);
+                $line = ' ' . substr($line, 73);
+            }
+            $out[] = $line;
+        }
+        return $out;
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Domain/RecurrenceExpander.php b/package/staging/calendar-plugin/src/Domain/RecurrenceExpander.php
new file mode 100644
index 0000000..71fe24e
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Domain/RecurrenceExpander.php
@@ -0,0 +1,193 @@
+diff($end);
+        $repeatType = (string) ($event['repeat_type'] ?? 'none');
+        $interval = max(1, (int) ($event['repeat_interval'] ?? 1));
+        $rangeMode = (string) ($event['repeat_range_mode'] ?? 'none');
+        $repeatCount = isset($event['repeat_count']) ? (int) $event['repeat_count'] : null;
+        $repeatUntil = null;
+        if ($rangeMode === 'until' && !empty($event['repeat_until'])) {
+            $repeatUntil = self::parseDateTime((string) $event['repeat_until'] . 'T23:59:59', $tz);
+        }
+
+        if ($repeatType === 'none') {
+            if (self::overlaps($start, $end, $windowStart, $windowEnd)) {
+                return [self::occurrence($event, $start, $end)];
+            }
+            return [];
+        }
+
+        $occurrences = [];
+        $current = $start;
+        $produced = 0;
+
+        for ($i = 0; $i < self::MAX_ITERATIONS; $i++) {
+            if ($rangeMode === 'count' && $repeatCount !== null && $produced >= $repeatCount) {
+                break;
+            }
+            if ($repeatUntil && $current > $repeatUntil) {
+                break;
+            }
+            $currentEnd = $current->add($duration);
+            if (self::overlaps($current, $currentEnd, $windowStart, $windowEnd)) {
+                $key = $current->format('c');
+                if (!isset($deletedMap[$key])) {
+                    $occurrences[] = self::occurrence($event, $current, $currentEnd);
+                }
+            }
+            if ($current > $windowEnd->modify('+400 days')) {
+                break;
+            }
+            $produced++;
+            $current = self::nextStart($current, $repeatType, $interval, $event);
+            if (!$current) {
+                break;
+            }
+        }
+
+        return $occurrences;
+    }
+
+    private static function nextStart(DateTimeImmutable $current, string $repeatType, int $interval, array $event): ?DateTimeImmutable
+    {
+        return match ($repeatType) {
+            'daily' => $current->add(new DateInterval('P' . $interval . 'D')),
+            'weekly', 'custom' => $current->add(new DateInterval('P' . $interval . 'W')),
+            'monthly' => self::nextMonthlyStart($current, $interval, $event),
+            'yearly' => $current->modify('+' . $interval . ' year') ?: null,
+            default => null,
+        };
+    }
+
+    private static function nextMonthlyStart(DateTimeImmutable $current, int $interval, array $event): ?DateTimeImmutable
+    {
+        $mode = (string) ($event['repeat_nth_mode'] ?? '');
+        $next = $current->modify('+' . $interval . ' month');
+        if (!$next) {
+            return null;
+        }
+        if ($mode === 'day_of_month' && isset($event['repeat_nth_day']) && $event['repeat_nth_day'] !== null) {
+            $day = max(1, (int) $event['repeat_nth_day']);
+            $daysInMonth = (int) $next->format('t');
+            return $next->setDate((int) $next->format('Y'), (int) $next->format('n'), min($day, $daysInMonth));
+        }
+        if ($mode === 'weekday_of_month' && isset($event['repeat_nth_pos'], $event['repeat_nth_weekday']) && $event['repeat_nth_pos'] !== null && $event['repeat_nth_weekday'] !== null) {
+            $year = (int) $current->format('Y');
+            $month = (int) $current->format('n');
+            for ($i = 0; $i < 120; $i++) {
+                [$year, $month] = self::addMonths($year, $month, max(1, $interval));
+                $day = self::nthWeekdayOfMonth($year, $month, (int) $event['repeat_nth_weekday'], (int) $event['repeat_nth_pos']);
+                if ($day !== null) {
+                    return $current->setDate($year, $month, $day);
+                }
+            }
+            return null;
+        }
+        return $next;
+    }
+
+    private static function nthWeekdayOfMonth(int $year, int $month, int $weekday, int $pos): ?int
+    {
+        $weekday = max(0, min(6, $weekday));
+        $tz = new DateTimeZone('Europe/London');
+        if ($pos === -1) {
+            $last = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz);
+            $last = $last->modify('last day of this month');
+            for ($day = (int) $last->format('j'); $day >= 1; $day--) {
+                $dt = $last->setDate($year, $month, $day);
+                if ((int) $dt->format('w') === $weekday) {
+                    return $day;
+                }
+            }
+            return null;
+        }
+        $first = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz);
+        $daysInMonth = (int) $first->format('t');
+        $seen = 0;
+        for ($day = 1; $day <= $daysInMonth; $day++) {
+            $dt = $first->setDate($year, $month, $day);
+            if ((int) $dt->format('w') !== $weekday) {
+                continue;
+            }
+            $seen++;
+            if ($seen === $pos) {
+                return $day;
+            }
+        }
+        return null;
+    }
+
+    private static function occurrence(array $event, DateTimeImmutable $start, DateTimeImmutable $end): array
+    {
+        return [
+            'event_id' => (int) ($event['id'] ?? 0),
+            'uid' => (string) ($event['uid'] ?? ''),
+            'title' => (string) ($event['title'] ?? ''),
+            'description' => (string) ($event['description'] ?? ''),
+            'location' => (string) ($event['location'] ?? ''),
+            'category' => (string) ($event['category'] ?? ''),
+            'all_day_event' => (bool) ($event['all_day_event'] ?? false),
+            'occurrence_start' => $start->format('c'),
+            'occurrence_end' => $end->format('c'),
+            'repeat_type' => (string) ($event['repeat_type'] ?? 'none'),
+        ];
+    }
+
+    private static function overlaps(DateTimeImmutable $start, DateTimeImmutable $end, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd): bool
+    {
+        return $start < $windowEnd && $end > $windowStart;
+    }
+
+    private static function parseDateTime(string $value, DateTimeZone $tz): ?DateTimeImmutable
+    {
+        if ($value === '') {
+            return null;
+        }
+        if (!str_contains($value, 'T') && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
+            return new DateTimeImmutable($value . 'T00:00:00', $tz);
+        }
+        try {
+            return new DateTimeImmutable($value, $tz);
+        } catch (\Throwable) {
+            return null;
+        }
+    }
+
+    private static function addMonths(int $year, int $month, int $delta): array
+    {
+        $index = ($year * 12) + ($month - 1) + $delta;
+        $newYear = (int) floor($index / 12);
+        $newMonth = ($index % 12) + 1;
+        if ($newMonth <= 0) {
+            $newMonth += 12;
+            $newYear -= 1;
+        }
+        return [$newYear, $newMonth];
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Domain/SettingsService.php b/package/staging/calendar-plugin/src/Domain/SettingsService.php
new file mode 100644
index 0000000..4c24285
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Domain/SettingsService.php
@@ -0,0 +1,91 @@
+ 'Public Calendar',
+        'url_slug' => '',
+        'verification_page_path' => '/calendar',
+        'ics_access_mode' => 'public_read',
+        'diagnostics_enabled' => '1',
+        'uninstall_cleanup_mode' => 'keep',
+    ];
+
+    private const OPTION_PREFIX = 'calendar_plugin_';
+
+    public function __construct(private readonly OptionsAdapterInterface $options)
+    {
+    }
+
+    public function getAll(): array
+    {
+        $out = [];
+        foreach (self::DEFAULTS as $key => $default) {
+            $out[$key] = $this->get($key, $default);
+        }
+        return $out;
+    }
+
+    public function get(string $key, mixed $default = null): mixed
+    {
+        $fallback = $default ?? (self::DEFAULTS[$key] ?? null);
+        return $this->options->get(self::OPTION_PREFIX . $key, $fallback);
+    }
+
+    public function update(array $payload): array
+    {
+        $allowed = array_keys(self::DEFAULTS);
+        $updated = $this->getAll();
+
+        foreach ($allowed as $key) {
+            if (!array_key_exists($key, $payload)) {
+                continue;
+            }
+            $value = $this->sanitize($key, $payload[$key]);
+            $this->options->set(self::OPTION_PREFIX . $key, $value);
+            $updated[$key] = $value;
+        }
+
+        return $updated;
+    }
+
+    private function sanitize(string $key, mixed $value): mixed
+    {
+        return match ($key) {
+            'caldav_calendar_name' => trim((string) $value) ?: self::DEFAULTS[$key],
+            'url_slug' => trim((string) $value, " \t\n\r\0\x0B/"),
+            'verification_page_path' => $this->normalizePath((string) $value),
+            'ics_access_mode' => in_array((string) $value, ['public_read', 'authenticated_read'], true)
+                ? (string) $value
+                : self::DEFAULTS['ics_access_mode'],
+            'diagnostics_enabled' => $this->isTruthy($value) ? '1' : '0',
+            'uninstall_cleanup_mode' => in_array((string) $value, ['keep', 'remove'], true) ? (string) $value : 'keep',
+            default => $value,
+        };
+    }
+
+    private function isTruthy(mixed $value): bool
+    {
+        return in_array(strtolower(trim((string) $value)), self::TRUE_VALUES, true);
+    }
+
+    private function normalizePath(string $value): string
+    {
+        $path = trim($value);
+        if ($path === '') {
+            return self::DEFAULTS['verification_page_path'];
+        }
+        if (!str_starts_with($path, '/')) {
+            $path = '/' . $path;
+        }
+        return '/' . trim($path, '/');
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Domain/UserService.php b/package/staging/calendar-plugin/src/Domain/UserService.php
new file mode 100644
index 0000000..2018195
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Domain/UserService.php
@@ -0,0 +1,473 @@
+getPrefix();
+        $stem = trim($tableStem, '_');
+        $this->usersTable = $prefix . $stem . '_users';
+        $this->tokensTable = $prefix . $stem . '_user_tokens';
+        $this->auditTable = $prefix . $stem . '_audit_log';
+    }
+
+    public function register(string $email, string $password): array
+    {
+        $email = $this->normalizeEmail($email);
+        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+            return $this->error('validation_error', 'email is required', 422);
+        }
+        if (strlen($password) < 8) {
+            return $this->error('validation_error', 'password must be at least 8 characters', 422);
+        }
+        if ($this->isRateLimited('register:' . $email, 10, 3600)) {
+            return $this->error('rate_limited', 'too many requests', 429);
+        }
+        if ($this->findUserByEmail($email)) {
+            return $this->error('conflict_error', 'account already exists', 409);
+        }
+
+        $now = gmdate('c');
+        $inserted = $this->db->insert(
+            $this->usersTable,
+            [
+                'email' => $email,
+                'password_hash' => password_hash($password, PASSWORD_DEFAULT),
+                'email_verified_at' => null,
+                'account_status' => 'pending_approval',
+                'created_at' => $now,
+                'updated_at' => $now,
+            ]
+        );
+        if ($inserted === false) {
+            return $this->error('internal_error', 'unable to create account', 500);
+        }
+
+        $userId = $this->db->insertId();
+        $token = $this->issueToken($userId, 'email_verify', 24 * 3600);
+        $this->audit('user.register', (string) $userId, 'success', ['email' => $email]);
+
+        return [
+            'ok' => true,
+            'user' => $this->publicUser((array) $this->getUserById($userId)),
+            'verify_token' => $token,
+            'message' => 'registration submitted',
+        ];
+    }
+
+    public function verifyEmail(string $token): array
+    {
+        if ($token === '') {
+            return $this->error('validation_error', 'token is required', 422);
+        }
+        $tok = $this->consumeToken($token, 'email_verify');
+        if (!$tok) {
+            return $this->error('validation_error', 'invalid or expired token', 422);
+        }
+        $user = $this->getUserById((int) $tok['user_id']);
+        if (!$user) {
+            return $this->error('not_found', 'user not found', 404);
+        }
+
+        $now = gmdate('c');
+        $this->db->update(
+            $this->usersTable,
+            ['email_verified_at' => $now, 'updated_at' => $now],
+            ['id' => (int) $user['id']]
+        );
+        $updated = $this->getUserById((int) $user['id']);
+        $this->audit('user.verify_email', (string) $user['id'], 'success', []);
+
+        return [
+            'ok' => true,
+            'user' => $updated ? $this->publicUser($updated) : $this->publicUser($user),
+            'message' => 'An admin will review your request and notify you if approved.',
+        ];
+    }
+
+    public function login(string $email, string $password): array
+    {
+        $email = $this->normalizeEmail($email);
+        if ($this->isRateLimited('login:' . $email, 20, 3600)) {
+            return $this->error('rate_limited', 'too many requests', 429);
+        }
+
+        $user = $this->findUserByEmail($email);
+        if (!$user || !password_verify($password, (string) ($user['password_hash'] ?? ''))) {
+            return $this->error('auth_required', 'Login failure', 401);
+        }
+        if (empty($user['email_verified_at'])) {
+            return $this->error('auth_required', 'Login failure', 401);
+        }
+        if ((string) ($user['account_status'] ?? '') !== 'active') {
+            return $this->error('auth_required', 'Login failure', 401);
+        }
+
+        $this->audit('user.login', (string) $user['id'], 'success', []);
+
+        return [
+            'ok' => true,
+            'user' => $this->publicUser($user),
+        ];
+    }
+
+    public function authenticateActiveUserCredentials(string $email, string $password): ?array
+    {
+        $email = $this->normalizeEmail($email);
+        if ($email === '' || $password === '') {
+            return null;
+        }
+        $user = $this->findUserByEmail($email);
+        if (!$user) {
+            return null;
+        }
+        if (!password_verify($password, (string) ($user['password_hash'] ?? ''))) {
+            return null;
+        }
+        if (empty($user['email_verified_at'])) {
+            return null;
+        }
+        if ((string) ($user['account_status'] ?? '') !== 'active') {
+            return null;
+        }
+        return $this->publicUser($user);
+    }
+
+    public function issueSessionToken(int $userId, int $ttlSeconds = 2592000): string
+    {
+        if ($userId <= 0) {
+            return '';
+        }
+        return $this->issueToken($userId, 'session', max(300, $ttlSeconds));
+    }
+
+    public function authenticateSessionToken(string $token): ?array
+    {
+        $row = $this->findValidToken($token, 'session');
+        if ($row === null) {
+            return null;
+        }
+
+        $user = $this->getUserById((int) ($row['user_id'] ?? 0));
+        if (!$user) {
+            return null;
+        }
+        if (empty($user['email_verified_at'])) {
+            return null;
+        }
+        if ((string) ($user['account_status'] ?? '') !== 'active') {
+            return null;
+        }
+
+        return $this->publicUser($user);
+    }
+
+    public function revokeSessionToken(string $token): void
+    {
+        $row = $this->findValidToken($token, 'session');
+        if ($row === null) {
+            return;
+        }
+        $this->db->update(
+            $this->tokensTable,
+            ['used_at' => gmdate('c')],
+            ['id' => (int) $row['id']]
+        );
+    }
+
+    public function requestPasswordReset(string $email): array
+    {
+        $email = $this->normalizeEmail($email);
+        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+            return $this->error('validation_error', 'email is required', 422);
+        }
+        if ($this->isRateLimited('reset:' . $email, 10, 3600)) {
+            return $this->error('rate_limited', 'too many requests', 429);
+        }
+
+        $user = $this->findUserByEmail($email);
+        if (!$user) {
+            return ['ok' => true, 'message' => 'if account exists, reset email will be sent'];
+        }
+
+        $token = $this->issueToken((int) $user['id'], 'password_reset', 30 * 60);
+        $this->audit('user.password_reset.request', (string) $user['id'], 'success', []);
+
+        return [
+            'ok' => true,
+            'reset_token' => $token,
+            'message' => 'password reset requested',
+        ];
+    }
+
+    public function resetPassword(string $token, string $newPassword): array
+    {
+        if (strlen($newPassword) < 8) {
+            return $this->error('validation_error', 'password must be at least 8 characters', 422);
+        }
+
+        $tok = $this->consumeToken($token, 'password_reset');
+        if (!$tok) {
+            return $this->error('validation_error', 'invalid or expired token', 422);
+        }
+
+        $user = $this->getUserById((int) $tok['user_id']);
+        if (!$user) {
+            return $this->error('not_found', 'user not found', 404);
+        }
+
+        $this->db->update(
+            $this->usersTable,
+            ['password_hash' => password_hash($newPassword, PASSWORD_DEFAULT), 'updated_at' => gmdate('c')],
+            ['id' => (int) $user['id']]
+        );
+        // Invalidate persistent web sessions after password reset.
+        $this->db->delete($this->tokensTable, ['user_id' => (int) $user['id'], 'token_type' => 'session']);
+        $this->audit('user.password_reset.complete', (string) $user['id'], 'success', []);
+
+        return ['ok' => true, 'message' => 'password updated'];
+    }
+
+    public function listUsers(): array
+    {
+        $rows = $this->db->getResults("SELECT * FROM {$this->usersTable} ORDER BY id ASC");
+        return array_map(fn(object $r): array => $this->publicUser((array) $r), $rows);
+    }
+
+    public function approveUser(int $id): ?array
+    {
+        $user = $this->getUserById($id);
+        if (!$user) {
+            return null;
+        }
+        if (empty($user['email_verified_at'])) {
+            return null;
+        }
+        $this->db->update(
+            $this->usersTable,
+            ['account_status' => 'active', 'updated_at' => gmdate('c')],
+            ['id' => $id]
+        );
+        $updated = $this->getUserById($id);
+        $this->audit('user.approve', (string) $id, 'success', []);
+        return $updated ? $this->publicUser($updated) : null;
+    }
+
+    public function removeUser(int $id): bool
+    {
+        $deleted = $this->db->delete($this->usersTable, ['id' => $id]);
+        $this->db->delete($this->tokensTable, ['user_id' => $id]);
+        if ($deleted !== false) {
+            $this->audit('user.remove', (string) $id, 'success', []);
+            return true;
+        }
+        return false;
+    }
+
+    private function findUserByEmail(string $email): ?array
+    {
+        foreach ($this->listRawUsers() as $user) {
+            if (strtolower((string) ($user['email'] ?? '')) === strtolower($email)) {
+                return $user;
+            }
+        }
+        return null;
+    }
+
+    private function getUserById(int $id): ?array
+    {
+        $sql = $this->db->prepare("SELECT * FROM {$this->usersTable} WHERE id = %d", $id);
+        $row = $this->db->getRow($sql);
+        return $row ? (array) $row : null;
+    }
+
+    private function listRawUsers(): array
+    {
+        $rows = $this->db->getResults("SELECT * FROM {$this->usersTable} ORDER BY id ASC");
+        return array_map(static fn(object $r): array => (array) $r, $rows);
+    }
+
+    private function issueToken(int $userId, string $type, int $ttlSeconds): string
+    {
+        $token = bin2hex(random_bytes(16));
+        $hash = hash('sha256', $token);
+        $expiresAt = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->modify('+' . $ttlSeconds . ' seconds')->format('c');
+
+        $this->db->insert(
+            $this->tokensTable,
+            [
+                'user_id' => $userId,
+                'token_type' => $type,
+                'token_hash' => $hash,
+                'expires_at' => $expiresAt,
+                'used_at' => null,
+                'created_at' => gmdate('c'),
+            ]
+        );
+
+        return $token;
+    }
+
+    private function consumeToken(string $token, string $type): ?array
+    {
+        $row = $this->findValidToken($token, $type);
+        if ($row === null) {
+            return null;
+        }
+        $this->db->update(
+            $this->tokensTable,
+            ['used_at' => gmdate('c')],
+            ['id' => (int) $row['id']]
+        );
+        return $row;
+    }
+
+    private function findValidToken(string $token, string $type): ?array
+    {
+        $raw = trim($token);
+        if ($raw === '') {
+            return null;
+        }
+        $hash = hash('sha256', $raw);
+        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+
+        $rows = $this->db->getResults("SELECT * FROM {$this->tokensTable} ORDER BY id ASC");
+        foreach ($rows as $rowObj) {
+            $row = (array) $rowObj;
+            if ((string) ($row['token_type'] ?? '') !== $type) {
+                continue;
+            }
+            if ((string) ($row['token_hash'] ?? '') !== $hash) {
+                continue;
+            }
+            if (!empty($row['used_at'])) {
+                return null;
+            }
+            try {
+                $expires = new DateTimeImmutable((string) $row['expires_at'], new DateTimeZone('UTC'));
+            } catch (\Throwable) {
+                return null;
+            }
+            if ($expires < $now) {
+                return null;
+            }
+            return $row;
+        }
+        return null;
+    }
+
+    private function publicUser(array $user): array
+    {
+        return [
+            'id' => (int) ($user['id'] ?? 0),
+            'email' => (string) ($user['email'] ?? ''),
+            'email_verified_at' => $user['email_verified_at'] ?? null,
+            'account_status' => (string) ($user['account_status'] ?? 'pending_approval'),
+            'created_at' => (string) ($user['created_at'] ?? ''),
+            'updated_at' => (string) ($user['updated_at'] ?? ''),
+        ];
+    }
+
+    private function error(string $code, string $message, int $status): array
+    {
+        return ['error' => ['code' => $code, 'message' => $message, 'status' => $status]];
+    }
+
+    private function normalizeEmail(string $email): string
+    {
+        return strtolower(trim($email));
+    }
+
+    private function audit(string $action, string $target, string $result, array $context): void
+    {
+        if (!$this->isDiagnosticsEnabled()) {
+            return;
+        }
+        $this->db->insert(
+            $this->auditTable,
+            [
+                'actor' => 'plugin',
+                'action' => $action,
+                'target' => $target,
+                'result' => $result,
+                'created_at' => gmdate('c'),
+                'context_json' => json_encode($context),
+            ]
+        );
+    }
+
+    private function isDiagnosticsEnabled(): bool
+    {
+        $optionsTable = $this->db->getPrefix() . 'options';
+        $sql = $this->db->prepare(
+            "SELECT option_value FROM {$optionsTable} WHERE option_name = %s LIMIT 1",
+            'calendar_plugin_diagnostics_enabled'
+        );
+        $row = $this->db->getRow($sql);
+        if (!$row || !property_exists($row, 'option_value')) {
+            return true;
+        }
+        return in_array(strtolower(trim((string) $row->option_value)), ['1', 'true', 'yes', 'on'], true);
+    }
+
+    private function isRateLimited(string $bucket, int $limit, int $windowSeconds): bool
+    {
+        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+        $type = 'rate:' . substr(hash('sha256', $bucket), 0, 32);
+        $sql = $this->db->prepare("SELECT * FROM {$this->tokensTable} WHERE token_type = %s ORDER BY id ASC", $type);
+        $rows = $this->db->getResults($sql);
+
+        $activeCount = 0;
+        foreach ($rows as $rowObj) {
+            $row = (array) $rowObj;
+            $id = (int) ($row['id'] ?? 0);
+            $usedAt = (string) ($row['used_at'] ?? '');
+            $expiresAtRaw = (string) ($row['expires_at'] ?? '');
+            $expired = true;
+            try {
+                $expiresAt = new DateTimeImmutable($expiresAtRaw, new DateTimeZone('UTC'));
+                $expired = $expiresAt < $now;
+            } catch (\Throwable) {
+                $expired = true;
+            }
+
+            if ($id > 0 && ($usedAt !== '' || $expired)) {
+                $this->db->delete($this->tokensTable, ['id' => $id]);
+                continue;
+            }
+
+            if (!$expired && $usedAt === '') {
+                $activeCount++;
+            }
+        }
+
+        if ($activeCount >= $limit) {
+            return true;
+        }
+
+        $this->db->insert(
+            $this->tokensTable,
+            [
+                'user_id' => 0,
+                'token_type' => $type,
+                'token_hash' => hash('sha256', bin2hex(random_bytes(16))),
+                'expires_at' => $now->modify('+' . max(1, $windowSeconds) . ' seconds')->format('c'),
+                'used_at' => null,
+                'created_at' => gmdate('c'),
+            ]
+        );
+        return false;
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Infrastructure/ServiceContainer.php b/package/staging/calendar-plugin/src/Infrastructure/ServiceContainer.php
new file mode 100644
index 0000000..874b495
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Infrastructure/ServiceContainer.php
@@ -0,0 +1,25 @@
+ */
+    private array $services = [];
+
+    public function set(string $id, object $service): void
+    {
+        $this->services[$id] = $service;
+    }
+
+    public function get(string $id): object
+    {
+        if (!isset($this->services[$id])) {
+            throw new \RuntimeException(sprintf('Service not found: %s', $id));
+        }
+
+        return $this->services[$id];
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Infrastructure/WordPress/MigrationManager.php b/package/staging/calendar-plugin/src/Infrastructure/WordPress/MigrationManager.php
new file mode 100644
index 0000000..cb2281c
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Infrastructure/WordPress/MigrationManager.php
@@ -0,0 +1,222 @@
+get_charset_collate();
+        $prefix = $this->db->getPrefix();
+        $stem = trim($this->tableStem, '_');
+
+        $events = $prefix . $stem . '_events';
+        $exceptions = $prefix . $stem . '_recurrence_exceptions';
+        $users = $prefix . $stem . '_users';
+        $tokens = $prefix . $stem . '_user_tokens';
+        $audit = $prefix . $stem . '_audit_log';
+
+        $sqlEvents = "CREATE TABLE {$events} (
+            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+            uid VARCHAR(191) NOT NULL,
+            title TEXT NOT NULL,
+            description LONGTEXT NOT NULL,
+            location TEXT NOT NULL,
+            category TEXT NOT NULL,
+            all_day_event TINYINT(1) NOT NULL DEFAULT 0,
+            start_datetime VARCHAR(64) NOT NULL,
+            end_datetime VARCHAR(64) NOT NULL,
+            repeat_type VARCHAR(24) NOT NULL DEFAULT 'none',
+            repeat_interval INT NOT NULL DEFAULT 1,
+            repeat_nth_mode VARCHAR(32) NOT NULL DEFAULT '',
+            repeat_nth_day INT NULL,
+            repeat_nth_pos INT NULL,
+            repeat_nth_weekday INT NULL,
+            repeat_range_mode VARCHAR(24) NOT NULL DEFAULT 'none',
+            repeat_count INT NULL,
+            repeat_until VARCHAR(16) NULL,
+            timezone VARCHAR(64) NOT NULL DEFAULT 'Europe/London',
+            caldav_resource VARCHAR(191) NULL,
+            etag VARCHAR(64) NULL,
+            sync_version INT NOT NULL DEFAULT 1,
+            last_modified_by_user_id BIGINT UNSIGNED NULL,
+            created_at VARCHAR(32) NOT NULL,
+            updated_at VARCHAR(32) NOT NULL,
+            PRIMARY KEY (id),
+            UNIQUE KEY uid (uid(191)),
+            UNIQUE KEY caldav_resource (caldav_resource),
+            KEY start_datetime (start_datetime(32)),
+            KEY end_datetime (end_datetime(32))
+        ) {$charsetCollate};";
+
+        $sqlExceptions = "CREATE TABLE {$exceptions} (
+            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+            event_id BIGINT UNSIGNED NOT NULL,
+            occurrence_key VARCHAR(64) NOT NULL,
+            exception_type VARCHAR(32) NOT NULL,
+            created_at VARCHAR(32) NOT NULL,
+            updated_at VARCHAR(32) NOT NULL,
+            PRIMARY KEY (id),
+            UNIQUE KEY event_occurrence (event_id, occurrence_key),
+            KEY event_id (event_id)
+        ) {$charsetCollate};";
+
+        $sqlUsers = "CREATE TABLE {$users} (
+            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+            email VARCHAR(191) NOT NULL,
+            password_hash VARCHAR(255) NOT NULL,
+            email_verified_at VARCHAR(32) NULL,
+            account_status VARCHAR(32) NOT NULL DEFAULT 'pending_approval',
+            created_at VARCHAR(32) NOT NULL,
+            updated_at VARCHAR(32) NOT NULL,
+            PRIMARY KEY (id),
+            UNIQUE KEY email (email)
+        ) {$charsetCollate};";
+
+        $sqlTokens = "CREATE TABLE {$tokens} (
+            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+            user_id BIGINT UNSIGNED NOT NULL,
+            token_type VARCHAR(32) NOT NULL,
+            token_hash VARCHAR(255) NOT NULL,
+            expires_at VARCHAR(32) NOT NULL,
+            used_at VARCHAR(32) NULL,
+            created_at VARCHAR(32) NOT NULL,
+            PRIMARY KEY (id),
+            KEY user_id (user_id),
+            KEY token_type (token_type)
+        ) {$charsetCollate};";
+
+        $sqlAudit = "CREATE TABLE {$audit} (
+            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+            actor VARCHAR(191) NOT NULL,
+            action VARCHAR(191) NOT NULL,
+            target VARCHAR(191) NOT NULL,
+            result VARCHAR(32) NOT NULL,
+            created_at VARCHAR(32) NOT NULL,
+            context_json LONGTEXT NULL,
+            PRIMARY KEY (id),
+            KEY action (action),
+            KEY created_at (created_at)
+        ) {$charsetCollate};";
+
+        dbDelta($sqlEvents);
+        dbDelta($sqlExceptions);
+        dbDelta($sqlUsers);
+        dbDelta($sqlTokens);
+        dbDelta($sqlAudit);
+
+        // Ensure every event has a stable CalDAV object resource name.
+        $this->db->query(
+            "UPDATE {$events}
+             SET caldav_resource = CONCAT(uid, '.ics')
+             WHERE (caldav_resource IS NULL OR caldav_resource = '')
+               AND uid IS NOT NULL
+               AND uid <> ''"
+        );
+        $this->normalizeEventDateTimesToLondon($events);
+
+        update_option(self::STEM_OPTION, $stem);
+        update_option('calendar_plugin_schema_version', self::SCHEMA_VERSION);
+    }
+
+    public function assertActivationSafe(): void
+    {
+        $stem = trim($this->tableStem, '_');
+        $ownedStem = trim((string) get_option(self::STEM_OPTION, ''), '_');
+        if ($ownedStem !== '' && $ownedStem !== $stem) {
+            throw new \RuntimeException(
+                sprintf(
+                    'Calendar Plugin is already initialized with table stem "%s". Requested stem "%s" is different.',
+                    $ownedStem,
+                    $stem
+                )
+            );
+        }
+        if ($ownedStem !== $stem && $this->anyTargetTablesExist($stem)) {
+            $legacySchemaVersion = trim((string) get_option('calendar_plugin_schema_version', ''));
+            if ($legacySchemaVersion === '') {
+                throw new \RuntimeException(
+                    sprintf(
+                        'Calendar Plugin activation blocked: target tables for stem "%s" already exist. Choose another table stem via CALENDAR_PLUGIN_TABLE_STEM.',
+                        $stem
+                    )
+                );
+            }
+        }
+    }
+
+    private function anyTargetTablesExist(string $stem): bool
+    {
+        $prefix = $this->db->getPrefix();
+        $tables = [
+            $prefix . $stem . '_events',
+            $prefix . $stem . '_recurrence_exceptions',
+            $prefix . $stem . '_users',
+            $prefix . $stem . '_user_tokens',
+            $prefix . $stem . '_audit_log',
+        ];
+        foreach ($tables as $table) {
+            $sql = $this->db->prepare('SHOW TABLES LIKE %s', $table);
+            if (count($this->db->getResults($sql)) > 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private function normalizeEventDateTimesToLondon(string $eventsTable): void
+    {
+        $rows = $this->db->getResults(
+            "SELECT id, start_datetime, end_datetime FROM {$eventsTable}"
+        );
+        $tz = new DateTimeZone('Europe/London');
+        foreach ($rows as $row) {
+            $id = (int) ($row->id ?? 0);
+            if ($id <= 0) {
+                continue;
+            }
+            $start = $this->normalizeDateTimeString((string) ($row->start_datetime ?? ''), $tz);
+            $end = $this->normalizeDateTimeString((string) ($row->end_datetime ?? ''), $tz);
+            if ($start === null || $end === null) {
+                continue;
+            }
+            $sql = $this->db->prepare(
+                "UPDATE {$eventsTable} SET start_datetime = %s, end_datetime = %s WHERE id = %d",
+                $start,
+                $end,
+                $id
+            );
+            $this->db->query($sql);
+        }
+    }
+
+    private function normalizeDateTimeString(string $value, DateTimeZone $tz): ?string
+    {
+        $value = trim($value);
+        if ($value === '') {
+            return null;
+        }
+        try {
+            $dt = new DateTimeImmutable($value, $tz);
+            return $dt->setTimezone($tz)->format('c');
+        } catch (\Throwable) {
+            return null;
+        }
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressAuthAdapter.php b/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressAuthAdapter.php
new file mode 100644
index 0000000..b1f988f
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressAuthAdapter.php
@@ -0,0 +1,31 @@
+user_email ?? '') : '';
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php b/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
new file mode 100644
index 0000000..4fdc04e
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
@@ -0,0 +1,60 @@
+wpdb->prefix;
+    }
+
+    public function prepare(string $query, mixed ...$args): string
+    {
+        return (string) $this->wpdb->prepare($query, ...$args);
+    }
+
+    public function query(string $query): int|false
+    {
+        return $this->wpdb->query($query);
+    }
+
+    public function getResults(string $query): array
+    {
+        return $this->wpdb->get_results($query) ?: [];
+    }
+
+    public function getRow(string $query): ?object
+    {
+        $row = $this->wpdb->get_row($query);
+        return is_object($row) ? $row : null;
+    }
+
+    public function insert(string $table, array $data, array $formats = []): int|false
+    {
+        return $this->wpdb->insert($table, $data, $formats);
+    }
+
+    public function update(string $table, array $data, array $where, array $formats = [], array $whereFormats = []): int|false
+    {
+        return $this->wpdb->update($table, $data, $where, $formats, $whereFormats);
+    }
+
+    public function delete(string $table, array $where, array $whereFormats = []): int|false
+    {
+        return $this->wpdb->delete($table, $where, $whereFormats);
+    }
+
+    public function insertId(): int
+    {
+        return (int) $this->wpdb->insert_id;
+    }
+}
diff --git a/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressHttpAdapter.php b/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressHttpAdapter.php
new file mode 100644
index 0000000..1157fd7
--- /dev/null
+++ b/package/staging/calendar-plugin/src/Infrastructure/WordPress/WordPressHttpAdapter.php
@@ -0,0 +1,25 @@
+tableStem = self::resolveTableStem();
+        $this->settingsService = new SettingsService($this->options);
+        $this->eventService = new EventService($this->db, $this->tableStem);
+        $this->icsService = new IcsService();
+        $this->calDavService = new CalDavService($this->eventService, $this->icsService);
+        $this->userService = new UserService($this->db, $this->tableStem);
+    }
+
+    public static function boot(string $pluginFile): void
+    {
+        global $wpdb;
+
+        $db = new WordPressDatabaseAdapter($wpdb);
+        $options = new WordPressOptionsAdapter();
+        $auth = new WordPressAuthAdapter();
+        $http = new WordPressHttpAdapter();
+
+        register_activation_hook($pluginFile, [self::class, 'activate']);
+        register_deactivation_hook($pluginFile, [self::class, 'deactivate']);
+
+        $plugin = new self($db, $options, $auth, $http);
+        $plugin->register();
+    }
+
+    public static function activate(): void
+    {
+        try {
+            global $wpdb;
+            $db = new WordPressDatabaseAdapter($wpdb);
+            $tableStem = self::resolveTableStem();
+            $migration = new MigrationManager($db, $tableStem);
+            $migration->assertActivationSafe();
+            $migration->migrate();
+        } catch (\Throwable $e) {
+            if (function_exists('wp_die')) {
+                wp_die(
+                    esc_html($e->getMessage()),
+                    'Calendar Plugin Activation Blocked',
+                    ['response' => 500, 'back_link' => true]
+                );
+            }
+            throw $e;
+        }
+    }
+
+    public static function deactivate(): void
+    {
+        // Intentionally no destructive behavior on deactivate.
+    }
+
+    public function register(): void
+    {
+        $this->http->addAction('init', [$this, 'ensureSchemaCurrent']);
+        $this->http->addAction('init', [$this, 'onInit']);
+        $this->http->addAction('rest_api_init', [$this, 'registerRoutes']);
+        $this->http->addAction('admin_menu', [$this, 'registerAdminMenu']);
+        $this->http->addAction('admin_post_calendar_plugin_download_diagnostics', [$this, 'handleDiagnosticsDownload']);
+        $this->http->addAction('template_redirect', [$this, 'maybeServeSpecialEndpoints']);
+    }
+
+    public function onInit(): void
+    {
+        $this->http->addShortcode('calendar', [$this, 'renderCalendarShortcode']);
+        $this->http->addShortcode('calendar_sidebar_upcoming', [$this, 'renderSidebarShortcode']);
+    }
+
+    public function ensureSchemaCurrent(): void
+    {
+        $version = trim((string) get_option('calendar_plugin_schema_version', ''));
+        if ($version === '3') {
+            return;
+        }
+        try {
+            self::activate();
+        } catch (\Throwable) {
+            // Avoid hard-failing page loads; diagnostics/setup will still surface issues.
+        }
+    }
+
+    public function registerRoutes(): void
+    {
+        $this->registerHealthRoute();
+        $this->registerSettingsRoutes();
+        $this->registerEventRoutes();
+        $this->registerPublicRoutes();
+        $this->registerUserRoutes();
+        $this->registerIcsRoutes();
+        $this->registerCalDavRoutes();
+    }
+
+    public function renderCalendarShortcode(): string
+    {
+        $caldavRoot = $this->caldavRootPath();
+        $icsUrl = $this->icsPath();
+        $ns = self::API_NAMESPACE;
+        $caldavRootEsc = esc_html($caldavRoot);
+        $icsUrlEsc = esc_html($icsUrl);
+        $nsEsc = esc_html($ns);
+        $todayEsc = esc_html(gmdate('Y-m-d'));
+
+        return strtr(<<<'HTML'
+
+
+
+ Not logged in + + + CalDAV + ICS +
+
+ +
+ + + + + + + + +
+ + +
+
+ +

+

Events

+
+
    + + + + + + + + +
    +HTML + , [ + '__CALDAV_ROOT__' => $caldavRootEsc, + '__ICS_URL__' => $icsUrlEsc, + '__TODAY__' => $todayEsc, + '__NS__' => $nsEsc, + ]); + } + + public function renderSidebarShortcode(): string + { + $items = $this->eventService->listSidebarUpcoming(14); + if (!$items) { + return '

    No upcoming events.

    '; + } + + $rows = []; + foreach ($items as $item) { + $start = (string) ($item['occurrence_start'] ?? ''); + $end = (string) ($item['occurrence_end'] ?? ''); + $startTs = strtotime($start); + $endTs = strtotime($end); + $dateLabel = $startTs !== false ? date('j F Y', $startTs) : substr($start, 0, 10); + $timeLabel = ''; + if ($startTs !== false && $endTs !== false) { + $startTime = date('H:i', $startTs); + $endTime = date('H:i', $endTs); + if ($startTime !== '00:00' || $endTime !== '00:00') { + $timeLabel = $this->formatSidebarTimeRange($startTs, $endTs); + } + } + $title = trim((string) ($item['title'] ?? '')); + $description = trim((string) ($item['description'] ?? '')); + $headline = $dateLabel; + if ($timeLabel !== '') { + $headline .= ', ' . $timeLabel; + } + $headline .= ', ' . ($title !== '' ? $title : $description); + $rows[] = sprintf( + '

    %s%s

    ', + esc_html($headline), + $description !== '' ? '
    ' . esc_html($description) : '' + ); + } + + return '
    ' . implode('', $rows) . '
    '; + } + + private function formatSidebarTimeRange(int $startTs, int $endTs): string + { + $startMeridiem = strtolower(date('a', $startTs)); + $endMeridiem = strtolower(date('a', $endTs)); + $startLabel = $this->formatSidebarTimeValue($startTs); + $endLabel = $this->formatSidebarTimeValue($endTs); + if ($startMeridiem === $endMeridiem) { + $startLabel = preg_replace('/(am|pm)$/', '', $startLabel) ?: $startLabel; + return $startLabel . '–' . $endLabel; + } + return $startLabel . '–' . $endLabel; + } + + private function formatSidebarTimeValue(int $ts): string + { + $hour = (int) date('G', $ts); + $minute = (int) date('i', $ts); + $meridiem = strtolower(date('a', $ts)); + $hour12 = $hour % 12; + if ($hour12 === 0) { + $hour12 = 12; + } + if ($minute === 0) { + return $hour12 . $meridiem; + } + return $hour12 . '.' . str_pad((string) $minute, 2, '0', STR_PAD_LEFT) . $meridiem; + } + + public function registerAdminMenu(): void + { + if (!function_exists('add_menu_page')) { + return; + } + + add_menu_page( + 'Calendar Plugin', + 'Calendar Plugin', + 'manage_options', + 'calendar-plugin', + [$this, 'renderAdminSetupPage'] + ); + + add_submenu_page( + 'calendar-plugin', + 'Users', + 'Users', + 'manage_options', + 'calendar-plugin-users', + [$this, 'renderAdminUsersPage'] + ); + + add_submenu_page( + 'calendar-plugin', + 'Setup', + 'Setup', + 'manage_options', + 'calendar-plugin-setup', + [$this, 'renderAdminSetupPage'] + ); + + add_submenu_page( + 'calendar-plugin', + 'Diagnostics', + 'Diagnostics', + 'manage_options', + 'calendar-plugin-diagnostics', + [$this, 'renderAdminDiagnosticsPage'] + ); + + if (function_exists('remove_submenu_page')) { + remove_submenu_page('calendar-plugin', 'calendar-plugin'); + } + } + + public function renderAdminSetupPage(): void + { + $settings = $this->settingsService->getAll(); + $currentStem = trim((string) get_option('calendar_plugin_table_stem', $this->tableStem), '_'); + if ($currentStem === '') { + $currentStem = $this->tableStem; + } + $dbPrefix = (string) $this->db->getPrefix(); + $currentTablePrefix = $dbPrefix . $currentStem; + $message = ''; + $error = ''; + + if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { + $nonce = (string) ($_POST['_calendar_setup_nonce'] ?? ''); + $nonceSupported = function_exists('wp_nonce_field') && function_exists('wp_verify_nonce'); + if ($nonceSupported && ($nonce === '' || !$this->auth->verifyNonce($nonce, 'calendar_setup_update'))) { + $error = 'Invalid setup form submission.'; + } else { + $action = (string) ($_POST['setup_action'] ?? 'save_settings'); + if ($action === 'delete_all_events') { + $deleted = $this->eventService->deleteAllEventsData(); + $message = 'Deleted ' . $deleted . ' events and all recurrence exceptions.'; + } elseif ($action === 'seed_events') { + try { + $seeded = $this->eventService->seedDefaultEvents(); + $message = 'Seeded ' . $seeded . ' default events.'; + } catch (\Throwable $e) { + $error = 'Seed failed: ' . $e->getMessage(); + } + } else { + $payload = [ + 'caldav_calendar_name' => (string) ($_POST['caldav_calendar_name'] ?? ''), + 'url_slug' => (string) ($_POST['url_slug'] ?? ''), + 'verification_page_path' => (string) ($_POST['verification_page_path'] ?? ''), + 'ics_access_mode' => (string) ($_POST['ics_access_mode'] ?? ''), + 'diagnostics_enabled' => isset($_POST['diagnostics_enabled']) ? '1' : '0', + 'uninstall_cleanup_mode' => (string) ($_POST['uninstall_cleanup_mode'] ?? 'keep'), + ]; + $settings = $this->settingsService->update($payload); + $requestedPrefixRaw = strtolower(trim((string) ($_POST['table_prefix'] ?? $currentTablePrefix))); + $requestedPrefix = preg_replace('/[^a-z0-9_]/', '', $requestedPrefixRaw) ?: $currentTablePrefix; + $requestedPrefix = trim($requestedPrefix, '_'); + if ($requestedPrefix === '') { + $requestedPrefix = $currentTablePrefix; + } + if ($requestedPrefix !== $currentTablePrefix) { + $prefixResult = $this->updatePluginTablePrefix($currentStem, $requestedPrefix); + if ($prefixResult['ok']) { + $currentStem = (string) $prefixResult['stem']; + $currentTablePrefix = $dbPrefix . $currentStem; + $message = 'Settings saved. Table prefix updated to ' . $currentTablePrefix . '.'; + } else { + $error = (string) $prefixResult['message']; + } + } else { + $message = 'Settings saved.'; + } + } + } + } + + $nonceField = ''; + if (function_exists('wp_nonce_field')) { + ob_start(); + wp_nonce_field('calendar_setup_update', '_calendar_setup_nonce'); + $nonceField = (string) ob_get_clean(); + } else { + $nonceField = ''; + } + + $saveNote = ''; + if ($error !== '') { + $saveNote .= '

    ' . esc_html($error) . '

    '; + } + if ($message !== '') { + $saveNote .= '

    ' . esc_html($message) . '

    '; + } + + $icsMode = (string) ($settings['ics_access_mode'] ?? 'public_read'); + $icsModePublicSel = $icsMode === 'public_read' ? 'selected' : ''; + $icsModeAuthSel = $icsMode === 'authenticated_read' ? 'selected' : ''; + $diagnosticsChecked = ((string) ($settings['diagnostics_enabled'] ?? '1')) === '1' ? 'checked' : ''; + $cleanupMode = (string) ($settings['uninstall_cleanup_mode'] ?? 'keep'); + $cleanupKeepSel = $cleanupMode === 'keep' ? 'selected' : ''; + $cleanupRemoveSel = $cleanupMode === 'remove' ? 'selected' : ''; + + $form = '
    ' + . $nonceField + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '

    ' + . ' ' + . '

    ' + . '
    '; + + echo $this->adminPageShell( + 'Setup', + $saveNote + . '

    Configure presentation and endpoint behavior below or via API: /wp-json/' . self::API_NAMESPACE . '/settings.

    ' + . $form + . '
      ' + . '
    • Calendar shortcode: [calendar]
    • ' + . '
    • Sidebar shortcode: [calendar_sidebar_upcoming] (next 14 days list)
    • ' + . '
    • ICS endpoint: ' . esc_html($this->icsPath()) . '
    • ' + . '
    • CalDAV root: ' . esc_html($this->caldavRootPath()) . '
    • ' + . '
    ' + ); + } + + public function renderAdminUsersPage(): void + { + $message = ''; + $error = ''; + + if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { + $nonce = (string) ($_POST['_calendar_users_nonce'] ?? ''); + $nonceSupported = function_exists('wp_nonce_field') && function_exists('wp_verify_nonce'); + if ($nonceSupported && ($nonce === '' || !$this->auth->verifyNonce($nonce, 'calendar_users_action'))) { + $error = 'Invalid users action submission.'; + } else { + $action = (string) ($_POST['users_action'] ?? ''); + $userId = (int) ($_POST['user_id'] ?? 0); + if ($userId <= 0) { + $error = 'Invalid user selection.'; + } elseif ($action === 'approve') { + $user = $this->userService->approveUser($userId); + if ($user === null) { + $error = 'User not found.'; + } else { + $message = 'User approved.'; + } + } elseif ($action === 'remove') { + $ok = $this->userService->removeUser($userId); + if (!$ok) { + $error = 'User not found.'; + } else { + $message = 'User removed.'; + } + } else { + $error = 'Unsupported users action.'; + } + } + } + + $nonceField = ''; + if (function_exists('wp_nonce_field')) { + ob_start(); + wp_nonce_field('calendar_users_action', '_calendar_users_nonce'); + $nonceField = (string) ob_get_clean(); + } else { + $nonceField = ''; + } + + $users = $this->userService->listUsers(); + $rows = ''; + foreach ($users as $u) { + $userId = (int) ($u['id'] ?? 0); + $status = (string) ($u['account_status'] ?? ''); + $isApproved = $status === 'active'; + $isVerified = !empty((string) ($u['email_verified_at'] ?? '')); + $approveButton = $isApproved + ? '' + : ($isVerified + ? '' + : ''); + $removeButton = ''; + $actions = '
    ' + . $nonceField + . '' + . $approveButton + . $removeButton + . '
    '; + $rows .= '' + . '' . $userId . '' + . '' . esc_html((string) $u['email']) . '' + . '' . esc_html($status) . '' + . '' . esc_html((string) ($u['email_verified_at'] ?? '')) . '' + . '' . $actions . '' + . ''; + } + $feedback = ''; + if ($error !== '') { + $feedback .= '

    ' . esc_html($error) . '

    '; + } + if ($message !== '') { + $feedback .= '

    ' . esc_html($message) . '

    '; + } + + $body = $feedback + . '

    Approve pending users or remove defunct users.

    ' + . '' + . ($rows !== '' ? $rows : '') + . '
    IDEmailStatusEmail VerifiedActions
    No users
    '; + echo $this->adminPageShell('Users', $body); + } + + public function renderAdminDiagnosticsPage(): void + { + $enabled = $this->isDiagnosticsEnabled(); + $diag = $this->collectDiagnosticsSnapshot($enabled); + $downloadUrl = admin_url('admin-post.php?action=calendar_plugin_download_diagnostics&_wpnonce=' . wp_create_nonce('calendar_diagnostics_download')); + $downloadButton = $enabled + ? '

    Download Diagnostics

    ' + : '

    Enable diagnostics in Setup to collect and download diagnostics.

    '; + echo $this->adminPageShell( + 'Diagnostics', + $downloadButton + . '

    Runtime diagnostics snapshot:

    ' . esc_html(json_encode($diag, JSON_PRETTY_PRINT)) . '
    ' + ); + } + + public function handleDiagnosticsDownload(): void + { + if (!$this->auth->currentUserCan('manage_options')) { + wp_die('Not authorized.', 'Forbidden', ['response' => 403]); + } + $nonce = (string) ($_GET['_wpnonce'] ?? ''); + if (!$this->auth->verifyNonce($nonce, 'calendar_diagnostics_download')) { + wp_die('Invalid diagnostics download request.'); + } + if (!$this->isDiagnosticsEnabled()) { + wp_die('Diagnostics are disabled in Setup.'); + } + + $diag = $this->collectDiagnosticsSnapshot(true); + nocache_headers(); + header('Content-Type: application/json; charset=utf-8'); + header('Content-Disposition: attachment; filename="calendar-diagnostics-' . gmdate('Ymd-His') . '.json"'); + header('X-Content-Type-Options: nosniff'); + echo json_encode($diag, JSON_PRETTY_PRINT); + exit; + } + + private function isDiagnosticsEnabled(): bool + { + return ((string) ($this->settingsService->get('diagnostics_enabled', '1') ?? '1')) === '1'; + } + + public function maybeServeSpecialEndpoints(): void + { + $uri = (string) ($_SERVER['REQUEST_URI'] ?? ''); + $path = parse_url($uri, PHP_URL_PATH); + if (!is_string($path)) { + return; + } + + if ($path === $this->icsPath()) { + $this->serveIcsResponse(); + exit; + } + + if ($path === '/.well-known/caldav') { + wp_redirect($this->caldavRootPath(), 301); + exit; + } + + if ($path === rtrim($this->caldavRootPath(), '/') . '/') { + $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); + if ($method === 'OPTIONS') { + header('Allow: OPTIONS, PROPFIND, REPORT'); + header('DAV: 1, calendar-access'); + http_response_code(200); + exit; + } + } + + if (str_starts_with($path, rtrim($this->caldavRootPath(), '/'))) { + $this->serveCalDavPath($path, strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'))); + exit; + } + } + + private function registerHealthRoute(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/health', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + return [ + 'status' => 'ok', + 'plugin' => 'calendar-plugin', + 'version' => '0.1.15', + 'db_prefix' => $this->db->getPrefix(), + ]; + }, + ] + ); + } + + private function registerSettingsRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/settings', + [ + 'methods' => 'GET', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => fn(): array => ['data' => $this->settingsService->getAll()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/settings', + [ + 'methods' => 'PATCH', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + return ['data' => $this->settingsService->update($payload)]; + }, + ] + ); + } + + private function registerEventRoutes(): void + { + $canWrite = fn($request = null): bool => $this->canWriteCalendar($request); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events', + [ + 'methods' => 'GET', + 'permission_callback' => $canWrite, + 'callback' => fn(): array => ['data' => $this->eventService->listEvents()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events', + [ + 'methods' => 'POST', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + try { + $deletedKeys = isset($payload['deleted_occurrence_keys']) && is_array($payload['deleted_occurrence_keys']) + ? array_values($payload['deleted_occurrence_keys']) + : []; + unset($payload['deleted_occurrence_keys']); + $created = $this->eventService->createEvent($payload); + if ($deletedKeys !== []) { + $this->eventService->syncDeletedOccurrenceKeys((int) ($created['id'] ?? 0), $deletedKeys, true); + $created = $this->eventService->getEvent((int) ($created['id'] ?? 0)) ?: $created; + } + return ['data' => $created]; + } catch (\InvalidArgumentException $e) { + return $this->error('validation_error', $e->getMessage(), 422); + } catch (\RuntimeException $e) { + return $this->error('internal_error', $e->getMessage(), 500); + } + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)', + [ + 'methods' => 'GET', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $event = $this->eventService->getEvent($id); + if (!$event) { + return $this->error('not_found', 'event not found', 404); + } + return ['data' => $event]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)', + [ + 'methods' => 'PATCH', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + try { + $deletedKeysProvided = isset($payload['deleted_occurrence_keys']) && is_array($payload['deleted_occurrence_keys']); + $deletedKeys = $deletedKeysProvided ? array_values((array) $payload['deleted_occurrence_keys']) : []; + unset($payload['deleted_occurrence_keys']); + $event = $this->eventService->updateEvent($id, $payload); + } catch (\InvalidArgumentException $e) { + return $this->error('validation_error', $e->getMessage(), 422); + } + if (!$event) { + return $this->error('not_found', 'event not found', 404); + } + if ($deletedKeysProvided) { + $this->eventService->syncDeletedOccurrenceKeys($id, $deletedKeys, true); + $event = $this->eventService->getEvent($id) ?: $event; + } + return ['data' => $event]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)', + [ + 'methods' => 'DELETE', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $ok = $this->eventService->deleteEvent($id); + if (!$ok) { + return $this->error('not_found', 'event not found', 404); + } + return ['data' => ['deleted' => true]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)/occurrences/(?P[^/]+)', + [ + 'methods' => 'DELETE', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $key = urldecode((string) $request->get_param('occurrence_key')); + $ok = $this->eventService->deleteOccurrence($id, $key); + if (!$ok) { + return $this->error('validation_error', 'invalid occurrence or event', 422); + } + return ['data' => ['deleted' => true]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/(?P\d+)/occurrences', + [ + 'methods' => 'GET', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $from = (string) ($request->get_param('from') ?: gmdate('Y-m-d')); + $months = (int) ($request->get_param('months') ?: 3); + $items = $this->eventService->listEventOccurrences($id, $from, $months); + if ($items === null) { + return $this->error('not_found', 'event not found', 404); + } + return ['data' => $items, 'meta' => ['count' => count($items), 'from' => $from, 'months' => max(1, min($months, 24))]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/events/preview-occurrences', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null || !is_array($payload)) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $event = isset($payload['event']) && is_array($payload['event']) ? (array) $payload['event'] : []; + $from = (string) ($payload['from'] ?? gmdate('Y-m-d')); + $months = (int) ($payload['months'] ?? 3); + try { + $items = $this->eventService->previewOccurrences($event, $from, $months); + } catch (\InvalidArgumentException $e) { + return $this->error('validation_error', $e->getMessage(), 422); + } + return ['data' => $items, 'meta' => ['count' => count($items), 'from' => $from, 'months' => max(1, min($months, 24))]]; + }, + ] + ); + } + + private function registerPublicRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/public/events', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array { + $view = (string) ($request->get_param('view') ?: 'month'); + $date = (string) ($request->get_param('date') ?: gmdate('Y-m-d')); + $futureOnlyRaw = (string) ($request->get_param('future_only') ?? ''); + $futureOnly = in_array(strtolower($futureOnlyRaw), ['1', 'true', 'yes', 'on'], true); + $items = $this->eventService->listPublicOccurrences($view, $date, $futureOnly); + return [ + 'data' => $items, + 'meta' => ['count' => count($items), 'view' => $view, 'future_only' => $futureOnly], + ]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/public/sidebar-events', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + $items = $this->eventService->listSidebarUpcoming(14); + return [ + 'data' => $items, + 'meta' => ['count' => count($items), 'window_days' => 14], + ]; + }, + ] + ); + } + + private function registerUserRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/register', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->register( + (string) ($payload['email'] ?? ''), + (string) ($payload['password'] ?? '') + ); + if (isset($result['error'])) { + return $this->unwrapServiceResult($result); + } + + $debugTokens = $this->allowDebugTokens() && !empty($payload['debug_tokens']); + $verifyToken = (string) ($result['verify_token'] ?? ''); + $email = (string) ($result['user']['email'] ?? ''); + if ($verifyToken !== '' && $email !== '') { + $this->sendUserEmailVerification($email, $verifyToken); + } + + if (!$debugTokens) { + unset($result['verify_token']); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/verify', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->verifyEmail((string) ($payload['token'] ?? '')); + if (!isset($result['error'])) { + $verifiedEmail = (string) ($result['user']['email'] ?? ''); + if ($verifiedEmail !== '') { + $this->sendAdminApprovalRequestEmail($verifiedEmail); + } + } + return $this->unwrapServiceResult($result); + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/login', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->login( + (string) ($payload['email'] ?? ''), + (string) ($payload['password'] ?? '') + ); + if (isset($result['error'])) { + return $this->unwrapServiceResult($result); + } + $sessionToken = $this->userService->issueSessionToken((int) ($result['user']['id'] ?? 0), self::SESSION_TTL_SECONDS); + if ($sessionToken !== '') { + $this->setCalendarSessionCookie($sessionToken); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/logout', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + $token = $this->readCalendarSessionTokenFromRequest(null); + if ($token !== '') { + $this->userService->revokeSessionToken($token); + } + $this->clearCalendarSessionCookie(); + return ['data' => ['logged_out' => true]]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/me', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $user = $this->resolveCalDavUserForRequest($request); + if ($user === null) { + return $this->error('auth_required', 'Login failure', 401); + } + return ['data' => $user]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/password/request', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->requestPasswordReset((string) ($payload['email'] ?? '')); + if (isset($result['error'])) { + return $this->unwrapServiceResult($result); + } + + $debugTokens = $this->allowDebugTokens() && !empty($payload['debug_tokens']); + $email = (string) ($payload['email'] ?? ''); + $token = (string) ($result['reset_token'] ?? ''); + if ($email !== '' && $token !== '') { + $this->sendPasswordResetEmail($email, $token); + } + if (!$debugTokens) { + unset($result['reset_token']); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/users/password/reset', + [ + 'methods' => 'POST', + 'permission_callback' => '__return_true', + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null) { + return $this->error('validation_error', 'invalid JSON payload', 422); + } + $result = $this->userService->resetPassword( + (string) ($payload['token'] ?? ''), + (string) ($payload['new_password'] ?? '') + ); + return $this->unwrapServiceResult($result); + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/admin/users', + [ + 'methods' => 'GET', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => fn(): array => ['data' => $this->userService->listUsers()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/admin/users/(?P\d+)/approve', + [ + 'methods' => 'PATCH', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $user = $this->userService->approveUser($id); + if (!$user) { + return $this->error('not_found', 'user not found', 404); + } + return ['data' => $user]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/admin/users/(?P\d+)', + [ + 'methods' => 'DELETE', + 'permission_callback' => fn(): bool => $this->auth->currentUserCan('manage_options'), + 'callback' => function ($request): array|\WP_Error { + $id = (int) $request->get_param('id'); + $ok = $this->userService->removeUser($id); + if (!$ok) { + return $this->error('not_found', 'user not found', 404); + } + return ['data' => ['deleted' => true]]; + }, + ] + ); + } + + private function registerIcsRoutes(): void + { + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/public/ics', + [ + 'methods' => 'GET', + 'permission_callback' => '__return_true', + 'callback' => function (): array { + $settings = $this->settingsService->getAll(); + $calendarName = (string) ($settings['caldav_calendar_name'] ?? 'Public Calendar'); + $ics = $this->icsService->buildCalendar( + $this->eventService->listEvents(), + fn(int $eventId): array => $this->eventService->getDeletedOccurrenceKeys($eventId), + $calendarName + ); + return ['data' => $ics]; + }, + ] + ); + } + + private function registerCalDavRoutes(): void + { + $canRead = fn($request = null): bool => $this->resolveCalDavUserForRequest($request) !== null; + $canWrite = fn($request = null): bool => $this->canWriteCalendar($request); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/resources', + [ + 'methods' => 'GET', + 'permission_callback' => $canRead, + 'callback' => fn(): array => ['data' => $this->calDavService->listResources()], + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/object/(?P[^/]+\.ics)', + [ + 'methods' => 'GET', + 'permission_callback' => $canRead, + 'callback' => function ($request): array|\WP_Error { + $resource = urldecode((string) $request->get_param('resource')); + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + return $this->error('not_found', 'resource not found', 404); + } + return ['data' => $obj]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/object/(?P[^/]+\.ics)', + [ + 'methods' => 'PUT', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $resource = urldecode((string) $request->get_param('resource')); + $payload = $this->jsonPayload($request); + if ($payload === null || !isset($payload['ics']) || !is_string($payload['ics'])) { + return $this->error('validation_error', 'ics payload is required', 422); + } + + $ifMatch = $this->requestHeader($request, 'if-match'); + $ifNoneMatch = $this->requestHeader($request, 'if-none-match'); + $result = $this->calDavService->putObject( + $resource, + $payload['ics'], + $ifMatch !== '' ? $ifMatch : null, + $ifNoneMatch !== '' ? $ifNoneMatch : null, + (int) (($this->resolveCalDavUserForRequest($request)['id'] ?? 0)) + ); + if (isset($result['error'])) { + $error = (array) $result['error']; + return $this->error( + (string) ($error['code'] ?? 'caldav_error'), + (string) ($error['message'] ?? 'caldav operation failed'), + (int) ($error['status'] ?? 500) + ); + } + + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/object/(?P[^/]+\.ics)', + [ + 'methods' => 'DELETE', + 'permission_callback' => $canWrite, + 'callback' => function ($request): array|\WP_Error { + $resource = urldecode((string) $request->get_param('resource')); + $result = $this->calDavService->deleteObject($resource); + if (isset($result['error'])) { + $error = (array) $result['error']; + return $this->error( + (string) ($error['code'] ?? 'caldav_error'), + (string) ($error['message'] ?? 'caldav operation failed'), + (int) ($error['status'] ?? 500) + ); + } + return ['data' => $result]; + }, + ] + ); + + $this->http->registerRestRoute( + self::API_NAMESPACE, + '/caldav/multiget', + [ + 'methods' => 'POST', + 'permission_callback' => $canRead, + 'callback' => function ($request): array|\WP_Error { + $payload = $this->jsonPayload($request); + if ($payload === null || !isset($payload['resources']) || !is_array($payload['resources'])) { + return $this->error('validation_error', 'resources array is required', 422); + } + return ['data' => $this->calDavService->multiget($payload['resources'])]; + }, + ] + ); + } + + private function serveIcsResponse(): void + { + $settings = $this->settingsService->getAll(); + if ( + (string) ($settings['ics_access_mode'] ?? 'public_read') === 'authenticated_read' + && $this->resolveCalDavUserForRequest(null) === null + ) { + http_response_code(401); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['error' => ['code' => 'auth_required', 'message' => 'authentication required']]); + return; + } + + $calendarName = (string) ($settings['caldav_calendar_name'] ?? 'Public Calendar'); + $ics = $this->icsService->buildCalendar( + $this->eventService->listEvents(), + fn(int $eventId): array => $this->eventService->getDeletedOccurrenceKeys($eventId), + $calendarName + ); + $etag = '"' . substr(sha1($ics), 0, 16) . '"'; + $lastModified = gmdate('D, d M Y H:i:s') . ' GMT'; + + http_response_code(200); + header('Content-Type: text/calendar; charset=utf-8'); + header('ETag: ' . $etag); + header('Last-Modified: ' . $lastModified); + header('Cache-Control: public, max-age=120'); + echo $ics; + } + + private function serveCalDavPath(string $path, string $method): void + { + $caldavUser = $this->resolveCalDavUserForRequest(null); + if ($caldavUser === null) { + http_response_code(401); + header('WWW-Authenticate: Basic realm="Calendar CalDAV"'); + header('Content-Type: application/xml; charset=utf-8'); + echo 'auth required'; + return; + } + + $root = rtrim($this->caldavRootPath(), '/'); + $principalCollection = $root . '/principals/'; + $principal = $principalCollection . (int) ($caldavUser['id'] ?? 0) . '/'; + $calendarsRoot = $root . '/calendars/'; + $collection = $root . '/calendars/public/'; + $resourcePrefix = $collection; + + if ($method === 'HEAD') { + if ($path === $root || $path === $root . '/' || $path === $calendarsRoot || $path === rtrim($calendarsRoot, '/') || $path === $collection || $path === rtrim($collection, '/')) { + header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD'); + header('DAV: 1, calendar-access'); + http_response_code(200); + return; + } + if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) { + $resource = basename($path); + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + http_response_code(404); + return; + } + header('Content-Type: text/calendar; charset=utf-8'); + header('ETag: ' . (string) ($obj['etag'] ?? '')); + http_response_code(200); + return; + } + } + + if ($method === 'OPTIONS') { + header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD'); + header('DAV: 1, calendar-access'); + http_response_code(200); + return; + } + + if ($method === 'PROPFIND') { + header('Content-Type: application/xml; charset=utf-8'); + http_response_code(207); + if ($path === $root || $path === $root . '/') { + echo $this->caldavPropfindRootXml($root, $principal, $calendarsRoot, $collection); + return; + } + if ($path === $principalCollection || $path === rtrim($principalCollection, '/')) { + echo $this->caldavPropfindPrincipalCollectionXml($principalCollection, $principal); + return; + } + if ($path === $principal || $path === rtrim($principal, '/')) { + echo $this->caldavPropfindPrincipalXml($principal, $calendarsRoot); + return; + } + if ($path === $calendarsRoot || $path === rtrim($calendarsRoot, '/')) { + echo $this->caldavPropfindCalendarsRootXml($calendarsRoot, $collection); + return; + } + if ($path === rtrim($collection, '/')) { + $path = $collection; + } + if ($path === $collection) { + echo $this->caldavPropfindCollectionXml($collection, $this->caldavSyncToken(), true); + return; + } + if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) { + $resource = urldecode(basename($path)); + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + http_response_code(404); + echo 'not found'; + return; + } + echo $this->caldavPropfindObjectXml($collection . $resource, (string) ($obj['etag'] ?? '')); + return; + } + http_response_code(404); + echo 'not found'; + return; + } + + if ($method === 'REPORT' && $path === $collection) { + $body = (string) file_get_contents('php://input'); + header('Content-Type: application/xml; charset=utf-8'); + http_response_code(207); + echo $this->caldavReportXml($collection, $body, $this->caldavSyncToken()); + return; + } + + if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) { + $resource = urldecode(basename($path)); + if ($method === 'GET') { + $obj = $this->calDavService->getObject($resource); + if ($obj === null) { + http_response_code(404); + return; + } + header('Content-Type: text/calendar; charset=utf-8'); + header('ETag: ' . (string) ($obj['etag'] ?? '')); + http_response_code(200); + echo (string) ($obj['ics'] ?? ''); + return; + } + if ($method === 'PUT') { + $raw = (string) file_get_contents('php://input'); + $ifMatch = trim((string) ($_SERVER['HTTP_IF_MATCH'] ?? '')); + $ifNoneMatch = trim((string) ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')); + $result = $this->calDavService->putObject( + $resource, + $raw, + $ifMatch !== '' ? $ifMatch : null, + $ifNoneMatch !== '' ? $ifNoneMatch : null, + (int) ($caldavUser['id'] ?? 0) + ); + if (isset($result['error'])) { + $error = (array) $result['error']; + http_response_code((int) ($error['status'] ?? 500)); + header('Content-Type: application/xml; charset=utf-8'); + echo '' . esc_html((string) ($error['message'] ?? 'error')) . ''; + return; + } + $status = (int) ($result['status'] ?? 204); + $event = (array) ($result['event'] ?? []); + if (!empty($event['etag'])) { + header('ETag: ' . (string) $event['etag']); + } + http_response_code($status); + return; + } + if ($method === 'DELETE') { + $result = $this->calDavService->deleteObject($resource); + if (isset($result['error'])) { + $error = (array) $result['error']; + http_response_code((int) ($error['status'] ?? 500)); + return; + } + http_response_code(204); + return; + } + } + + http_response_code(405); + header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD'); + } + + private function caldavPropfindRootXml(string $root, string $principal, string $calendarsRoot, string $collection): string + { + return '' + . '' + . '' . htmlspecialchars($root . '/', ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'CalDAV Root' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($calendarsRoot, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'Calendar Home Set' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($collection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars((string) $this->settingsService->get('caldav_calendar_name', 'Public Calendar'), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindPrincipalCollectionXml(string $principalCollection, string $principal): string + { + return '' + . '' + . '' . htmlspecialchars($principalCollection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'Principals' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindPrincipalXml(string $principal, string $calendarsRoot): string + { + return '' + . '' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars($principal, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars($calendarsRoot, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindCalendarsRootXml(string $calendarsRoot, string $collection): string + { + return '' + . '' + . '' . htmlspecialchars($calendarsRoot, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . 'Calendar Home Set' + . 'HTTP/1.1 200 OK' + . '' . htmlspecialchars($collection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars((string) $this->settingsService->get('caldav_calendar_name', 'Public Calendar'), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars(sha1($this->caldavSyncToken()), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . $this->caldavSupportedReportSetXml() + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavPropfindCollectionXml(string $collection, string $syncToken, bool $includeMembers = false): string + { + $xml = '' + . '' + . '' . htmlspecialchars($collection, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars((string) $this->settingsService->get('caldav_calendar_name', 'Public Calendar'), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars(sha1($syncToken), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars(sha1($syncToken), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars($syncToken, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . $this->caldavSupportedReportSetXml() + . 'HTTP/1.1 200 OK'; + + if ($includeMembers) { + $resources = $this->calDavService->listResources(); + foreach ($resources as $r) { + $resource = (string) ($r['resource'] ?? ''); + if ($resource === '') { + continue; + } + $etag = (string) ($r['etag'] ?? ''); + $href = $collection . $resource; + $xml .= '' . htmlspecialchars($href, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars($etag, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'text/calendar; charset=utf-8' + . 'HTTP/1.1 200 OK'; + } + } + + return $xml . ''; + } + + private function caldavPropfindObjectXml(string $href, string $etag): string + { + return '' + . '' + . '' . htmlspecialchars($href, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' + . '' . htmlspecialchars($etag, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . 'text/calendar; charset=utf-8' + . 'HTTP/1.1 200 OK' + . ''; + } + + private function caldavSupportedReportSetXml(): string + { + return '' + . '' + . '' + . '' + . ''; + } + + private function caldavReportXml(string $collection, string $xmlBody, string $syncToken): string + { + $bodyLower = strtolower($xmlBody); + $resources = array_map(static fn(array $r): string => (string) ($r['resource'] ?? ''), $this->calDavService->listResources()); + $items = []; + + if (str_contains($bodyLower, 'sync-collection')) { + $items = $this->calDavService->multiget($resources); + } elseif (str_contains($bodyLower, 'calendar-query')) { + $items = $this->calDavService->multiget($resources); + if (preg_match('/start\\s*=\\s*"([0-9TzZ]+)"/i', $xmlBody, $s) && preg_match('/end\\s*=\\s*"([0-9TzZ]+)"/i', $xmlBody, $e)) { + $start = $this->icalToIso($s[1]); + $end = $this->icalToIso($e[1]); + $items = array_values(array_filter($items, function (array $it) use ($start, $end): bool { + if ((int) ($it['status'] ?? 404) !== 200) { + return false; + } + $ics = (string) ($it['ics'] ?? ''); + $dt = $this->extractFirstDtStartIso($ics); + if ($dt === null) { + return false; + } + return $dt >= $start && $dt <= $end; + })); + } + } else { + preg_match_all('#<[^>]*href[^>]*>([^<]+)]*href>#i', $xmlBody, $matches); + $requested = array_values(array_filter(array_map(static function (string $href): string { + $trimmed = trim($href); + $path = parse_url($trimmed, PHP_URL_PATH); + $candidate = is_string($path) && $path !== '' ? $path : $trimmed; + return urldecode(basename($candidate)); + }, (array) ($matches[1] ?? [])))); + $items = $requested ? $this->calDavService->multiget($requested) : $this->calDavService->multiget($resources); + } + + $responses = ''; + foreach ($items as $item) { + $status = (int) ($item['status'] ?? 404); + $resource = (string) ($item['resource'] ?? ''); + $responses .= '' . htmlspecialchars($collection . $resource, ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''; + if ($status === 200) { + $responses .= '' . htmlspecialchars((string) ($item['etag'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' + . '' . htmlspecialchars((string) ($item['ics'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''; + } + $responses .= 'HTTP/1.1 ' . $status . ($status === 200 ? ' OK' : ' Not Found') + . ''; + } + return '' + . htmlspecialchars($syncToken, ENT_XML1 | ENT_QUOTES, 'UTF-8') + . '' + . $responses + . ''; + } + + private function caldavSyncToken(): string + { + $rows = $this->calDavService->listResources(); + $seed = ''; + foreach ($rows as $row) { + $seed .= (string) ($row['resource'] ?? '') . '|' . (string) ($row['etag'] ?? '') . ';'; + } + return 'urn:calendar-plugin:sync:' . sha1($seed); + } + + private function icalToIso(string $value): string + { + $value = trim($value); + if (preg_match('/^\\d{8}T\\d{6}Z$/', $value)) { + $dt = \DateTimeImmutable::createFromFormat('Ymd\\THis\\Z', $value, new \DateTimeZone('UTC')); + if ($dt instanceof \DateTimeImmutable) { + return $dt->setTimezone(new \DateTimeZone('Europe/London'))->format('c'); + } + } + if (preg_match('/^\\d{8}T\\d{6}$/', $value)) { + $dt = \DateTimeImmutable::createFromFormat('Ymd\\THis', $value, new \DateTimeZone('Europe/London')); + if ($dt instanceof \DateTimeImmutable) { + return $dt->format('c'); + } + } + return '1970-01-01T00:00:00+00:00'; + } + + private function extractFirstDtStartIso(string $ics): ?string + { + if (!preg_match('/^DTSTART(?:;[^:]+)?:([0-9TzZ]+)$/mi', $ics, $m)) { + return null; + } + return $this->icalToIso((string) $m[1]); + } + + private function unwrapServiceResult(array $result): array|\WP_Error + { + if (isset($result['error']) && is_array($result['error'])) { + $error = (array) $result['error']; + return $this->error( + (string) ($error['code'] ?? 'service_error'), + (string) ($error['message'] ?? 'request failed'), + (int) ($error['status'] ?? 500) + ); + } + return ['data' => $result]; + } + + private function collectDiagnosticsSnapshot(bool $enabled): array + { + $diag = [ + 'diagnostics_enabled' => $enabled, + 'generated_at_utc' => gmdate('c'), + 'db_prefix' => $this->db->getPrefix(), + 'table_stem' => $this->tableStem, + 'ics_path' => $this->icsPath(), + 'caldav_root' => $this->caldavRootPath(), + 'current_user_id' => $this->auth->currentUserId(), + 'current_user_email' => $this->auth->currentUserEmail(), + ]; + if (!$enabled) { + $diag['note'] = 'Diagnostics collection is disabled in Setup.'; + return $diag; + } + + $auditTable = $this->db->getPrefix() . trim($this->tableStem, '_') . '_audit_log'; + $sql = $this->db->prepare( + "SELECT actor, action, target, result, created_at, context_json FROM {$auditTable} ORDER BY id DESC LIMIT %d", + 200 + ); + $rows = $this->db->getResults($sql); + $diag['audit_log_recent'] = array_map( + static function (object $row): array { + return [ + 'actor' => (string) ($row->actor ?? ''), + 'action' => (string) ($row->action ?? ''), + 'target' => (string) ($row->target ?? ''), + 'result' => (string) ($row->result ?? ''), + 'created_at' => (string) ($row->created_at ?? ''), + 'context_json' => (string) ($row->context_json ?? ''), + ]; + }, + $rows + ); + $diag['audit_log_count'] = count($diag['audit_log_recent']); + return $diag; + } + + private function updatePluginTablePrefix(string $currentStem, string $requestedPrefix): array + { + $dbPrefix = (string) $this->db->getPrefix(); + if (!str_starts_with($requestedPrefix, $dbPrefix)) { + return ['ok' => false, 'message' => 'Table prefix must start with WordPress DB prefix: ' . $dbPrefix]; + } + $newStem = trim(substr($requestedPrefix, strlen($dbPrefix)), '_'); + if ($newStem === '') { + return ['ok' => false, 'message' => 'Invalid table prefix.']; + } + if ($newStem === $currentStem) { + return ['ok' => true, 'stem' => $currentStem]; + } + + $suffixes = ['events', 'recurrence_exceptions', 'users', 'user_tokens', 'audit_log']; + foreach ($suffixes as $suffix) { + $from = $dbPrefix . $currentStem . '_' . $suffix; + $to = $dbPrefix . $newStem . '_' . $suffix; + $existsFrom = $this->db->getResults($this->db->prepare('SHOW TABLES LIKE %s', $from)); + if (count($existsFrom) === 0) { + continue; + } + $existsTo = $this->db->getResults($this->db->prepare('SHOW TABLES LIKE %s', $to)); + if (count($existsTo) > 0) { + return ['ok' => false, 'message' => 'Target table already exists: ' . $to]; + } + $this->db->query(sprintf('RENAME TABLE `%s` TO `%s`', $from, $to)); + } + update_option('calendar_plugin_table_stem', $newStem); + return ['ok' => true, 'stem' => $newStem]; + } + + private function adminPageShell(string $title, string $bodyHtml): string + { + $nav = '

    Calendar Plugin

    '; + return '

    ' . esc_html($title) . '

    ' + . '
    ' . $nav + . '
    ' . $bodyHtml . '
    '; + } + + private function slugPrefix(): string + { + $slug = trim((string) ($this->settingsService->get('url_slug', '') ?? ''), '/'); + return $slug === '' ? '' : ('/' . $slug); + } + + private function icsPath(): string + { + return $this->slugPrefix() . '/calendar.ics'; + } + + private function caldavRootPath(): string + { + return $this->slugPrefix() . '/caldav/'; + } + + private function canWriteCalendar(mixed $request = null): bool + { + if ($this->auth->currentUserCan('edit_posts')) { + return true; + } + return $this->resolveCalDavUserForRequest($request) !== null; + } + + private static function resolveTableStem(): string + { + $stem = 'cs_calendar'; + $saved = ''; + if (function_exists('get_option')) { + $saved = (string) get_option('calendar_plugin_table_stem', ''); + if ($saved !== '') { + $stem = $saved; + } + } + if (defined('CALENDAR_PLUGIN_TABLE_STEM')) { + $stem = (string) CALENDAR_PLUGIN_TABLE_STEM; + } + if (function_exists('apply_filters')) { + /** @var mixed $filtered */ + $filtered = apply_filters('calendar_plugin_table_stem', $stem); + if (is_string($filtered)) { + $stem = $filtered; + } + } + $stem = strtolower(trim($stem)); + $stem = preg_replace('/[^a-z0-9_]/', '', $stem) ?: 'calendar'; + $stem = trim($stem, '_') ?: 'calendar'; + if ($saved === '') { + $detected = self::detectExistingStem(); + if ($detected !== '') { + return $detected; + } + } + return $stem; + } + + private static function detectExistingStem(): string + { + global $wpdb; + if (!isset($wpdb) || !is_object($wpdb) || !property_exists($wpdb, 'prefix')) { + return ''; + } + $prefix = (string) $wpdb->prefix; + $candidates = ['cs_calendar', 'calendar']; + foreach ($candidates as $candidate) { + $table = $prefix . $candidate . '_events'; + $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); + if (is_string($exists) && $exists !== '') { + return $candidate; + } + } + return ''; + } + + private function resolveCalDavUserForRequest(mixed $request = null): ?array + { + if ($this->auth->currentUserId() > 0) { + return [ + 'id' => $this->auth->currentUserId(), + 'email' => $this->auth->currentUserEmail(), + 'source' => 'wp', + ]; + } + + $sessionToken = $this->readCalendarSessionTokenFromRequest($request); + if ($sessionToken !== '') { + $sessionUser = $this->userService->authenticateSessionToken($sessionToken); + if ($sessionUser !== null) { + $sessionUser['source'] = 'calendar_session'; + return $sessionUser; + } + } + + [$email, $password] = $this->readBasicAuthCredentialsFromRequest($request); + if ($email === '' || $password === '') { + return null; + } + $user = $this->userService->authenticateActiveUserCredentials($email, $password); + if (!$user) { + return null; + } + $user['source'] = 'calendar_user'; + return $user; + } + + private function readBasicAuthCredentialsFromRequest(mixed $request = null): array + { + $header = ''; + if (is_object($request) && method_exists($request, 'get_header')) { + $header = (string) $request->get_header('authorization'); + } + if ($header === '') { + $header = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? ''); + } + if ($header === '' && isset($_SERVER['PHP_AUTH_USER'])) { + return [(string) ($_SERVER['PHP_AUTH_USER'] ?? ''), (string) ($_SERVER['PHP_AUTH_PW'] ?? '')]; + } + if ($header === '' || !str_starts_with(strtolower($header), 'basic ')) { + return ['', '']; + } + $decoded = base64_decode(substr($header, 6), true); + if (!is_string($decoded) || !str_contains($decoded, ':')) { + return ['', '']; + } + [$u, $p] = explode(':', $decoded, 2); + return [(string) $u, (string) $p]; + } + + private function readCalendarSessionTokenFromRequest(mixed $request = null): string + { + $name = self::SESSION_COOKIE; + if (isset($_COOKIE[$name]) && is_string($_COOKIE[$name])) { + return trim((string) $_COOKIE[$name]); + } + + $cookieHeader = ''; + if (is_object($request) && method_exists($request, 'get_header')) { + $cookieHeader = (string) $request->get_header('cookie'); + } + if ($cookieHeader === '') { + $cookieHeader = (string) ($_SERVER['HTTP_COOKIE'] ?? ''); + } + if ($cookieHeader === '') { + return ''; + } + + foreach (explode(';', $cookieHeader) as $pair) { + $parts = explode('=', trim($pair), 2); + if (count($parts) !== 2) { + continue; + } + if (trim((string) $parts[0]) !== $name) { + continue; + } + return trim((string) $parts[1]); + } + + return ''; + } + + private function setCalendarSessionCookie(string $token): void + { + if ($token === '') { + return; + } + $secure = $this->isHttpsRequest(); + setcookie(self::SESSION_COOKIE, $token, [ + 'expires' => time() + self::SESSION_TTL_SECONDS, + 'path' => '/', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } + + private function clearCalendarSessionCookie(): void + { + $secure = $this->isHttpsRequest(); + setcookie(self::SESSION_COOKIE, '', [ + 'expires' => time() - 3600, + 'path' => '/', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } + + private function isHttpsRequest(): bool + { + if (!empty($_SERVER['HTTPS']) && (string) $_SERVER['HTTPS'] !== 'off') { + return true; + } + $forwardedProto = strtolower(trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))); + if ($forwardedProto !== '') { + foreach (explode(',', $forwardedProto) as $proto) { + if (trim($proto) === 'https') { + return true; + } + } + } + if (function_exists('home_url')) { + $home = (string) home_url('/'); + if (str_starts_with(strtolower($home), 'https://')) { + return true; + } + } + return false; + } + + private function allowDebugTokens(): bool + { + return defined('CALENDAR_PLUGIN_ALLOW_DEBUG_TOKENS') && CALENDAR_PLUGIN_ALLOW_DEBUG_TOKENS === true; + } + + private function sendUserEmailVerification(string $email, string $token): void + { + if (!function_exists('wp_mail')) { + return; + } + $verifyUrl = (string) home_url($this->verificationPagePath() . '?calendar_verify_token=' . rawurlencode($token)); + @wp_mail( + $email, + 'Verify your calendar account', + "Please verify your calendar account using this link:\n\n{$verifyUrl}\n\nVerification token (for copy/paste):\n{$token}\n" + ); + } + + private function verificationPagePath(): string + { + $path = trim((string) ($this->settingsService->get('verification_page_path', '/calendar') ?? '/calendar')); + if ($path === '') { + $path = '/calendar'; + } + if (!str_starts_with($path, '/')) { + $path = '/' . $path; + } + return '/' . trim($path, '/'); + } + + private function sendPasswordResetEmail(string $email, string $token): void + { + if (!function_exists('wp_mail')) { + return; + } + $resetUrl = (string) home_url('/?calendar_reset_token=' . rawurlencode($token)); + @wp_mail( + $email, + 'Calendar password reset', + "A password reset was requested for your calendar account.\n\nReset link:\n{$resetUrl}\n" + ); + } + + private function sendAdminApprovalRequestEmail(string $email): void + { + if (!function_exists('wp_mail') || !function_exists('get_option')) { + return; + } + $adminEmail = (string) get_option('admin_email', ''); + if ($adminEmail === '') { + return; + } + $url = (string) admin_url('admin.php?page=calendar-plugin-users'); + @wp_mail( + $adminEmail, + 'Calendar user pending approval', + "A new user is pending approval: {$email}\nReview: {$url}\n" + ); + } + + private function jsonPayload($request): ?array + { + if (!is_object($request) || !method_exists($request, 'get_json_params')) { + return null; + } + $payload = $request->get_json_params(); + return is_array($payload) ? $payload : []; + } + + private function requestHeader(object $request, string $name): string + { + if (method_exists($request, 'get_header')) { + $value = $request->get_header($name); + return is_string($value) ? trim($value) : ''; + } + return ''; + } + + private function error(string $code, string $message, int $status): \WP_Error + { + return new \WP_Error($code, $message, ['status' => $status]); + } +} diff --git a/package/staging/calendar-plugin/src/bootstrap.php b/package/staging/calendar-plugin/src/bootstrap.php new file mode 100644 index 0000000..9fa19ca --- /dev/null +++ b/package/staging/calendar-plugin/src/bootstrap.php @@ -0,0 +1,18 @@ +prefix; + +$tables = [ + $prefix . $stem . '_events', + $prefix . $stem . '_recurrence_exceptions', + $prefix . $stem . '_users', + $prefix . $stem . '_user_tokens', + $prefix . $stem . '_audit_log', +]; + +foreach ($tables as $table) { + $wpdb->query("DROP TABLE IF EXISTS `{$table}`"); +} + +$options = [ + 'calendar_plugin_caldav_calendar_name', + 'calendar_plugin_url_slug', + 'calendar_plugin_ics_access_mode', + 'calendar_plugin_diagnostics_enabled', + 'calendar_plugin_uninstall_cleanup_mode', + 'calendar_plugin_table_stem', + 'calendar_plugin_schema_version', +]; +foreach ($options as $optionName) { + delete_option($optionName); +} diff --git a/read.me b/read.me new file mode 100644 index 0000000..8ba4ea2 --- /dev/null +++ b/read.me @@ -0,0 +1,9 @@ +Status as of 2026-03-31 when codex credit ran out: +1. Removal of plugin did not work. Need to set all files owner to www-data. + +2. Deletion of event in ui doesn't delete event in thunderbird +3. Cannot subscribe to an empty calendar in thunderbird +4. Click inside month cell doesn't add event. +5. Login pane hidden under website hero/banner image +6. Updating an events description via caldav creates weird sequences, e.g. +a space ends up as: text/html,%C2%A0": diff --git a/requirements/api.md b/requirements/api.md new file mode 100644 index 0000000..075a6d6 --- /dev/null +++ b/requirements/api.md @@ -0,0 +1,141 @@ +# API Requirements + +## Purpose +Define the non-CalDAV HTTP API contract for calendar, user-access workflow, and operational endpoints used by the plugin UI and tests. + +## Scope +This document covers: + +- API base path and versioning +- Event CRUD endpoints +- CalDAV-user workflow endpoints +- Standard request/response and error contracts +- Authn/Authz expectations for API calls + +## Normative Boundaries +- API authentication and token/session behavior are defined in `requirements/authentication.md`. +- Authorization matrix and role constraints are defined in `requirements/authorization.md`. +- Error payload/status normalization is defined in `requirements/error_model.md`. +- Recurrence exception behavior is defined in `requirements/recurrence_exceptions.md`. +- Data persistence schema is defined in `requirements/data_schema.md`. + +## API Baseline +- Base path: `/wp-json/calendar/v1` +- If `url_slug` is configured in setup, canonical API paths are prefixed: `//wp-json/calendar/v1`. +- Content type: `application/json; charset=utf-8` +- Time format: ISO 8601 with timezone offset +- All endpoints must be deterministic under `Europe/London` default timezone assumptions unless a timezone is explicitly supplied. + +## Versioning +- Breaking changes require a new version namespace (`v2`, etc.). +- Non-breaking additions are permitted in the current version. +- Deprecated fields/endpoints must remain for at least one release cycle with documentation notice. + +## Event Endpoints + +### Create Event +- `POST /events` +- Requires calendar write capability. +- Request body supports fields in `requirements/editor.md`. +- Response: `201` with created event payload and identifiers. + +### List Events +- `GET /events` +- Supports query params: + - `from` (optional) + - `to` (optional) + - `view` (optional: `list`, `day`, `week`, `month`, `year`) + - `page` and `per_page` (optional) +- Response: `200` with array plus pagination metadata if paged. + +### Get Event +- `GET /events/{event_id}` +- Response: `200` with event payload or `404`. + +### Update Event +- `PUT /events/{event_id}` or `PATCH /events/{event_id}` +- Requires calendar write capability. +- Must enforce optimistic concurrency via version/etag precondition checks. +- Response: `200` with updated event payload. + +### Delete Event +- `DELETE /events/{event_id}` +- Requires calendar write capability. +- Response: `204` on success. + +### Delete Single Occurrence +- `DELETE /events/{event_id}/occurrences/{occurrence_key}` +- Requires calendar write capability. +- Deletes only one occurrence in a recurring series by creating an exception. +- Must not split the underlying recurring series. +- Response: `204` on success. + +## CalDAV User Workflow Endpoints + +### Register +- `POST /users/register` +- Public endpoint with abuse controls. +- Creates account in `pending_approval`. +- Registration is implicitly a write-access request; no separate request endpoint exists. + +### Verify Email +- `POST /users/verify` +- Consumes single-use verification token. +- Marks email as verified while account remains `pending_approval` until admin approval. + +### Forgot Password +- `POST /users/forgot-password` +- Issues password reset token by email. + +### Reset Password +- `POST /users/reset-password` +- Consumes single-use reset token. + +### Admin User List/Update/Delete +- `GET /admin/users` +- `PATCH /admin/users/{user_id}` +- `DELETE /admin/users/{user_id}` +- Admin-only endpoints for approval state transitions and user removal. + +### Admin Diagnostics +- `GET /admin/diagnostics?limit=20` +- Admin-only endpoint. +- Returns recent request/response trace entries for operational troubleshooting. +- Sensitive fields must remain redacted per `requirements/observability.md`. + +## Response Contract +Success responses should include: + +- `data`: endpoint payload +- `meta`: optional metadata (pagination, timestamps, version) + +Error responses should include: + +- `error.code` (stable machine-readable code) +- `error.message` (human-readable summary) +- `error.details` (optional field-level/context details) + +## Validation and Error Statuses +- `400` malformed request +- `401` unauthenticated +- `403` unauthorized +- `404` not found +- `409` conflict (state transition conflict) +- `412` precondition failed (etag/version mismatch) +- `422` semantic validation failure +- `429` rate-limited +- `500` internal server error + +## Security Requirements +- HTTPS required for all authenticated API operations. +- CSRF/nonce protections for cookie-authenticated endpoints. +- Rate limiting for register/verify/reset/login-like flows. +- Error responses must avoid user enumeration leakage. + +## Verification Requirements +Acceptance should verify: + +- Endpoint paths and methods behave as documented. +- Validation and error payloads are consistent. +- Single-occurrence delete creates recurrence exception rather than split series. +- Authz rules enforce role/access constraints. diff --git a/requirements/architecture.md b/requirements/architecture.md new file mode 100644 index 0000000..a48270e --- /dev/null +++ b/requirements/architecture.md @@ -0,0 +1,30 @@ +# Architecture and Runtime Separation Requirements + +## Purpose +Define strict boundaries between deployable plugin code and local WordPress emulation/testing support. + +## Core Separation Rule +- `code/` contains deployable WordPress plugin code only. +- `compatibility-layer/` contains local emulation/shims/test harness support only. + +## Deployable Code Requirements (`code/`) +- Code under `code/` must be deployable to real WordPress without modification. +- Code under `code/` must not import, include, or require files from `compatibility-layer/`. +- Runtime behavior in `code/` must not branch on "test vs production" environment flags. +- Plugin logic in `code/` should depend on WordPress APIs/contracts, not on harness-specific APIs. + +## Compatibility Layer Requirements (`compatibility-layer/`) +- Provides stand-alone/local execution support by emulating required WordPress behavior. +- Must adapt to `code/` contracts; `code/` must not adapt to compatibility-layer internals. +- May include local HTTP harness, fake WP functions, and local data/bootstrap tooling for tests. + +## Packaging and Deployment Boundary +- Build/package inputs for production come from `code/` (plus approved runtime assets) only. +- `compatibility-layer/`, `tests/`, and local tooling are excluded from production artifacts. + +## Verification Requirements +Acceptance must verify: + +1. The same `code/` content runs in real WordPress without source edits. +2. Production package contains no files from `compatibility-layer/`. +3. No `code/` references to `compatibility-layer/` paths/symbols are present. diff --git a/requirements/authentication.md b/requirements/authentication.md new file mode 100644 index 0000000..6fba507 --- /dev/null +++ b/requirements/authentication.md @@ -0,0 +1,72 @@ +# Authentication Requirements + +## Purpose +Define authentication behavior for admin UI, API, CalDAV, and ICS access. + +## Scope +This document covers: + +- Credential types and login flows +- Session/token behavior +- Password rules and recovery +- Abuse controls and lockout behavior + +## Credential Domains +Authentication domains: + +- WordPress admin authentication (for plugin admin pages) +- Plugin CalDAV user authentication (for CalDAV access) +- API authentication (WordPress auth for admin API and plugin user auth where relevant) + +Credentials must not be shared in plaintext between domains. + +## Admin UI Authentication +- Admin pages (`Users`, `Setup`) require valid WordPress authenticated session. +- Capability checks are enforced after authentication. +- Non-authenticated access redirects/fails using WordPress-standard behavior. + +## CalDAV Authentication +- CalDAV endpoints require authenticated plugin CalDAV user credentials over HTTPS. +- Supported first-pass mechanism: HTTP Basic over TLS against plugin user store. +- Password verification must use secure hash comparison. +- Unverified or non-approved (`pending_approval`) users cannot authenticate. + +## API Authentication +- Admin API endpoints require WordPress-authenticated context plus nonce/CSRF protections where cookie auth is used. +- User lifecycle endpoints may allow public access only where explicitly required (`register`, `verify`, `forgot/reset`) with abuse controls. +- Authenticated user endpoints require either WordPress user context or plugin user context as documented per endpoint. + +## Password Policy +- Minimum length: 8 characters. +- Passwords must be stored only as secure hashes (never reversible encryption). +- Password reset rotates credentials immediately. + +## Token Policy +- Verification and reset tokens must be single-use, random, and time-limited. +- Token reuse must fail deterministically. +- Expired tokens must produce actionable but non-sensitive error messaging. + +## Session and Revocation +- After password reset, prior login sessions/tokens for that identity should be invalidated. +- Users not in `active` status lose access immediately for new requests. + +## Abuse Controls +- Rate limiting required for: + - registration + - verification attempts + - forgot/reset flows + - login attempts +- Lockout/backoff behavior must be documented in operational docs. + +## Logging and Privacy +- Authentication failures should be logged with timestamp and source context. +- Logs must not contain plaintext passwords or tokens. +- Responses must avoid user enumeration details. + +## Verification Requirements +Acceptance should verify: + +- Valid credentials authenticate to intended surfaces only. +- Invalid credentials fail safely. +- Pending/unverified users are denied. +- Password reset and token expiry behavior works as specified. diff --git a/requirements/authorization.md b/requirements/authorization.md new file mode 100644 index 0000000..85e826e --- /dev/null +++ b/requirements/authorization.md @@ -0,0 +1,61 @@ +# Authorization Requirements + +## Purpose +Define role/capability-based authorization rules for admin UI, API, CalDAV, and ICS behavior. + +## Scope +This document covers: + +- Authorization matrix by actor and operation +- Admin override behavior +- Default-deny rules + +## Authorization Model +- Authorization is explicit and default-deny. +- Authentication is required before capability checks. +- Least privilege is required for all operations. + +## Actor Types +- `wp_admin`: WordPress user with calendar admin capability +- `wp_editor`: WordPress user with editor-level calendar capability (if enabled) +- `caldav_write`: plugin CalDAV user with approved write access +- `public`: unauthenticated user + +## Capability Matrix + +### Admin UI +- `Users`: `wp_admin` only +- `Setup`: `wp_admin` only + +### Event Data Operations +- Create/update/delete events in admin/API: requires write calendar capability +- Delete one recurrence occurrence: requires write capability and must create exception + +### CalDAV Operations +- Discovery/read/query (`OPTIONS`, `PROPFIND`, `REPORT`, `GET`): allowed for authenticated approved users +- Write operations (`PUT`, `DELETE`): allowed for authenticated approved users + +### User Management +- Approve/remove users: `wp_admin` only + +### ICS Access +- Public ICS endpoint visibility is governed by setup configuration. +- If endpoint is public: `public` may read only approved public events. +- If endpoint requires auth: enforce configured auth policy consistently. + +## Ownership and Scope Rules +- CalDAV users operate against the single shared `public` calendar collection. +- Access control is enforced by account approval state (`pending_approval` vs `active`) rather than per-user private calendars. +- Admin actions must not silently grant broader rights than requested. + +## State Transition Rules +- User transition `pending_approval` -> `active` requires explicit admin action. +- Unauthorized transition attempts return `403`. + +## Verification Requirements +Acceptance should verify: + +- Capability matrix is enforced across all surfaces. +- Pending/unverified users cannot perform CalDAV operations. +- `Users` and `Setup` cannot be accessed by unauthorized roles. +- Public/authorized ICS access respects configured policy. diff --git a/requirements/caldav.md b/requirements/caldav.md new file mode 100644 index 0000000..7d267dc --- /dev/null +++ b/requirements/caldav.md @@ -0,0 +1,190 @@ +# CalDAV Requirements + +## Purpose +Define requirements for exposing the plugin calendar through a CalDAV endpoint with read/write behavior and standards-compatible resource representations. + +## Standards and RFC +CalDAV behavior must be standards-compatible with: + +- RFC 4791: Calendaring Extensions to WebDAV (CalDAV) +- RFC 4918: HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV) +- RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar) +- RFC 6578: Collection Synchronization for WebDAV (sync report) +- RFC 7232: HTTP conditional requests (ETag/If-Match semantics) + +If additional CalDAV/WebDAV extensions are used, they must be documented and not break baseline client interoperability. + +## Timezone Assumption +Unless explicitly overridden by a future requirement, all plugin dates and times are assumed to be in the `Europe/London` timezone. + +## Scope +This document covers: + +- CalDAV endpoint structure and resources +- Required WebDAV/CalDAV operations +- Mapping between plugin data and CalDAV/iCalendar resources +- Concurrency, sync, and error behavior + +## Normative Boundaries +To avoid ambiguity across requirement documents: + +- Exact CalDAV URI layout, method matrix, and DAV property/report behavior are defined in `requirements/caldav_endpoints.md`. +- Authentication and authorization policy is defined in `requirements/authentication.md` and `requirements/authorization.md`. +- Error response normalization is defined in `requirements/error_model.md`. +- Recurrence exception semantics are defined in `requirements/recurrence_exceptions.md`. +- Concrete persistence schema is defined in `requirements/data_schema.md`. + +## Endpoint and Resource Model +The plugin must expose a CalDAV hierarchy with authenticated user principals and calendar collections. + +Minimum resource model: + +- Principal resource per authenticated CalDAV user +- Shared calendar home set +- Single shared `public` calendar collection +- Event resources as `text/calendar` (`VEVENT`-based `.ics` objects) within collections + +URI requirements: + +- Resource URIs must be stable for the lifetime of each object. +- Event resource URI should be derived from a stable internal event identifier. +- Recurrence exceptions must remain part of the same logical series and not create split series artifacts. + +## Authentication and Authorization +- CalDAV access must require HTTPS and authenticated credentials. +- Authorization must enforce account approval state (`pending_approval` vs `active`). +- Unverified or non-approved users must be denied authentication/authorization. +- `active` users are write-enabled for the shared calendar. +- All authenticated users access the same shared calendar collection. + +## Required CalDAV/WebDAV Operations +The endpoint must support these operations at minimum. + +### Discovery and Collection Introspection +- `OPTIONS`: advertise DAV capabilities, including CalDAV support. +- `PROPFIND`: + - discover principals, calendar home sets, and calendar collections + - retrieve core properties (display name, resource type, ctag/sync metadata where available) + +### Read and Query +- `REPORT` (`calendar-query`): return events in collection, including time-range filtering. +- `REPORT` (`calendar-multiget`): fetch specific event resources by href. +- `GET`: retrieve individual event resource (`text/calendar`). + +### Create and Update +- `PUT`: create new event resource or replace an existing event resource. +- `PUT` updates must preserve recurrence/exception semantics from iCalendar input. +- Write operations must require approved authenticated access. + +### Delete +- `DELETE`: remove event resource when permitted. +- Deleting a single occurrence of a recurring event must be represented as a recurrence exception in the series (`EXDATE` and/or `RECURRENCE-ID` override pattern), not by splitting into multiple independent series. + +### Concurrency and Sync +- `ETag` must be emitted for event resources. +- `If-Match`/`If-None-Match` preconditions must be honored for safe updates/creates. +- `REPORT` (`sync-collection`) should be supported for incremental sync tokens. +- Sync token invalidation/rotation behavior must be deterministic and documented. + +## iCalendar Representation Requirements +CalDAV event payloads must be standards-compatible `VCALENDAR` with `VEVENT` components. + +Minimum mapping expectations: + +- Internal stable event id -> `UID` +- Title -> `SUMMARY` +- Description -> `DESCRIPTION` +- Location -> `LOCATION` +- Category -> `CATEGORIES` +- Start/end -> `DTSTART` / `DTEND` +- Last modification timestamp -> `DTSTAMP` (and `LAST-MODIFIED` when available) +- Recurrence rules -> `RRULE` +- Recurrence exceptions -> `EXDATE` and/or additional `VEVENT` with matching `UID` plus `RECURRENCE-ID` + +Recurrence behavior: + +- Series-level recurrence remains a single logical event sequence keyed by `UID`. +- Exception instances must be represented as exceptions to that `UID`, not a new split sequence. +- Sequence/version metadata should be updated on write operations so clients detect changes. +- Monthly ordinal rules must round-trip accurately, including `BYDAY=2SA`-style forms and `BYSETPOS=-1` (`last` weekday in month). + +## Plugin Data Mapping Requirements +Plugin persistence must represent CalDAV resources in a way that supports idempotent read/write sync. + +Required persisted mapping fields (direct columns or normalized equivalents): + +- Internal event id +- CalDAV resource path/name +- `UID` +- Current `ETag` +- Calendar collection id +- Last-modified-by user id (nullable for system/import operations) +- Serialized recurrence rule data +- Recurrence exception records (date-only exceptions and/or overridden instances) +- Created/updated timestamps + +Behavior requirements: + +- Importing/updating from CalDAV must map to existing records by stable identifiers (`UID` + resource identity rules). +- Duplicate creation from repeated client retries must be prevented. +- Data model must preserve enough metadata to regenerate standards-compliant responses. + +## Error Handling Requirements +- Malformed iCalendar payloads must return appropriate client error responses. +- Authorization failures must return appropriate auth status without leaking sensitive details. +- Write precondition failures (etag mismatch) must return precondition errors and no partial write. +- Server errors must be logged with enough detail for troubleshooting. + +## Security and Privacy +- Transport must be TLS-only for credentials and calendar data. +- Sensitive tokens/credentials must not be logged in plaintext. +- Responses must not leak admin-only or internal plugin metadata. + +## Interoperability Targets +The implementation should interoperate with common CalDAV clients, including: + +- Apple Calendar +- Thunderbird +- DAVx5-class clients + +Client-specific workarounds, if required, must be documented. + +## Majority-Client Compatibility Strategy +There is no single guaranteed feature set that satisfies every CalDAV client implementation, but interoperability for the majority can be managed by combining: + +- strict baseline standards compliance (RFC 4791 + RFC 4918 + RFC 5545) +- a stable compatibility profile for discovery/auth/report/write behavior +- continuous regression tests against representative client patterns + +The project must maintain a compatibility profile with three levels: + +- Level A (required for release): standards-critical discovery/auth/read + - `401` with `WWW-Authenticate` on unauthenticated CalDAV access + - principal discovery (`current-user-principal`) + - principal `calendar-home-set` + - discoverable calendar collection with `` + - `REPORT` support for `calendar-query` and `calendar-multiget` +- Level B (required for release): practical write/sync interoperability + - `PUT`/`DELETE` with stable ETag behavior + - recurrence + exception round-trip fidelity + - sync collection stability for incremental updates +- Level C (best effort): client-specific ergonomics/extensions + - optional properties beyond baseline RFC surface + - minor behavior adjustments for specific client quirks that do not break A/B + +Release gating must include at least: + +- `fixture-tests/fixture_caldav_client_compat_smoke.sh` (legacy local discovery/auth compatibility; retained for archival comparison) +- existing fixture smoke + security smoke suites +- at least one real-client manual smoke (for example Thunderbird or Apple Calendar) for release candidates + +## Verification Requirements +Acceptance should verify: + +- Principal and calendar discovery works via `OPTIONS` and `PROPFIND`. +- Calendar query and multiget reports return correct data for time-range and href selection. +- `GET`, `PUT`, and `DELETE` behaviors match access level permissions. +- ETag and conditional writes prevent lost updates. +- Sync collection reports provide incremental changes. +- Recurring-event single-occurrence delete results in exception representation, not sequence split. +- Returned iCalendar validates against RFC 5545 expectations and is accepted by target clients. diff --git a/requirements/caldav_endpoints.md b/requirements/caldav_endpoints.md new file mode 100644 index 0000000..07770e0 --- /dev/null +++ b/requirements/caldav_endpoints.md @@ -0,0 +1,117 @@ +# CalDAV Endpoint Requirements + +## Purpose +Define exact CalDAV URI structure, required methods/reports/properties, and expected status behavior for interoperability. + +## Scope +This document covers: + +- CalDAV URI layout +- Method support by resource type +- Required WebDAV/CalDAV properties and reports +- Required status-code behavior + +## Normative Boundaries +- This document is authoritative for CalDAV URI structure and method/property/report support. +- Event/recurrence data semantics are defined in `requirements/caldav.md` and `requirements/recurrence_exceptions.md`. +- Authentication/authorization and error behavior norms are defined in: + - `requirements/authentication.md` + - `requirements/authorization.md` + - `requirements/error_model.md` + +## Endpoint Layout +Base CalDAV root: + +- `/caldav/` +- If `url_slug` is configured in setup, canonical CalDAV root is `//caldav/`. + +Resource hierarchy: + +- Principal collection: `/caldav/principals/` +- User principal: `/caldav/principals/{user_id}/` +- Calendar home set: `/caldav/calendars/` +- Shared public calendar collection: `/caldav/calendars/public/` +- Event object resource: `/caldav/calendars/public/{object_id}.ics` + +URI rules: + +- `{user_id}` is stable and URL-safe. +- `{object_id}` is stable for object lifetime. +- All authenticated principals discover the same shared calendar home set and `public` collection. +- Object rename/move behavior is unsupported in first pass unless explicitly implemented. + +## Methods by Resource Type + +### `/caldav/` +- `OPTIONS` +- `PROPFIND` (Depth 0/1) + +### Principal resources +- `PROPFIND` +- `REPORT` where applicable for principal discovery support + +### Calendar collection resources +- `OPTIONS` +- `PROPFIND` +- `REPORT` (`calendar-query`, `calendar-multiget`, `sync-collection`) + +### Event object resources +- `GET` +- `PUT` +- `DELETE` +- `PROPFIND` (Depth 0) + +## Required DAV/CalDAV Properties +Calendar collection and principal responses must support, at minimum, these properties (where applicable): + +- `resourcetype` +- `displayname` +- `current-user-principal` +- `principal-URL` +- `calendar-home-set` +- `supported-calendar-component-set` +- `getctag` (or equivalent documented change tag) +- `getetag` for object resources +- `sync-token` for collections supporting sync + +## REPORT Support +- `calendar-query` with time-range filtering +- `calendar-multiget` by href set +- `sync-collection` for incremental changes since sync token + +If a report is unsupported for a resource, server returns standards-appropriate error status with DAV error body. + +## Status Behavior +- `200` successful read/report/property retrieval +- `201` object created by `PUT` +- `204` successful delete/update with no body where applicable +- `207` multi-status for PROPFIND/REPORT responses +- `401` unauthenticated +- `403` authenticated but forbidden +- `404` resource not found +- `405` method not allowed on resource +- `409` parent/resource state conflict +- `412` precondition failed (etag/if-match semantics) +- `415` unsupported media type + +## Content Handling +- Event objects use `text/calendar` payloads with RFC 5545-compatible `VCALENDAR`. +- Unsupported component types should be rejected unless explicitly mapped. +- Server should normalize line endings/content as required by iCalendar compatibility. + +## Concurrency +- Object resources must emit `ETag`. +- `If-Match` and `If-None-Match` must be honored on `PUT`. +- Lost-update prevention is required on concurrent writes. + +## Recurrence Exception Behavior +- Deleting one recurrence occurrence must be represented as an exception for the existing series. +- The resulting data must remain one logical series (same `UID`), without splitting into separate series resources unless explicitly required by standards-compatible override semantics. + +## Verification Requirements +Acceptance should verify: + +- URI layout and discovery flows are stable. +- Required methods return expected statuses. +- REPORT responses include correct event sets. +- Conditional write and etag behavior prevents stale overwrite. diff --git a/requirements/caldav_users.md b/requirements/caldav_users.md new file mode 100644 index 0000000..33d01e5 --- /dev/null +++ b/requirements/caldav_users.md @@ -0,0 +1,130 @@ +# CalDAV User Access Requirements + +## Purpose +Define requirements for CalDAV user accounts and permissions for the calendar plugin, with minimal maintenance burden for non-technical users. + +## Scope +This document covers: + +- Self-service account creation for calendar app access +- Email verification and password recovery +- WordPress admin approval and user state management +- CalDAV authentication and authorization behavior + +## Normative Boundaries +- User lifecycle and state expectations are defined here. +- Detailed authentication controls are defined in `requirements/authentication.md`. +- Detailed authorization matrix is defined in `requirements/authorization.md`. +- Concrete persisted schema is defined in `requirements/data_schema.md`. + +## Admin Navigation +CalDAV user administration must be provided through the shared calendar plugin admin menu. + +Requirements: + +- User administration appears in a `Users` sub-entry under the calendar plugin main menu. +- User administration is not implemented as a separate top-level plugin menu. +- Access to `Users` is restricted to authorized WordPress admin roles/capabilities. + +## User Experience Goals +The access model must prioritize low-friction onboarding and low ongoing maintenance. + +Requirements: + +- A user can create a calendar access account without admin intervention. +- A user can recover access independently using password reset. +- A user does not need to understand WordPress internals to use CalDAV access. +- Error messages and emails must be plain language and action-oriented. + +## Account Model +CalDAV access must use plugin-managed user accounts tied to identity and approval state. + +Required account fields: + +- Unique login identifier (email address) +- Password hash (never plaintext) +- Email verification status +- Account status (`pending_approval`, `active`) +- Created/updated timestamps + +Notes: + +- Registration is implicitly a request for write access. +- There is no separate `read_only` approval tier in the user model. + +## Registration and Email Verification +Users must be able to self-register and verify their email before admin approval. + +Requirements: + +- Registration requires email address and password. +- Registration creates account state `pending_approval`. +- A verification email is sent with a single-use, time-limited token/link. +- CalDAV authentication must fail until email verification is complete. +- Successful verification marks account email as verified while remaining `pending_approval`. +- Expired or invalid verification links must return a deterministic validation error. + +## Password Recovery +Users must be able to recover account access without administrator support. + +Requirements: + +- "Forgot password" flow is available from the login/access page. +- Password reset uses a single-use, time-limited token sent by email. +- Password policy minimum length is 8 characters. +- Reset token invalidation occurs immediately after successful password change. +- Existing sessions/tokens should be revoked after password reset. +- Clear success/failure messaging is required. + +## Approval Workflow +WordPress admins must have a dedicated interface to manage pending and approved users. + +Requirements: + +- Admin view lists users with email, verification state, and account status. +- Admin can approve a verified pending user by setting status to `active`. +- Admin can remove defunct users. +- Admin actions must be logged with actor, timestamp, and action outcome. +- State transitions must be explicit and validated. + +## CalDAV Authentication and Authorization +CalDAV endpoint access must authenticate users and authorize operations by approval state. + +Requirements: + +- CalDAV endpoint requires HTTPS and authenticated credentials. +- Authentication uses plugin account credentials. +- Authorization checks apply on every CalDAV request. +- Accounts that are unverified or not `active` must be denied. +- `active` users are write-enabled for the shared calendar. + +## Email Delivery Requirements +Email-dependent workflows must be reliable and understandable. + +Requirements: + +- Emails are sent for verification and password reset. +- Admin notification email is sent when a user completes verification and is pending approval. +- Templates must include clear subject lines and concise next-step instructions. +- Email sends must fail closed (no auth bypass) when delivery is unavailable. + +## Security and Abuse Controls +The account system must include baseline controls to reduce abuse risk. + +Requirements: + +- Passwords must meet minimum strength requirements. +- Registration, login, and password-reset request endpoints should include rate limiting. +- Tokens must be cryptographically strong and time-limited. +- Sensitive actions must use CSRF protections in web forms. +- User enumeration should be minimized in public responses. + +## Verification Requirements +Acceptance should verify: + +- A new user can register, verify email, and become pending approval. +- Unverified or pending users cannot authenticate to CalDAV. +- Password reset succeeds end-to-end and invalidates prior sessions/tokens. +- Admin can approve (`active`) and remove users. +- Approved users can authenticate and perform CalDAV writes. +- Admin user list accurately reflects user states and recent actions. diff --git a/requirements/data.md b/requirements/data.md new file mode 100644 index 0000000..3c838a5 --- /dev/null +++ b/requirements/data.md @@ -0,0 +1,91 @@ +# Data Requirements + +## Purpose +Define requirements for calendar data storage in WordPress and for local testing data behavior. + +## Scope +This document covers: + +- Persistent plugin data in WordPress database +- Data model expectations for calendar entries and recurrence +- Data handling for local compatibility-harness testing + +## Normative Boundaries +- This document defines high-level data requirements and test-data expectations. +- Concrete schema fields, constraints, and indexing are defined in `requirements/data_schema.md`. +- Recurrence exception semantics are defined in `requirements/recurrence_exceptions.md`. +- Runtime/code separation constraints are defined in `requirements/architecture.md`. + +## Timezone Assumption +Unless explicitly overridden by a future requirement, all plugin dates and times are assumed to be in the `Europe/London` timezone. + +## WordPress Database Storage +Plugin data must be stored using WordPress-compatible database access patterns. + +Requirements: + +- Plugin-owned tables use WordPress table prefix conventions (`$wpdb->prefix`). +- Schema creation/migration uses WordPress APIs (see lifecycle requirements). +- Data access uses WordPress database APIs (`$wpdb`) with prepared/safe queries. +- Stored records support full CRUD and recurrence features required by editor/web UI. + +## Minimum Event Data Model +The stored model must support these fields (direct columns or normalized equivalents): + +- Event identifier (internal primary key) +- `title` +- `location` +- `category` +- `all_day_event` flag +- `start_datetime` +- `end_datetime` +- `repeat_type` (`none`, `daily`, `weekly`, `monthly`, `yearly`, `custom`) +- Recurrence configuration (`interval`, `range_mode`, `count`, `until_date`, base unit) +- `description` +- Created/updated timestamps + +## Data Integrity Requirements +- `title` and start time/date must be required. +- End must not be before start. +- Recurrence values must be internally consistent (e.g., positive interval/count). +- Deletes must not leave orphaned recurrence metadata or exception records. +- Migrations must preserve existing user data. + +## Query and Retrieval Requirements +- Data model must support efficient retrieval by date range for day/week/month/year UI windows. +- Data model must support generation of recurrence occurrences for both web UI and ICS output. +- Queries for public UI must return only intended public event data. + +## Local Testing Data Requirements +The local compatibility-harness test environment must support deterministic data setup and teardown. + +Requirements: + +- Provide seeded test data covering: + - Single non-recurring events + - All-day events + - Each recurrence type + - Custom recurrence with each range mode + - Edge cases (month boundaries, leap year, DST transitions where relevant) +- Test runs must isolate state between cases (clean database state or controlled fixtures). +- Test data setup scripts/fixtures must be version-controlled under `tests/` and/or `compatibility-layer/`. + +## Test Validation Data Requirements +Automated/local verification should include assertions for: + +- CRUD persistence correctness +- Recurrence expansion correctness in 3-month preview windows +- Correct behavior of keep/remove data paths during uninstall +- Correct mapping readiness for ICS export fields + +## Backup and Recovery (Server Data) +- Production/test server data changes should be recoverable via normal backup processes. +- Destructive operations (e.g., uninstall data removal) require explicit intent and must be test-covered. + +## Documentation Requirements +Project docs must include: + +- Current schema description +- Migration/versioning approach +- Local compatibility-harness usage +- Data retention behavior across install/upgrade/uninstall diff --git a/requirements/data_schema.md b/requirements/data_schema.md new file mode 100644 index 0000000..fda8f31 --- /dev/null +++ b/requirements/data_schema.md @@ -0,0 +1,138 @@ +# Data Schema Requirements + +## Purpose +Define concrete schema requirements for events, recurrence exceptions, CalDAV metadata, and user-access lifecycle state. + +## Scope +This document covers: + +- Required tables/entities and key fields +- Indexing and uniqueness rules +- Migration/versioning expectations +- Data integrity constraints + +## Schema Baseline +- All plugin tables must use WordPress prefix (`$wpdb->prefix`). +- Schema creation/migration uses WordPress mechanisms (`dbDelta`, controlled migrations). +- Charset/collation should follow WordPress defaults. + +## Required Logical Entities + +### Events +Required fields: +- `id` (PK) +- `uid` (stable iCalendar UID) +- `title` +- `description` +- `location` +- `category` +- `all_day_event` +- `start_datetime` +- `end_datetime` +- `repeat_type` +- `repeat_interval` +- `repeat_range_mode` +- `repeat_count` (nullable) +- `repeat_until` (nullable) +- `timezone` (default `Europe/London`) +- `created_at` +- `updated_at` + +Constraints: +- `title`, `start_datetime` required +- `end_datetime >= start_datetime` +- recurrence fields internally consistent + +### Recurrence Exceptions +Required fields: +- `id` (PK) +- `event_id` (FK -> events.id) +- `occurrence_key` (canonical occurrence datetime key) +- `exception_type` (`deleted_occurrence`, `override_occurrence`) +- `override_payload` (nullable structured data for modified occurrence) +- `created_at` +- `updated_at` + +Constraints: +- unique (`event_id`, `occurrence_key`) +- deleted-occurrence exception must suppress that one occurrence without splitting series + +### CalDAV Objects +Required fields: +- `id` (PK) +- `event_id` (FK -> events.id) +- `calendar_id` +- `resource_path` +- `etag` +- `sync_version` or equivalent change sequence +- `last_modified_by_user_id` (FK -> caldav_users.id, nullable) +- `created_at` +- `updated_at` + +Constraints: +- unique (`calendar_id`, `resource_path`) +- unique `etag` progression by object version + +### CalDAV Users +Required fields: +- `id` (PK) +- `email` (unique) +- `password_hash` +- `email_verified_at` (nullable) +- `account_status` (`pending_approval`, `active`) +- `access_level` (implementation detail; approved users are write-enabled) +- `request_state` (implementation detail; tracks approval pipeline when present) +- `created_at` +- `updated_at` + +### User Tokens +Required fields: +- `id` (PK) +- `user_id` (FK -> caldav_users.id) +- `token_type` (`verify_email`, `reset_password`) +- `token_hash` +- `expires_at` +- `used_at` (nullable) +- `created_at` + +Constraints: +- tokens are single-use +- expired/used tokens are invalid + +### Audit Log +Required fields: +- `id` (PK) +- `actor_type` (`wp_user`, `caldav_user`, `system`) +- `actor_id` +- `action` +- `target_type` +- `target_id` +- `result` (`success`, `failure`) +- `context_json` +- `created_at` + +## Indexing Requirements +- Events index on `start_datetime`, `end_datetime` +- Events unique index on `uid` where logical uniqueness is required +- Exceptions index on `event_id` +- CalDAV objects index on `calendar_id`, `resource_path`, `etag` +- Users unique index on `email` +- Tokens index on `user_id`, `token_type`, `expires_at` +- Audit log index on `created_at`, `actor_id`, `action` + +## Migration and Versioning +- Schema version must be stored in plugin options. +- Upgrades must be incremental, idempotent, and logged. +- Downgrade strategy must be documented; if unsupported, explicit warning required. + +## Data Retention +- Behavior on uninstall follows lifecycle requirements. +- If removal is selected, plugin-owned tables and options are removed safely. +- If retention is selected, schema/data remains for future reactivation. + +## Verification Requirements +Acceptance should verify: + +- Fresh install creates expected schema. +- Upgrade applies required structural changes without data loss. +- Constraints enforce recurrence exception uniqueness and no split-series artifacts. diff --git a/requirements/deployment.md b/requirements/deployment.md new file mode 100644 index 0000000..a165cb0 --- /dev/null +++ b/requirements/deployment.md @@ -0,0 +1,105 @@ +# Deployment Requirements + +## Purpose +Define how the plugin is deployed to the remote WordPress host and how deployment correctness is verified, including an exact-match validation between the approved artifact and deployed runtime files. + +## Scope +This document covers: + +- Remote host deployment target and access assumptions +- Artifact-only deployment model +- Pre-deploy checks +- Post-deploy validation +- Exact-match validation requirements +- Rollback requirements + +This document does not define packaging rules (see `requirements/packaging.md`) or runtime feature behavior. + +## Normative References +- Packaging requirements: `requirements/packaging.md` +- Environment/runtime requirements: `requirements/environment.md` +- Architecture/runtime separation requirements: `requirements/architecture.md` +- Smoke/regression expectations: `tests/smoke_tests.md` + +## Deployment Model +- Deployments must use a built plugin artifact (zip) produced from approved repository content. +- Direct ad-hoc editing of production plugin files is not permitted. +- Deployment target path must point to the active WordPress plugin directory. +- Deployable runtime must come from `code/` only; compatibility/emulation assets are not deployable. + +For the current remote test target: +- WordPress root: `/var/www/wordpress` +- Plugin directory root: `/var/www/wordpress/wp-content/plugins` +- Plugin deploy directory: `/var/www/wordpress/wp-content/plugins/calendar-plugin` + +## Pre-Deployment Requirements +Before deployment: + +1. Package artifact has passed packaging validation. +2. Local and remote smoke checks required for the release scope are green. +3. Remote path existence/permissions are verified. +4. Backup or rollback artifact for currently deployed version is available. +5. Deployment record includes target host, artifact name, version, timestamp, and operator. + +## Deployment Procedure Requirements +Required high-level procedure: + +1. Transfer approved artifact to remote host staging area. +2. Extract artifact to a clean temporary directory on remote host. +3. Validate extracted plugin directory structure. +4. Synchronize extracted plugin directory to deploy directory. +5. Run post-deploy verification checks. + +## Exact-Match Validation (Required) +After deployment, deployed plugin files must exactly match the approved artifact contents (excluding allowed mutable runtime files if any are explicitly listed). + +Validation must include: + +1. File set equality: +- No missing files in deployment compared to artifact. +- No extra files in deployment compared to artifact. + +2. File content equality: +- Each deployed file content hash must match artifact file hash. + +3. Optional metadata check (recommended): +- File mode/permissions match expected deployment policy. + +Accepted implementation options: +- Manifest-based validation: generate a sorted list of ` ` from extracted artifact and deployed directory and compare byte-for-byte. +- Rsync dry-run checksum validation (for example `rsync -avznc --delete`) plus explicit failure on any reported delta. + +Any mismatch must fail deployment validation and trigger rollback decision. + +## Post-Deployment Verification Requirements +After exact-match validation: + +1. Plugin is present and loadable by WordPress. +2. Plugin activation state is verified (as required by release process). +3. CalDAV endpoint discovery and ICS endpoint health checks pass. +4. Critical UI/API smoke checks pass. + +## Rollback Requirements +If deployment validation or post-deploy checks fail: + +1. Revert to previous known-good plugin artifact. +2. Re-run minimum smoke checks to confirm recovery. +3. Record incident details and remediation before next deploy attempt. + +## Audit and Traceability Requirements +Each deployment must record: + +- artifact name/version +- source revision/tag +- target host/path +- deploy timestamp +- validation result (including exact-match evidence) +- rollback status if applicable + +## Acceptance Criteria +Deployment process is acceptable only if: + +1. Artifact-only deployment is enforced. +2. Exact-match file and hash validation is performed and passes. +3. Required post-deploy smoke checks pass. +4. Deployment record contains all traceability fields. diff --git a/requirements/editor.md b/requirements/editor.md new file mode 100644 index 0000000..34ec166 --- /dev/null +++ b/requirements/editor.md @@ -0,0 +1,93 @@ +# Calendar Editor Requirements + +## Purpose +Define implemented requirements for the authenticated calendar editor used from the `/calendar` UI. + +## Scope +This document covers: + +- Event CRUD in the modal editor +- Supported event fields and validation +- Implemented recurrence options +- Recurrence preview and single-occurrence deletion UX + +## Access Model +- Public users are read-only. +- Plugin users in `active` status can create, update, and delete events. +- WordPress users with `edit_posts` capability can also perform write actions. + +## CRUD Requirements +The editor must support: + +- Create event +- Edit event +- Delete event +- Delete one occurrence from a recurring event (exception-based, no series split) + +Behavior requirements: + +- Create/edit use modal overlay forms. +- Save/update/delete actions show status feedback. +- Delete event requires explicit user confirmation. +- Single-occurrence delete requires selecting a specific occurrence key. + +## Event Fields +Supported fields: + +- `title` (required) +- `description` +- `location` +- `category` +- `all_day_event` +- `start_datetime` (required) +- `end_datetime` (required, must be on/after start) +- `repeat_type` +- `repeat_interval` +- `repeat_range_mode` +- `repeat_count` +- `repeat_until` +- Monthly pattern fields: `repeat_nth_mode`, `repeat_nth_day`, `repeat_nth_pos`, `repeat_nth_weekday` + +UI behavior requirements: + +- Start and end use separate date and time controls. +- Date controls use date-picker inputs. +- Time controls hide when `all_day_event` is selected. +- Floating/persistent labels keep field purpose visible. +- Internal event ID is hidden from users. + +## Recurrence Model +Supported `repeat_type` values: + +- `none` +- `daily` +- `weekly` +- `monthly` +- `yearly` + +Supported monthly modes: + +- `day_of_month` (nth day) +- `weekday_of_month` (nth weekday, including `last` via negative position) + +Supported range modes: + +- `none` +- `count` +- `until` + +## Recurrence Preview and Occurrence Selection +- For recurring events, editor shows a 3-month occurrence chooser grid. +- Grid supports previous/next month paging. +- Selecting an occurrence sets the occurrence key used by `Delete Occurrence`. +- Deselect/reselect behavior must be supported. +- For `repeat_type=none`, occurrence controls remain hidden and delete-occurrence is disabled. + +## Verification Requirements +Acceptance should verify: + +- Approved/authenticated users can complete full CRUD. +- Recurring events render selectable occurrences in preview grid. +- Deleting one occurrence creates an exception while preserving the series. +- Monthly ordinal patterns save and reload correctly. +- Validation blocks invalid start/end ranges. diff --git a/requirements/environment.md b/requirements/environment.md new file mode 100644 index 0000000..c48e956 --- /dev/null +++ b/requirements/environment.md @@ -0,0 +1,131 @@ +# Development Environment + +## Purpose +Define the standard development environment for the WordPress calendar plugin so that development, testing, packaging, and deployment are consistent across contributors. + +## Repository Layout +The repository is organized into the following top-level folders: + +- `requirements/`: Functional and non-functional requirements. +- `docs/`: Documentation (test execution, installation, data structures, workflows). +- `code/`: Plugin source code. +- `compatibility-layer/`: Local WordPress emulation and stand-alone harness/shim code for tests. +- `tests/`: Automated and manual test assets. +- `package/`: Build outputs and distributable plugin package artifacts. + +Note: the project convention may refer to `documentation/`; in this workspace the equivalent directory is `docs/`. + +## Supported Development Platforms +Developers should use one of: + +- Linux (preferred) +- macOS + +Windows is acceptable when using WSL2 with a Linux userland. + +## Core Tooling +Minimum expected tools: + +- `bash` shell +- `ssh` client for remote test server access +- `zip` (or equivalent) for plugin packaging +- `rsync` or `scp` for deployment transfer +- Subversion (`svn`) and/or Git as required by the hosting/deployment workflow + +If project scripts introduce additional dependencies (for example PHP CLI, Composer, Node.js, or WP-CLI), those requirements must be documented in `docs/` and kept aligned with this environment spec. + +## Runtime Targets +Development and verification occur against two runtime targets: + +1. Local compatibility harness (`compatibility-layer/`) +2. Remote WordPress test server (via SSH) + +Production deployment is by packaged plugin artifact, not by direct source sync from `code/`. + +Strict runtime/code separation requirements are defined in `requirements/architecture.md`. + +## Local Development Workflow Expectations +- Implement plugin changes in `code/`. +- Keep requirements updates in `requirements/`. +- Add or update tests in `tests/`. +- Validate behavior first in the local compatibility harness (`compatibility-layer/`). +- Build distributable package into `package/`. + +## Local Compatibility Harness Environment +The local harness in `compatibility-layer/` must emulate the WordPress functions/APIs needed by the plugin. + +Environment expectations: + +- Deterministic startup and teardown for repeatable tests. +- Isolated test data/state between runs. +- Ability to run plugin unit/integration-style tests without external network dependencies unless explicitly required. +- Clear commands documented in `docs/` for: + - harness startup + - test execution + - harness reset/cleanup + +## Data Access Boundaries (Compatibility Harness and Tests) +When code under `code/` is exercised via the compatibility harness, data access must remain within approved WordPress mechanisms. + +Requirements: + +- Plugin code must access persistent data through WordPress-supported mechanisms (for example `$wpdb`, WordPress options/settings APIs, and documented plugin tables via WordPress DB access patterns). +- Plugin code must not bypass WordPress mechanisms by directly accessing external/non-WordPress data stores during compatibility-harness execution. +- Compatibility-harness and test runs must not depend on or read data outside the harness-managed test database/state. +- Any integration that requires external data access must be explicitly documented and excluded from default regression runs unless intentionally enabled. + +## Remote Test Server Environment +Remote test validation is performed over SSH to a dedicated test environment. + +Environment expectations: + +- SSH access is key-based and restricted to authorized developers. +- Test server WordPress and PHP versions should mirror production as closely as practical. +- Deployment to test should use packaged plugin artifacts or an equivalent controlled sync process. +- Test data handling must avoid accidental production data access. + +Required documentation in `docs/`: + +- Host access method +- Deployment command(s) +- Rollback approach +- Post-deploy smoke test checklist + +## Packaging and Deployment Environment +Production release uses a packaged plugin artifact produced from repository sources. + +Detailed remote deployment and exact-match validation requirements are defined in `requirements/deployment.md`. + +Packaging expectations: + +- Package is generated in `package/` with versioned naming. +- Artifact contains only required plugin files. +- Development-only files (temporary files, compatibility-layer artifacts, etc.) are excluded. +- Package integrity is validated before deployment. + +Deployment expectations: + +- Upload/install package on production server via approved operational process. +- Record deployed version and deployment timestamp. +- Maintain a rollback artifact for the previous known-good release. + +## Configuration and Secrets +- Do not commit secrets to source control. +- SSH credentials/keys, server addresses, and environment-specific configuration must be stored outside the repo (or in approved secret management). +- Local and remote environment variables should be documented by name and purpose in `docs/` without exposing secret values. +- Compatibility-harness email workflows use SMTP variables sourced from `credentials/.env` (or process env): +- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USE_TLS`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_FROM`, `SMTP_ADMIN_TO` + +## Quality Gates +Before packaging for test or production: + +- Requirements impacted by change are updated. +- Relevant tests in `tests/` pass in local compatibility harness. +- Remote test server smoke checks pass. +- Package contents are verified. + +## Change Control +Any material change to tools, runtime versions, harness behavior, or deployment process must update: + +- this file (`requirements/environment.md`) +- corresponding operational docs in `docs/` diff --git a/requirements/error_model.md b/requirements/error_model.md new file mode 100644 index 0000000..dff9a54 --- /dev/null +++ b/requirements/error_model.md @@ -0,0 +1,73 @@ +# Error Model Requirements + +## Purpose +Define a consistent error taxonomy and response structure across admin UI, API, CalDAV, and ICS surfaces. + +## Scope +This document covers: + +- Error categories and stable codes +- API/CalDAV/ICS status behavior +- UI-facing error messaging principles +- Logging expectations for failures + +## Error Categories +- `validation_error` +- `authentication_error` +- `authorization_error` +- `not_found` +- `conflict_error` +- `precondition_failed` +- `rate_limited` +- `integration_error` +- `internal_error` + +## API Error Contract +JSON errors must include: + +- `error.code` (stable machine identifier) +- `error.message` (safe human message) +- `error.details` (optional object/list with field-level data) +- `error.request_id` (optional correlation id) + +Status mapping: + +- `400` malformed payload/query +- `401` unauthenticated +- `403` unauthorized +- `404` not found +- `409` conflict +- `412` precondition failed +- `422` validation error +- `429` rate-limited +- `500` internal error + +## CalDAV/WebDAV Error Behavior +- Use standards-appropriate HTTP status codes and DAV error bodies. +- On multistatus operations, each href must report accurate per-resource status. +- Precondition failures must be explicit for conditional writes. + +## ICS Error Behavior +- Non-success ICS responses should return clear status (`4xx/5xx`) and not malformed calendar text. +- Internal diagnostics go to logs, not to public response bodies. + +## Admin UI Error Messaging +- Messages must be actionable and non-technical for end users where possible. +- Field-level validation errors must indicate affected field and remedy. +- Authentication/authorization failures should avoid revealing sensitive internals. + +## Retry Guidance +- Transient errors (rate limit, temporary integration failure) should indicate retry expectation. +- Permanent validation errors should not be presented as retriable without change. + +## Logging Requirements +- All `5xx` and security-relevant `4xx` failures must be logged. +- Logs should include actor, endpoint/action, status, timestamp, and request id. +- Logs must not include plaintext secrets/tokens/passwords. + +## Verification Requirements +Acceptance should verify: + +- Error payload shape is consistent for API endpoints. +- CalDAV and ICS failures return standards-appropriate statuses. +- UI displays user-safe messages while logs retain troubleshooting detail. diff --git a/requirements/ics.md b/requirements/ics.md new file mode 100644 index 0000000..d07fbde --- /dev/null +++ b/requirements/ics.md @@ -0,0 +1,96 @@ +# ICS Export Requirements + +## Purpose +Define requirements for generating iCalendar (.ics) output from plugin calendar data. + +## Standards and RFC +ICS output must comply with the iCalendar specification: + +- RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar) + +Where additional iCalendar properties are used beyond RFC 5545 core, they must be documented and standards-compatible. + +## Timezone Assumption +Unless explicitly overridden by a future requirement, all plugin dates and times are assumed to be in the `Europe/London` timezone. + +## Scope +This document covers: + +- ICS feed/file generation +- Mapping from plugin event data to iCalendar properties +- Recurrence representation +- Validation and interoperability expectations + +## Output Requirements +- Content type must be `text/calendar`. +- Character encoding must be UTF-8. +- Calendar payload must include required VCALENDAR envelope properties. +- Output must be consumable by common calendar clients (Apple, Google, Outlook-class clients). + +## VCALENDAR Requirements +The ICS output must include, at minimum: + +- `BEGIN:VCALENDAR` +- `VERSION:2.0` +- `PRODID` identifying the plugin/system +- `CALSCALE:GREGORIAN` (recommended default) +- `VTIMEZONE` definition for `Europe/London` +- `END:VCALENDAR` + +## VEVENT Mapping +Each plugin calendar entry maps to at least one `VEVENT`. + +Field mapping requirements: + +- Plugin `title` -> `SUMMARY` +- Plugin `description` -> `DESCRIPTION` +- Plugin `location` -> `LOCATION` +- Plugin start date/time -> `DTSTART` +- Plugin end date/time -> `DTEND` +- Plugin category -> `CATEGORIES` +- Stable unique event identifier -> `UID` +- Last update timestamp -> `DTSTAMP` + +Additional mapping guidance: + +- All-day events use date-based `DTSTART`/`DTEND` semantics per RFC 5545. +- Timed events must use `DTSTART;TZID=Europe/London` and `DTEND;TZID=Europe/London`. +- `DTEND` must represent a valid end boundary and not precede `DTSTART`. + +## Recurrence Mapping +Recurring entries should be represented with `RRULE` where possible. + +Mapping expectations: + +- Daily/weekly/monthly/yearly repeat -> corresponding `FREQ` values. +- Custom interval (`every n`) -> `INTERVAL=n`. +- Range modes: + - No end date -> RRULE without `UNTIL` or `COUNT`. + - Create `` appointments -> RRULE with `COUNT=n`. + - Repeat until `` -> RRULE with `UNTIL=`. + +If specific recurrence shapes cannot be represented in a single RRULE, plugin may emit standards-compliant expanded VEVENT instances as fallback, documented in implementation notes. + +## Timezone Requirements +- ICS generation must use a deterministic timezone strategy documented in settings/docs. +- Current project baseline timezone is `Europe/London` unless explicitly overridden by a future requirement. +- For the baseline configuration, ICS output must include a `VTIMEZONE` block describing `Europe/London`. +- For the baseline configuration, timed `DTSTART` and `DTEND` values must use `TZID=Europe/London`. +- If a future configurable timezone mode is introduced, output must remain standards-compliant and subscriber-coherent. + +## Data Quality and Escaping +- Text values must be escaped/formatted according to iCalendar rules (e.g., commas, semicolons, line folding). +- Invalid or incomplete records must not produce malformed ICS output. +- Generation errors should fail safely with observable error handling. + +## Security and Privacy +- Export must include only events intended for public/user-facing distribution. +- Internal/admin-only metadata must not leak into ICS properties. + +## Verification Requirements +Acceptance should verify: + +- Output validates as RFC 5545-compatible ICS. +- Field mappings are correct for single and recurring events. +- All-day and timed events render correctly in major clients. +- Recurrence limits (`COUNT`/`UNTIL`) behave as configured. diff --git a/requirements/ics_endpoint.md b/requirements/ics_endpoint.md new file mode 100644 index 0000000..b252bbf --- /dev/null +++ b/requirements/ics_endpoint.md @@ -0,0 +1,61 @@ +# ICS Endpoint Requirements + +## Purpose +Define the ICS endpoint path, visibility model, caching behavior, and synchronization expectations. + +## Scope +This document covers: + +- ICS endpoint routing and response contract +- Public vs authenticated access policy +- Caching/refresh behavior +- Multi-calendar considerations + +## Normative Boundaries +- This document is authoritative for ICS endpoint path/access/cache behavior. +- ICS payload formatting and field mapping are defined in `requirements/ics.md`. +- Recurrence exception export behavior is defined in `requirements/recurrence_exceptions.md`. +- Authorization policy integration is defined in `requirements/authorization.md`. +- Error normalization is defined in `requirements/error_model.md`. + +## Endpoint Contract +- Default endpoint path: `/calendar.ics` +- If `url_slug` is configured in setup, canonical endpoint path is `//calendar.ics`. +- Alternative implementation path may be used if documented; one canonical URL must be exposed in UI. +- Response content type: `text/calendar; charset=utf-8` + +## Access Policy +- Access mode is configurable in `Setup`: + - `public_read`: endpoint accessible without auth, only public events included + - `authenticated_read`: endpoint requires authenticated access +- Default mode: `public_read` unless overridden by policy requirements. + +## Data Scope +- ICS output includes only events intended for the selected endpoint audience. +- Internal/admin-only metadata must never appear in output. +- Recurrence and exception semantics follow `requirements/ics.md` and `requirements/recurrence_exceptions.md`. + +## Stability and Subscription +- Endpoint URL should remain stable for long-lived subscriptions. +- If URL must change, documented migration/redirection behavior is required. + +## Caching and Freshness +- Responses should include deterministic cache headers. +- `ETag` and/or `Last-Modified` should be emitted where practical. +- Cache policy must balance freshness and performance for subscriber clients. + +## Failure Behavior +- On generation failure, return a clear HTTP error status and log details. +- Endpoint must not return malformed partial ICS payload. + +## Multi-Calendar Support +- First pass supports single canonical calendar feed. +- If multiple feeds are added later, each feed requires stable URL and explicit audience rules. + +## Verification Requirements +Acceptance should verify: + +- Canonical ICS URL is discoverable from web UI. +- Access policy mode is enforced correctly. +- Payload is valid and subscriber-compatible. +- ETag/Last-Modified behavior supports efficient refresh checks. diff --git a/requirements/life_cycle.md b/requirements/life_cycle.md new file mode 100644 index 0000000..160d87d --- /dev/null +++ b/requirements/life_cycle.md @@ -0,0 +1,130 @@ +# Plugin Life Cycle (Production) + +## Purpose +Define the production-server lifecycle for the calendar plugin, including installation, configuration, ongoing data maintenance, and uninstallation behavior. + +## Scope +This requirement applies to: + +- First-time installation on production +- Plugin upgrades on production +- Operational maintenance of plugin-owned data +- Uninstallation from production + +## Preconditions +Before installation: + +- The production WordPress instance is healthy and backed up. +- The plugin package artifact has been built and verified. +- Deployment user has appropriate WordPress admin permissions. +- Database user configured for WordPress has permission to create/alter/drop plugin tables. + +## Installation (Production) +Installation must be performed using the packaged plugin artifact through approved production procedures. + +Required behavior on activation: + +- Register plugin activation logic using WordPress plugin lifecycle hooks. +- Create/update required plugin database schema using WordPress APIs only. +- Initialize plugin default options/settings in WordPress options storage. +- Record plugin schema/version metadata for future migrations. + +## Database Table Creation (WP APIs Only) +Plugin-owned tables must be created and migrated via WordPress APIs only; direct shell/database tooling is not part of normal runtime lifecycle. + +Requirements: + +- Use `$wpdb` for table naming with WordPress table prefix support. +- Use `dbDelta()` (from WordPress upgrade API) for table creation and compatible schema updates. +- Use WordPress charset/collation helpers (e.g., `$wpdb->get_charset_collate()`). +- Version schema changes using a stored option (for example, plugin schema version option). +- Table creation/migration must be idempotent and safe to run multiple times. + +Non-requirements: + +- No raw CLI SQL execution as part of plugin activation/deactivation lifecycle. + +## Server Configuration and Settings +On production install, plugin settings must be configurable in WordPress admin and persisted using WordPress settings/options APIs. + +Requirements: + +- Register settings via WordPress Settings API. +- Validate and sanitize all user-provided settings before persistence. +- Apply secure defaults during first activation. +- Restrict configuration UI/actions to authorized WordPress capabilities. +- Document required settings and operational defaults in project docs. + +## Data Maintenance on Server +The plugin must maintain its production data safely across normal operation and upgrades. + +Requirements: + +- Maintain backward-compatible migrations for schema evolution where practical. +- Preserve data on plugin upgrades by default. +- Avoid destructive data operations during activation/deactivation unless explicitly required by migration logic. +- Provide routine cleanup/retention behavior only through explicit plugin logic (e.g., scheduled cleanup), not implicit uninstall behavior. +- Log or surface migration/maintenance failures via WordPress-compatible error/reporting paths. + +## Upgrade Lifecycle +On plugin update: + +- Activation/upgrade routine checks stored plugin/schema version. +- Required migrations run in deterministic order. +- Migration completion updates stored schema/version markers. +- Existing user settings are preserved unless a documented migration transforms them. + +## Uninstallation (Production) +Uninstall must support two outcomes: + +1. Remove plugin code but keep plugin database data. +2. Remove plugin code and remove plugin database data. + +Uninstall behavior must be explicit and predictable. + +### Data Retention Option +Requirements: + +- Provide an administrator-controlled setting (or equivalent explicit control) named clearly for data retention on uninstall. +- Default behavior should be conservative: keep data unless administrator explicitly chooses removal. +- The retention setting must be read during uninstall execution. + +### Uninstall Execution +Requirements: + +- Implement uninstall logic using WordPress uninstall mechanisms (`uninstall.php` and/or uninstall hook). +- Always remove plugin options/settings that are not required after uninstall when full removal is selected. +- If “keep data” is selected: + - Leave plugin-owned database tables and business data intact. + - Remove only runtime/transient/cache entries where appropriate. +- If “remove data” is selected: + - Drop plugin-owned tables via WordPress database APIs (`$wpdb`), respecting table prefixes. + - Delete plugin-owned options, metadata, and scheduled tasks. + - Ensure cleanup is scoped only to this plugin’s data. + +## Safety and Recovery +Requirements: + +- Uninstall must never affect WordPress core tables or other plugins’ data. +- Destructive cleanup must require explicit administrator intent through the retention setting. +- Operational runbooks should include pre-uninstall backup guidance. +- If uninstall cleanup partially fails, failures must be detectable (admin notice/log entry) for manual remediation. + +## Verification Requirements +Production lifecycle acceptance should verify: + +- Fresh install creates required tables/options. +- Re-activation is idempotent. +- Upgrade runs required migrations without data loss. +- Uninstall with “keep data” preserves plugin tables/data. +- Uninstall with “remove data” removes plugin-owned tables/options/scheduled tasks. + +## Documentation Requirements +The following must be documented in project docs: + +- Production installation steps +- Required server permissions +- Configuration options and defaults +- Upgrade/migration behavior +- Uninstall procedure with keep/remove data choice +- Recovery procedure for failed migration or uninstall cleanup diff --git a/requirements/local-database.md b/requirements/local-database.md new file mode 100644 index 0000000..e46d52e --- /dev/null +++ b/requirements/local-database.md @@ -0,0 +1,86 @@ +# Local Database Requirements (Testing) + +## Purpose +Define local database requirements for reliable, repeatable testing of the calendar plugin. + +## Scope +This document applies to local development and automated test execution using the local compatibility harness. + +It covers: + +- Local test database setup +- Schema creation and migration behavior +- Test data seeding and isolation +- Reset/cleanup requirements between test runs + +## Environment Assumptions +- Database runs locally or in a local containerized service. +- Test database is separate from production/staging databases. +- All plugin date/time values are treated as `Europe/London` unless a test explicitly validates timezone conversion behavior. + +## Database Access Requirements +- Tests must connect using non-production credentials. +- Credentials are provided via local environment configuration (e.g., `credentials/.env`) and must not be hardcoded in source. +- Test runner/harness must fail fast with clear errors if DB connectivity is unavailable. + +## Schema Requirements +- Plugin schema must be created using WordPress APIs used in runtime lifecycle (`$wpdb`, `dbDelta()`), not ad-hoc raw SQL scripts as the primary path. +- Schema initialization for tests must be idempotent. +- Schema versioning must be testable (fresh install and upgrade path). + +## Required Data Coverage +Seed test data must include: + +- Single non-recurring event +- All-day event +- Daily recurrence +- Weekly recurrence +- Monthly recurrence +- Yearly recurrence +- Custom recurrence with: + - No end date + - Count-limited (`create appointments`) + - Date-limited (`repeat until `) +- Edge cases: + - Month-end boundaries + - Leap-year behavior + - DST boundary behavior for `Europe/London` + +## Isolation and Determinism +- Each test run must be deterministic. +- Test cases must not depend on leftover data from previous runs. +- Harness must provide one of: + - Full DB reset before suite/test group, or + - Transactional rollback strategy. + +## Reset and Cleanup Requirements +- A documented command/process must reset local test DB to a known baseline. +- Cleanup must remove plugin test data and temporary artifacts. +- Cleanup must not affect non-test databases. + +## Performance Expectations +- Local test database setup + seed should complete fast enough for iterative development. +- Query performance should be sufficient for day/week/month/year view tests without timeouts under expected local data volumes. + +## Security Requirements +- No real production data may be used in local tests. +- Sensitive values (passwords, hostnames) must be stored outside committed requirement docs and source. +- Example values in docs must be clearly dummy placeholders. + +## Validation Requirements +Test workflow must validate: + +- Successful schema creation from clean state +- Successful migration from older schema version(s) +- CRUD correctness +- Recurrence expansion correctness +- Correct behavior of uninstall keep/remove data options +- ICS field mapping readiness based on stored data + +## Documentation Requirements +Project docs must describe: + +- Local DB engine/version prerequisites +- Connection settings required for compatibility harness +- Commands for setup, seed, reset, and teardown +- Troubleshooting steps for common DB issues in local testing diff --git a/requirements/observability.md b/requirements/observability.md new file mode 100644 index 0000000..c418041 --- /dev/null +++ b/requirements/observability.md @@ -0,0 +1,78 @@ +# Observability Requirements + +## Purpose +Define logging, auditability, and operational visibility requirements for calendar, user access, API, CalDAV, and ICS behavior. + +## Scope +This document covers: + +- Operational logs +- Audit logs for privileged actions +- Metrics and health signals +- Retention and privacy controls + +## Logging Principles +- Logs must support troubleshooting and security review. +- Log format should be structured where practical. +- Sensitive values must be redacted. +- Payload capture should be bounded and summarized to avoid oversized/noisy traces (for example large HTML responses). + +## Required Operational Logs +Must log: + +- Plugin startup/activation and migration outcomes +- API/CalDAV/ICS request summaries with status and latency +- Error events (`5xx`, auth failures, precondition conflicts where relevant) +- Email workflow outcomes (verification/reset/admin-approval notifications) + +Recommended fields: + +- timestamp +- request id/correlation id +- actor type/id (if available) +- endpoint/action +- status code/outcome +- latency + +## Required Audit Logs +Must audit: + +- Event create/update/delete actions from admin/API +- Single-occurrence delete/edit exception actions +- CalDAV user state transitions (`pending_approval`, `active`) +- Admin approval/removal actions for CalDAV users +- Setup/configuration changes + +Audit entry minimum fields: + +- actor identity +- action +- target identity +- prior state (where applicable) +- resulting state +- timestamp + +## Health and Metrics +At minimum expose/log: + +- request counts by surface (admin/api/caldav/ics) +- error rates by category +- average and p95 latency for key endpoints +- queue/dispatch failures for email workflows + +## Retention and Access +- Log retention duration must be documented in operational docs. +- Access to detailed logs should be restricted to authorized operators/admins. +- Audit logs should be tamper-evident by process and protected from casual deletion. +- Fixture admin diagnostics UI may expose only a recent bounded window (for example last 20 entries) for quick troubleshooting. + +## Privacy and Compliance +- Do not log passwords, token raw values, or full credential headers. +- PII in logs should be minimized to necessary operational scope. + +## Verification Requirements +Acceptance should verify: + +- Required events produce expected logs/audit records. +- Security-sensitive data is redacted. +- Operators can trace a failed user flow end-to-end using request/audit identifiers. diff --git a/requirements/packaging.md b/requirements/packaging.md new file mode 100644 index 0000000..4cb9956 --- /dev/null +++ b/requirements/packaging.md @@ -0,0 +1,100 @@ +# Plugin Packaging Requirements + +## Purpose +Define the required process for creating a production-ready WordPress plugin package artifact from this repository. + +## Scope +This document covers: + +- Preparing package contents from repository sources +- Building a distributable archive +- Validating the archive before deployment + +It does not cover: + +- Production installation steps +- Runtime plugin behavior + +Deployment execution and remote exact-match validation are defined in `requirements/deployment.md`. +Runtime/code separation constraints are defined in `requirements/architecture.md`. + +## Source and Output Locations +- Source code root: `code/` +- Packaging workspace/output: `package/` +- Tests used for pre-package validation: `tests/` + +## Packaging Preconditions +Before creating a package: + +- Relevant tests pass in the local compatibility harness. +- Plugin version for release is set in plugin metadata/files. +- Working tree contains intended release changes only. +- Required build tools (`zip` or equivalent) are available. + +## Package Content Rules +The package must include only files required to run the plugin in WordPress. + +Required inclusions: + +- Main plugin file and all runtime PHP source under `code/` +- Runtime assets (CSS/JS/images) required by plugin features +- Any required vendor/runtime dependencies needed in production +- License/readme files required by project policy + +Required exclusions: + +- `tests/`, `compatibility-layer/`, and local development-only assets +- Temporary files, editor files, OS metadata files +- Build scripts and internal notes not needed at runtime +- Secrets, credentials, or environment-specific private data + +## Package Structure Requirements +- The archive must expand into a single plugin directory. +- The plugin directory name must be stable and suitable for WordPress plugin installation. +- Directory structure inside the package must preserve runtime-relative paths expected by plugin code. + +## Build Procedure Requirements +Packaging process must be deterministic and repeatable. + +Required steps: + +1. Create/clean a staging folder under `package/`. +2. Copy approved runtime files from `code/` into staging. +3. Apply exclusion rules to remove non-runtime artifacts. +4. Create a versioned zip archive in `package/`. +5. Record artifact name and version in release notes/changelog. + +Artifact naming requirement: + +- Use a versioned filename pattern, for example: `-.zip` + +## Validation Requirements +Before the artifact is accepted: + +- Archive can be opened successfully. +- Archive root contains exactly one plugin folder. +- Main plugin entry file exists at expected location. +- No excluded directories/files are present. +- No files from `compatibility-layer/` are present. +- Plugin activates successfully in local compatibility-harness/test environment. + +## Integrity and Traceability +- Each package build must be traceable to a source revision/tag. +- Build date/time and source revision should be recorded with the artifact. +- Rebuilding from the same revision should produce functionally equivalent contents. + +## Failure Handling +If packaging fails validation: + +- Artifact must not be promoted to test or production. +- Failures must be documented with cause and remediation. +- Packaging is re-run only after corrective changes are applied. + +## Documentation Requirements +Project docs must include: + +- Exact packaging command(s) used +- Exclusion/inclusion rules +- Artifact naming convention +- Validation checklist +- Location of produced archives diff --git a/requirements/plugin_description.md b/requirements/plugin_description.md new file mode 100644 index 0000000..bfc46e1 --- /dev/null +++ b/requirements/plugin_description.md @@ -0,0 +1,13 @@ +# Plugin Description Requirement + +## Purpose +Define the WordPress plugin Description text shown on the installed plugins screen. + +## Required Description Text +The plugin description must clearly summarize the feature set in about 50 words: + +"Provides a single shared calendar for WordPress with public display, authenticated event editing, user approval workflow, ICS publishing, and CalDAV read/write sync. Supports recurring events, single-occurrence exceptions, admin setup and diagnostics pages, and shortcode rendering for full calendar and upcoming-events sidebar views." + +## Acceptance +- The description appears in the plugin header metadata (`Description:`) and is visible in `wp-admin` Plugins list. +- The text remains concise (target ~50 words) and reflects current capabilities. diff --git a/requirements/recurrence_exceptions.md b/requirements/recurrence_exceptions.md new file mode 100644 index 0000000..bacce61 --- /dev/null +++ b/requirements/recurrence_exceptions.md @@ -0,0 +1,76 @@ +# Recurrence Exception Requirements + +## Purpose +Define normative behavior for recurring-event exception operations across admin UI, API, CalDAV, and ICS. + +## Scope +This document covers: + +- Single-occurrence delete and edit semantics +- Exception storage model +- Merge/precedence rules for recurrence expansion +- Cross-surface consistency requirements + +## Core Principle +- A recurring event is one logical series keyed by stable series identity (`event_id`/`UID`). +- Exception operations modify occurrence behavior within that series. +- Exception operations must not split one series into multiple independent series unless explicitly invoked by a future "split series" feature. + +## Exception Types +- `deleted_occurrence`: one generated occurrence is suppressed. +- `override_occurrence`: one generated occurrence is replaced with modified instance values. + +## Delete-One-Occurrence Semantics +When a user deletes one occurrence of a recurring series: + +- Add `deleted_occurrence` exception for the target occurrence key. +- Keep original series RRULE/repeat definition unchanged. +- Do not create a second series record. +- Future edits to series must still consider existing exceptions. + +## Edit-One-Occurrence Semantics +When a user edits one occurrence in a recurring series: + +- Add/update `override_occurrence` exception for the target occurrence key. +- Override payload contains changed fields for that occurrence. +- Parent series remains the source for non-overridden fields. + +## Expansion Precedence Rules +Occurrence generation order: + +1. Generate base occurrences from series rule. +2. Remove occurrences matching `deleted_occurrence`. +3. Apply `override_occurrence` records to matching occurrence keys. +4. Return sorted final set for window/query. + +If both delete and override exist for the same occurrence key, the system must reject the conflicting write and require explicit conflict resolution. + +## Validation Rules +- Exception target must correspond to a valid generated occurrence in series context. +- Duplicate exception entries for same occurrence key are not allowed. +- Exception operations on non-recurring events are rejected. + +## CalDAV Representation +- Series remains same `UID`. +- Deleted occurrence represented with `EXDATE` and/or standards-compliant override pattern. +- Overridden occurrence represented with `RECURRENCE-ID` VEVENT override. +- Do not emit split UID sequences for single-occurrence deletes. + +## ICS Representation +- Export includes base RRULE for series. +- Deleted occurrences exported as exceptions. +- Overridden occurrences exported as recurrence-id overrides. +- Consumer synchronization should observe missing/changed occurrence without series duplication. + +## UI/API Behavior +- UI and API must expose "this occurrence only" actions distinctly from "entire series" actions. +- Confirmation messaging must clearly indicate operation scope. +- Audit records must capture actor, series, target occurrence key, and operation type. + +## Verification Requirements +Acceptance should verify: + +- Delete-one-occurrence removes only target occurrence. +- Series remains unified in storage and output. +- Override-one-occurrence updates only target occurrence. +- CalDAV/ICS outputs reflect same exception semantics as admin/API views. diff --git a/requirements/settings.md b/requirements/settings.md new file mode 100644 index 0000000..fc42c55 --- /dev/null +++ b/requirements/settings.md @@ -0,0 +1,67 @@ +# Plugin Settings Requirements + +## Purpose +Define implemented setup/settings and diagnostics behavior for the calendar plugin admin pages. + +## Scope +This document covers: + +- Admin menu structure for plugin management pages +- Setup page controls and persisted settings +- Diagnostics page and diagnostics download behavior + +## Admin Navigation +The plugin must provide a single WordPress admin menu entry with these pages: + +- Top-level menu: `Calendar Plugin` +- Sub-pages: `Setup`, `Users`, `Diagnostics` +- Access capability: `manage_options` + +The top-level duplicate submenu entry should be removed so only the explicit pages appear in navigation. + +## Setup Page +The Setup page must support: + +- Saving plugin settings +- Seeding default events +- Deleting all event and recurrence-exception data + +Setup form controls: + +- `CalDAV/ICS Calendar Name` (`caldav_calendar_name`) +- `URL Slug Prefix` (`url_slug`) +- `Verification Page Path` (`verification_page_path`) +- `Plugin Table Prefix` (`table_prefix`) +- `ICS Access Mode` (`ics_access_mode`: `public_read` or `authenticated_read`) +- `Diagnostics Enabled` (`diagnostics_enabled`) +- `Uninstall Cleanup` (`uninstall_cleanup_mode`: `keep` or `remove`) + +Behavior requirements: + +- Save, seed, and delete-all actions provide success/failure messaging on the Setup page. +- Setup POST actions are nonce-protected. +- `table_prefix` updates rename plugin tables (events, exceptions, users, tokens, audit log) without dropping existing data. +- Prefix changes must fail safely if target tables already exist. +- `url_slug` affects canonical routed paths for ICS and CalDAV endpoints. + +## Settings Persistence +- Settings are persisted through plugin options storage and returned by the admin settings API (`/wp-json/calendar/v1/settings`). +- Runtime endpoints and UI links must reflect the latest saved settings. + +## Diagnostics Page +The Diagnostics page must be admin-only and show: + +- Whether diagnostics are enabled +- Runtime metadata snapshot (generation time, endpoint paths, user context, table stem) +- Recent audit log rows when diagnostics are enabled + +Diagnostics download behavior: + +- Download action is available only when `diagnostics_enabled=1` +- Download uses a nonce-protected admin-post action +- Response is JSON attachment (`calendar-diagnostics-*.json`) + +## Security and Access +- Setup, Users, and Diagnostics pages require `manage_options`. +- Form actions must validate WordPress nonces. +- Inputs must be sanitized/normalized before persistence. diff --git a/requirements/template.md b/requirements/template.md new file mode 100644 index 0000000..4cc59bf --- /dev/null +++ b/requirements/template.md @@ -0,0 +1,125 @@ +# + +## Purpose +Describe why this requirement document exists and what decision or behavior it defines. + +## Scope +Define what is included and excluded. + +Included: + +- +- + +Excluded: + +- +- + +## Definitions +Define project-specific terms used in this document. + +- ``: +- ``: + +## Stakeholders +List roles responsible for implementation, approval, and operations. + +- Product/Owner: +- Engineering: +- Operations: +- QA/Test: + +## Preconditions +State assumptions that must be true before this requirement applies. + +- +- + +## Requirements +List clear, testable requirements. + +### Functional Requirements +- FR-001: +- FR-002: + +### Non-Functional Requirements +- NFR-001: +- NFR-002: + +### Constraints +- C-001: +- C-002: + +## Data Requirements +Describe data model, persistence, retention, and migration expectations. + +- +- + +## Configuration Requirements +Describe configurable settings, defaults, validation, and permissions. + +- +- + +## Error Handling and Recovery +Define expected failure behavior and recovery paths. + +- +- + +## Security and Access +Define capability checks, secret handling, and least-privilege requirements. + +- +- + +## Operational Requirements +Describe deployment, monitoring, maintenance, and rollback expectations. + +- +- + +## Verification and Acceptance +Define how compliance will be validated. + +### Test Cases +- TC-001: +- TC-002: + +### Acceptance Criteria +- AC-001: +- AC-002: + +## Dependencies +List dependencies on systems, services, plugins, APIs, or teams. + +- +- + +## Risks and Mitigations +Document known risks and planned mitigations. + +- Risk: + Mitigation: +- Risk: + Mitigation: + +## Open Questions +Track unresolved decisions. + +- OQ-001: +- OQ-002: + +## Change Log +Record meaningful revisions to the requirement. + +- - - +- - - + +## References +Link related requirement, documentation, test, or implementation files. + +- +- diff --git a/requirements/test_strategy.md b/requirements/test_strategy.md new file mode 100644 index 0000000..e0c04fe --- /dev/null +++ b/requirements/test_strategy.md @@ -0,0 +1,84 @@ +# Test Strategy Requirements + +## Purpose +Define mandatory test execution policy, suite composition, and quality gates for preventing regressions. + +## Scope +This document covers: + +- Test levels and ownership +- Required suites by pipeline stage +- Compatibility-harness isolation/reset strategy +- Pass/fail and release gates + +## Test Levels +- Unit tests: pure logic and transformation behavior +- Integration tests: plugin + compatibility-harness DB/APIs +- End-to-end tests: UI/API/CalDAV/ICS end-to-end flows +- Smoke tests: fast high-signal regression checks + +Reference documents: + +- `tests/calendar_entries.md` +- `tests/e2e_test_cases.md` +- `tests/api_test_cases.md` +- `tests/smoke_tests.md` +- `tests/lifecycle_test_cases.md` +- `tests/user_ui_caldav_1hr_spec.md` +- `requirements/architecture.md` + +## Execution Policy + +### Per Commit / PR +Must run: + +- lint/static checks (when available) +- smoke suite +- changed-area unit/integration tests + +### Nightly +Must run: + +- full unit + integration suite +- full E2E suite +- full API suite (if API enabled) +- CalDAV and ICS interoperability checks + +### Pre-Release +Must run: + +- full nightly suite +- upgrade/migration tests +- rollback sanity checks +- packaging/install/uninstall validation +- 60-minute user functional sweep (UI + CalDAV) using `tests/user_ui_caldav_1hr_spec.md` + +## Harness Determinism +- Tests must run against isolated compatibility-harness-managed state. +- Reset to known baseline before suite groups. +- No test may depend on leftover state from prior runs. +- Default CI runs must not depend on external systems. + +## Flake Management +- Flaky tests must be tagged and tracked with issue references. +- Repeatedly flaky tests cannot remain in required gates without mitigation. + +## Coverage Expectations +- CRUD flows for single and recurring events are mandatory coverage. +- Recurrence exception behavior (single occurrence delete without split) is mandatory coverage. +- Authn/Authz paths for admin, API, and CalDAV roles are mandatory coverage. +- Error-model contract coverage is mandatory for API endpoints. + +## Pass/Fail Gates +- Any required suite failure blocks merge/release as applicable. +- Failures must capture logs/artifacts sufficient for debugging. + +## Reporting +- CI should publish suite summary with: + - passed/failed/skipped counts + - runtime + - links to failure artifacts + +## Change Management +- New features must include updated tests and requirement-traceable cases. +- Requirement updates that change behavior must update corresponding test docs and automation. diff --git a/requirements/web-ui.md b/requirements/web-ui.md new file mode 100644 index 0000000..c4f5589 --- /dev/null +++ b/requirements/web-ui.md @@ -0,0 +1,151 @@ +# Web UI Requirements + +## Purpose +Define requirements for the public-facing calendar web UI rendered by the plugin. + +## Scope +This document covers: + +- Shortcode-based rendering of calendar UI content +- View modes for event presentation +- Interactive event create/edit flows in the web UI for authorized users +- Public ICS subscription/export link behavior +- User login/registration/verification entry points used by the calendar page + +## Shortcode Integration +The plugin must export a shortcode that renders the calendar web UI into page/post content. + +Requirements: + +- A plugin shortcode is registered and publicly documented (see `requirements/settings.md`). +- Rendering the shortcode generates HTML output representing calendar data. +- Shortcode output is safe for inclusion in standard WordPress pages/posts. +- Shortcode rendering must gracefully handle empty/no-event states. + +## UI Views +The web UI must support selectable calendar views: + +- Event list view +- Day view +- Week view +- Month view +- Year view + +Behavior requirements: + +- Users can switch between supported views from within the UI. +- View selection updates displayed event data accordingly. +- Current view state is visually clear. +- If no events match the current window/filter, show a clear empty-state message. + +### Graphical View Requirements +- `week` view must render seven day columns. +- Timed events in `week` view must be positioned vertically by start/end time. +- All-day events in `week` view must appear at the top of each day column. +- `week` view must include a visible time scale on the left. +- Overlapping timed events in `week` view must remain readable (side-by-side or equivalent non-obscuring layout). +- `month` view must render a day-of-week grid where each cell represents one date in the month window. +- `month` view cells must list that day’s events in start-time order. +- `year` view must render twelve month blocks in a grid. +- Each year-view month block must render day cells in day-of-week order. +- In `year` view, day numbers with one or more events must be visually emphasized (for example bold text). +- Clicking a day in `year` view must switch to `week` view anchored to a week that includes the selected date. +- In `week` and `month` views, event display text must include start time before title for timed events. + +### Display Filters +- UI must provide a `future dates only` filter control that limits display to today or later. +- In the current UX model, `future dates only` is visible and active only in list mode. + +### Theme Selector +- UI must provide a theme selector with predefined visual themes. +- Theme switching updates shell/panel/list styling without reloading the page. + +### Date Navigation Controls +- UI must provide previous and next navigation controls (left/right arrows) around a `today` control/icon. +- Previous/next controls shift the anchor date by one unit based on current view mode: +- Day/List: +/- 1 day +- Week: +/- 1 week +- Month: +/- 1 month +- Year: +/- 1 year +- UI must expose a single `Date` picker control. +- Arrow and `today` actions update `Date` to the first date of the current period for the selected view. +- Choosing a date must select the enclosing week/month/year period for those views, or the exact day in day view. + +## Data Display Requirements +For each displayed event, UI should present core event information suitable for end users. + +Minimum display requirements: + +- Title +- Start date/time (or all-day presentation) +- End date/time where applicable +- Location (if provided) +- Category (if provided) +- Description excerpt/summary (if configured for display) + +## Web Event Editing UX (Authorized Users) +Where the viewer has write permission, the web UI must support event creation/editing. + +Requirements: + +- Create and edit actions open in an overlay/modal form. +- Start and end are entered with separate date and time controls. +- Date controls must use date-picker inputs. +- Irrelevant recurrence fields must be hidden based on current recurrence selections. +- Form controls use persistent inline/floating labels so field purpose remains visible after input. +- Event ID must not be shown in the UI. +- `location` and `category` should be rendered on one row. +- `start date/time` and `end date/time` should be rendered on one row. +- `repeat type` and `every` interval should be rendered on one row. +- `range mode` and `repeat until` should be rendered on one row. +- Recurrence interval label text uses `Every` and includes a readable interval summary (for example `1 week`, `2 weeks`). +- The readable interval unit text should be integrated into the same interval control rather than a separate display-only field. +- Monthly recurrence must support both `nth day of month` and `nth weekday of month` patterns. +- A `Delete a Single Occurrence` action must be available when editing recurring events. +- `Delete Occurrence` may be shown inline with main modal action buttons. +- Single-occurrence controls appear whenever repeat type is not `none`. +- Single-occurrence controls must remain hidden when repeat type is `none`. +- Single-occurrence selection must be user-friendly via a date list or compact calendar selection. +- The compact selection mode should support a 3-month grid with previous/next navigation. +- The compact selection grid must remain readable in modal layout. + +## Login and Access Modes +- `/calendar` must support two user modes: public (not logged in) and logged-in. +- Public mode is read-only. +- A `Login` action must open an in-page dialog. +- Login dialog must support credential login. +- Login dialog must support registration (registration is a request for approval/write access). +- Login dialog must support email verification token submission. +- Login dialog must support password-reset request initiation. +- Logged-in but non-approved users remain read-only. +- Approved users can perform event CRUD. + +## ICS Link in Web UI +The UI must include a user-visible link to an ICS representation of calendar data. + +Requirements: + +- A link/button is presented in the web UI for calendar subscription/export. +- Link target returns `text/calendar` output suitable for device calendar subscription/import. +- Link should be stable enough for user bookmarking/subscription where feasible. +- Canonical endpoint path and access policy must follow `requirements/ics_endpoint.md`. +- Link behavior is documented for users/admins in project docs. + +## Accessibility and UX +- Controls for view selection must be keyboard-accessible. +- Output should be readable across desktop and mobile screen sizes. +- Time/date presentation should follow site locale/timezone behavior defined by plugin settings. +- Action buttons should be disabled (visibly greyed) when required input/permissions are not present. + +## Security and Performance +- Shortcode rendering must sanitize/escape output appropriately. +- Querying calendar data for UI views should be efficient for expected dataset sizes. +- Public UI endpoints and ICS link handling must avoid leaking non-public admin-only data. + +## Verification Requirements +Acceptance should verify: + +- Shortcode renders without errors on standard WordPress pages. +- All five view modes are selectable and render expected event windows. +- Empty-state behavior is clear and user-friendly. +- ICS link is present and returns valid calendar payload. diff --git a/tests/api_test_cases.md b/tests/api_test_cases.md new file mode 100644 index 0000000..03a977b --- /dev/null +++ b/tests/api_test_cases.md @@ -0,0 +1,170 @@ +# API Test Cases + +## Purpose +Define detailed API tests if an HTTP API is exposed for calendar/admin/caldav-support operations. + +If no API is exposed beyond CalDAV and ICS endpoints, this file remains as the contract for future API introduction and should be marked `not-applicable` in CI. + +## Assumed Endpoint Groups +- Admin calendar entries API (example: `/api/calendar/events`) +- Admin users/access API (example: `/api/calendar/users`) +- ICS endpoint (example: `/calendar.ics` or `/api/calendar/ics`) +- CalDAV endpoint (separately tested in E2E/CalDAV suites) + +Adjust paths to actual implementation while preserving case coverage. + +## Common Assertions +- AuthN/AuthZ enforced for each endpoint. +- Validation errors return structured error payloads. +- Deterministic timezone behavior (`Europe/London` default). +- No sensitive internals leaked in responses. + +## Event API Tests + +### API-EVT-001 Create Valid Event +- Fixture: CE-001 +- Method: `POST /api/calendar/events` +- Assertions: + - `201` created. + - Response includes stable id and canonicalized fields. + - Event visible in admin UI and public UI after create. + +### API-EVT-002 Create Invalid Event +- Payload: missing `title` or end before start +- Assertions: + - `400`/`422` validation failure. + - Actionable field-level errors returned. + - No partial data persisted. + +### API-EVT-002b Unauthorized Event Write +- Method: `POST /wp-json/calendar/v1/events` without plugin or WordPress write auth +- Assertions: + - Request is denied (`403`). + - No event record is persisted. + +### API-EVT-003 Read/List with Filters +- Fixtures: CE-002..CE-007 +- Method: `GET /api/calendar/events?from=...&to=...&view=month` +- Assertions: + - Date-window filtering includes expected occurrences. + - Pagination/sorting (if present) stable and documented. + +### API-EVT-004 Update with Concurrency +- Fixture: CE-001 +- Method: `PUT/PATCH /api/calendar/events/{id}` +- Assertions: + - Valid update returns success and modified timestamp/version. + - Stale ETag/version returns precondition/conflict. + - No silent overwrite on concurrent edits. + +### API-EVT-005 Delete Single Event +- Fixture: CE-001 +- Method: `DELETE /api/calendar/events/{id}` +- Assertions: + - Delete succeeds with expected status. + - Event absent from list, UI, and ICS. + +### API-EVT-006 Delete One Recurrence Occurrence +- Fixture: CE-010 +- Method: `DELETE /api/calendar/events/{id}/occurrences/{occurrence_key}` (or equivalent) +- Assertions: + - Operation creates exception record. + - Remaining series unchanged. + - No split-series records created. + +### API-EVT-007 Preview Occurrences (Valid + Invalid) +- Method: `POST /wp-json/calendar/v1/events/preview-occurrences` +- Assertions: + - Valid recurrence payload returns computed preview items in requested window. + - Invalid range (`end < start`) returns `422`. + - Preview endpoint is non-destructive (does not create records). + +### API-EVT-008 Monthly Ordinal Recurrence Parity +- Method: `POST /wp-json/calendar/v1/events` + `GET /events/{id}/occurrences` +- Assertions: + - `repeat_nth_mode=weekday_of_month` with `repeat_nth_pos=-1` and weekday set maps to expected monthly dates. + - Saved ordinal recurrence reloads without drift. + - Series stays single UID-based sequence. + +## User/Access API Tests + +### API-USR-001 Register User +- Method: `POST /api/calendar/users/register` +- Assertions: + - Account created as `pending_approval`. + - Verification email dispatch recorded. + +### API-USR-002 Verify Email +- Method: tokenized verification endpoint +- Assertions: + - Valid token marks email verified while user remains `pending_approval`. + - Reused/expired token fails cleanly. + +### API-USR-003 Password Recovery +- Methods: forgot + reset endpoints +- Assertions: + - Token issued and reset succeeds once. + - Previous sessions/tokens invalidated. + +### API-USR-004 Admin Approve/Remove +- Method: admin-only action endpoint +- Assertions: + - Admin can set `pending_approval`/`active`. + - Admin can remove defunct users. + - Unauthorized user receives denial. + - Audit log record created. + +### API-USR-005 Remove User Revokes Access +- Method: admin `DELETE /wp-json/calendar/v1/admin/users/{id}` then user login/me checks +- Assertions: + - Removed user can no longer authenticate (`401`). + - Removed user sessions/tokens do not continue to authorize requests. + +## ICS Endpoint API Tests + +### API-ICS-001 Basic Response +- Method: `GET /calendar.ics` +- Assertions: + - `200` status, `text/calendar` content type, UTF-8. + - Valid VCALENDAR envelope. + +### API-ICS-002 Recurrence + Exception Mapping +- Fixture: CE-010 with deleted occurrence exception +- Assertions: + - RRULE is present for series. + - Exception for deleted occurrence is exported. + - No split-series artifact in output. + +### API-ICS-003 Slugged Path Link Contract +- Method: set `url_slug`, render shortcode, inspect rendered links +- Assertions: + - ICS link uses `//calendar.ics`. + - CalDAV link uses `//caldav/`. + - Link path changes match configured slug value. + +## Negative and Security Tests + +### API-SEC-001 Unauthorized Access +- Assertions: + - Unauthenticated requests blocked where required. + - No data leakage in error responses. + +### API-SEC-002 Input Fuzz/Injection +- Assertions: + - Script/SQL-like payloads are rejected or safely encoded. + - No server error or malformed persistence. + +### API-SEC-003 Rate Limit Behavior +- Assertions: + - Login/reset/verification endpoints throttle abusive attempts. + +### API-SET-001 Table Prefix Setting +- Method: `GET/PATCH /wp-json/calendar/v1/settings` (admin context) +- Assertions: + - `table_prefix` is visible in setup/admin workflow and defaults to `wp_cs_calendar`. + - Changing table prefix to another valid value triggers table rename migration. + - Existing data remains accessible after prefix change. + +## Execution Cadence +- Run full API suite on PR and nightly builds. +- Run a trimmed API smoke subset on each commit (see `tests/smoke_tests.md`). diff --git a/tests/calendar_entries.md b/tests/calendar_entries.md new file mode 100644 index 0000000..e93faab --- /dev/null +++ b/tests/calendar_entries.md @@ -0,0 +1,130 @@ +# Calendar Entry Test Fixtures + +## Purpose +Define canonical calendar entries used across admin UI, public UI, CalDAV, ICS, and API tests so scenarios are consistent end-to-end. + +## Global Assumptions +- Timezone baseline: `Europe/London` +- Date format: ISO 8601 in docs, converted as needed by UI/API +- IDs/UIDs are stable in test environments + +## Fixture Set + +### CE-001 Single Timed Event +- Title: `Board Meeting` +- Location: `Room A` +- Category: `Governance` +- Start: `2026-04-01T10:00:00+01:00` +- End: `2026-04-01T11:30:00+01:00` +- Repeat: `none` +- Description: `Quarterly board review.` + +### CE-002 All-Day Event +- Title: `Office Closed` +- Location: `HQ` +- Category: `Operations` +- Start: `2026-05-04` (all day) +- End: `2026-05-05` (exclusive end for all-day semantics) +- Repeat: `none` +- Description: `Public holiday closure.` + +### CE-003 Daily Recurrence (Count-Limited) +- Title: `Daily Standup` +- Location: `Online` +- Category: `Team` +- Start: `2026-04-06T09:00:00+01:00` +- End: `2026-04-06T09:15:00+01:00` +- Repeat: `daily` +- Custom interval: `1` +- Range mode: `count` +- Count: `10` +- Description: `15 minute sync.` + +### CE-004 Weekly Recurrence (No End) +- Title: `Community Lunch` +- Location: `Cafeteria` +- Category: `Community` +- Start: `2026-04-08T12:30:00+01:00` +- End: `2026-04-08T13:30:00+01:00` +- Repeat: `weekly` +- Custom interval: `1` +- Range mode: `no_end` +- Description: `Weekly community lunch.` + +### CE-005 Monthly Recurrence (Until Date) +- Title: `Finance Close` +- Location: `Finance Office` +- Category: `Finance` +- Start: `2026-04-30T17:00:00+01:00` +- End: `2026-04-30T18:00:00+01:00` +- Repeat: `monthly` +- Custom interval: `1` +- Range mode: `until` +- Until: `2026-08-31` +- Description: `Month-end close process.` + +### CE-006 Yearly Recurrence +- Title: `Annual Conference` +- Location: `Main Hall` +- Category: `Events` +- Start: `2026-06-15T10:00:00+01:00` +- End: `2026-06-15T17:00:00+01:00` +- Repeat: `yearly` +- Custom interval: `1` +- Range mode: `count` +- Count: `3` +- Description: `Annual community conference.` + +### CE-007 Custom Every 2 Weeks (Until Date) +- Title: `Fortnightly Coaching` +- Location: `Online` +- Category: `Training` +- Start: `2026-04-07T15:00:00+01:00` +- End: `2026-04-07T16:00:00+01:00` +- Repeat: `custom` +- Base unit: `weekly` +- Interval: `2` +- Range mode: `until` +- Until: `2026-07-31` +- Description: `Coaching check-in.` + +### CE-008 DST Boundary Event +- Title: `DST Validation Event` +- Location: `Lab` +- Category: `QA` +- Start: `2026-10-25T00:30:00+01:00` +- End: `2026-10-25T02:30:00+00:00` +- Repeat: `none` +- Description: `Validates DST transition rendering.` + +### CE-009 Leap-Year Annual Event +- Title: `Leap Day Marker` +- Location: `Calendar` +- Category: `QA` +- Start: `2028-02-29T09:00:00+00:00` +- End: `2028-02-29T10:00:00+00:00` +- Repeat: `yearly` +- Custom interval: `1` +- Range mode: `count` +- Count: `3` +- Description: `Leap day recurrence behavior.` + +### CE-010 Recurring Event with Exception Delete +- Title: `Therapy Session` +- Location: `Clinic` +- Category: `Health` +- Start: `2026-04-03T14:00:00+01:00` +- End: `2026-04-03T15:00:00+01:00` +- Repeat: `weekly` +- Custom interval: `1` +- Range mode: `count` +- Count: `8` +- Description: `Used for single-occurrence delete exception tests.` +- Exception target occurrence: `2026-04-17T14:00:00+01:00` (delete this occurrence only) + +## Core Assertions by Fixture +- CE-001/CE-002 validate single-event CRUD, all-day handling, UI display. +- CE-003..CE-007 validate recurrence creation, expansion, update, and persistence. +- CE-008 validates DST render/sync behavior. +- CE-009 validates leap-year recurrence behavior. +- CE-010 validates recurrence exception behavior (no series split). diff --git a/tests/e2e_test_cases.md b/tests/e2e_test_cases.md new file mode 100644 index 0000000..1885ae4 --- /dev/null +++ b/tests/e2e_test_cases.md @@ -0,0 +1,232 @@ +# End-to-End Test Cases + +## Purpose +Define end-to-end scenarios that exercise: +- Admin entry CRUD +- Public/user display +- CalDAV CRUD +- Display/sync behavior from ICS + +This suite uses fixtures in `tests/calendar_entries.md`. + +## Pre-Run Setup +- Reset DB to known baseline. +- Seed users: + - `admin_user` with calendar admin capability + - `pending_user` with CalDAV `pending_approval` + - `rw_user` with CalDAV `active` +- Enable plugin and ensure admin menu with `Users`, `Setup`, `Diagnostics`. + +## Admin Entry CRUD + +### E2E-ADM-001 Create Single Event +- Fixture: CE-001 +- Steps: + 1. Admin logs in. + 2. Open `/calendar` as a write-enabled user. + 3. Create event using CE-001. + 4. Save. +- Assertions: + - Success feedback shown. + - Event appears in admin list. + - Event appears in public list/day/week/month/year views. + +### E2E-ADM-002 Update Single Event +- Fixture: CE-001 +- Steps: + 1. Open CE-001 in editor. + 2. Change title to `Board Meeting (Updated)` and location. + 3. Save. +- Assertions: + - Updated values shown in admin list and public views. + - Last-modified metadata changes. + +### E2E-ADM-003 Delete Single Event +- Fixture: CE-001 +- Steps: + 1. Delete CE-001 from admin list and confirm. +- Assertions: + - Event removed from admin list. + - Event removed from public views. + - Event removed from ICS and CalDAV query results. + +### E2E-ADM-004 Create Recurrence Types +- Fixtures: CE-003, CE-004, CE-005, CE-006, CE-007 +- Steps: + 1. Create each recurring event via editor. + 2. Re-open each and verify saved recurrence config. +- Assertions: + - Persisted values match fixture. + - Preview highlights expected near-term occurrences. + - UI views render expected occurrences. + +### E2E-ADM-005 Delete One Occurrence as Exception +- Fixture: CE-010 +- Steps: + 1. Create CE-010. + 2. Delete only occurrence `2026-04-17T14:00:00+01:00`. + 3. Reload series in editor and public views. +- Assertions: + - Only selected occurrence is removed. + - Series remains a single sequence (not split). + - Exception is represented in sync outputs (CalDAV/ICS rules). + +### E2E-ADM-006 Table Prefix Configuration +- Steps: + 1. Open Setup page. + 2. Verify plugin table prefix defaults to `wp_cs_calendar`. + 3. Change prefix to a valid alternative (staging only) and save. + 4. Reload calendar/admin views. +- Assertions: + - Plugin continues to operate without data loss. + - Event/user records remain available after prefix migration. + +### E2E-ADM-007 Setup Title Removal +- Steps: + 1. Open Setup page. + 2. Inspect available controls. + 3. Render a page containing `[calendar]` with a custom page heading in content. +- Assertions: + - Setup does not show a `Calendar Title` field. + - Calendar shortcode does not inject its own top heading. + - Page/theme heading remains the source of title styling. + +### E2E-ADM-008 User Removal Revokes Access +- Steps: + 1. Register, verify, and approve a plugin user. + 2. Confirm login/me works. + 3. Remove the same user from `Users` admin. +- Assertions: + - Subsequent login and `/users/me` fail for removed user. + - Removed user cannot perform event write actions. + +## Public/User Display + +### E2E-UI-001 Cross-View Rendering +- Fixtures: CE-002..CE-007 +- Steps: + 1. Open public calendar UI. + 2. Switch list/day/week/month/year views. +- Assertions: + - Required fields render correctly (title/time/location/category/description excerpt). + - All-day semantics for CE-002 are correct. + - Empty-state messaging works when date window has no matches. + +### E2E-UI-002 DST and Leap-Year Display +- Fixtures: CE-008, CE-009 +- Steps: + 1. Navigate to date windows covering fixture dates. +- Assertions: + - CE-008 shows expected local times around DST boundary. + - CE-009 recurrence behavior is correct for non-leap years. + +### E2E-UI-003 Calendar Shell Control Presence +- Steps: + 1. Render `[calendar]` shortcode. +- Assertions: + - Login/register/verify/reset controls are present. + - Future-only filter and theme selector controls are present. + - Recurrence occurrence-delete controls are present in editor shell markup. + +## CalDAV CRUD + +### E2E-CDV-001 Auth Gate (Pending User) +- User: `pending_user` +- Fixtures: CE-002, CE-003 +- Steps: + 1. Connect CalDAV client as `pending_user`. +- Assertions: + - Authentication is denied until admin approval. + +### E2E-CDV-002 Create/Update/Delete (Write User) +- User: `rw_user` +- Fixture: CE-001 as CalDAV-created item +- Steps: + 1. Create event via CalDAV `PUT`. + 2. Modify via CalDAV `PUT` with valid ETag precondition. + 3. Delete via CalDAV `DELETE`. +- Assertions: + - Admin UI reflects CalDAV-created and updated data. + - Public UI reflects changes. + - Deletion propagates to admin/public views. + +### E2E-CDV-003 Recurrence Exception Round-Trip +- User: `rw_user` +- Fixture: CE-010 +- Steps: + 1. Create CE-010 via admin or CalDAV. + 2. Delete a single occurrence from CalDAV client. + 3. Sync and inspect in admin editor. +- Assertions: + - Exception is present for deleted occurrence. + - Series stays unified (single UID sequence). + - No split-series artifact created. + +### E2E-CDV-004 CalDAV Single-Occurrence Delete Sync Persistence +- User: `rw_user` +- Fixture: CE-010 +- Steps: + 1. Create recurring event via UI or CalDAV. + 2. In CalDAV client, delete one occurrence only (client emits `EXDATE` and/or cancelled `RECURRENCE-ID` component). + 3. Force sync/refresh in client and fixture UI. + 4. Restart fixture and refresh again. +- Assertions: + - Deleted occurrence remains deleted after sync cycles and restart. + - Persisted exception exists in fixture data store (`recurrence_exceptions`). + - No duplicate recurrence series is created. + +### E2E-CDV-005 Monthly Ordinal Round-Trip +- User: `rw_user` +- Steps: + 1. Create a monthly ordinal recurring event (`weekday_of_month`, `last`, chosen weekday). + 2. Sync via CalDAV and read back object. + 3. Validate resulting occurrence dates in UI/API. +- Assertions: + - Monthly ordinal rule round-trips without changing meaning. + - Expected target dates (for example last Sunday of month) are preserved. + - No split or duplicate series created. + +## ICS Display and Sync + +### E2E-ICS-001 ICS Link Health and Parsing +- Fixtures: CE-002..CE-007 +- Steps: + 1. Fetch public ICS URL. + 2. Parse payload with validator/parser. +- Assertions: + - HTTP content type is `text/calendar`. + - VCALENDAR envelope is valid. + - VEVENT fields map correctly from fixtures. + +### E2E-ICS-002 Recurrence and Exception Export +- Fixture: CE-010 with deleted occurrence exception +- Steps: + 1. Fetch ICS after exception delete. +- Assertions: + - Recurrence is represented with RRULE. + - Deleted occurrence is represented as exception (EXDATE and/or RECURRENCE-ID pattern). + - No split into independent series. + +### E2E-ICS-003 Consumer Sync +- Fixtures: CE-002, CE-003, CE-010 +- Steps: + 1. Subscribe external test calendar to ICS feed. + 2. Update source entries in admin. + 3. Refresh subscriber. +- Assertions: + - Create/update/delete changes propagate. + - Exception delete for CE-010 propagates as missing occurrence in subscriber. + +### E2E-ICS-004 Slug Path Link Consistency +- Steps: + 1. Set `url_slug` in Setup. + 2. Render `[calendar]`. +- Assertions: + - Rendered ICS and CalDAV links use slugged path. + - Links remain navigable and consistent with configured endpoint prefix. + +## Traceability +- Admin CRUD/lifecycle: E2E-ADM-001..008 +- User display/UI shell: E2E-UI-001..003 +- CalDAV CRUD/sync: E2E-CDV-001..005 +- ICS display/sync/pathing: E2E-ICS-001..004 diff --git a/tests/lifecycle_test_cases.md b/tests/lifecycle_test_cases.md new file mode 100644 index 0000000..772553f --- /dev/null +++ b/tests/lifecycle_test_cases.md @@ -0,0 +1,54 @@ +# Plugin Lifecycle Test Cases + +## Purpose +Verify activation, deactivation, and removal semantics, including table cleanup behavior controlled from Setup (`uninstall_cleanup_mode`). + +## Preconditions +- Run on a disposable/staging WordPress environment. +- Plugin package installed but tests may deactivate/remove plugin. +- Record current value of `calendar_plugin_uninstall_cleanup_mode`. + +## LIF-001 First-Time Activation +- Steps: + 1. Ensure plugin is not active. + 2. Activate plugin. + 3. Open Setup page once. +- Assertions: + - Activation succeeds without fatal errors. + - Required plugin tables exist (`*_events`, `*_recurrence_exceptions`, `*_users`, `*_user_tokens`, `*_audit_log`). + - Default setup values exist, including `uninstall_cleanup_mode=keep`. + +## LIF-002 Deactivation (No Data Loss) +- Steps: + 1. Seed at least one event and one user. + 2. Deactivate plugin. + 3. Re-activate plugin. +- Assertions: + - Deactivation does not drop plugin tables. + - Seeded data remains available after re-activation. + +## LIF-003 Removal With Keep Mode +- Steps: + 1. Set Setup -> Uninstall Cleanup = `keep`. + 2. Remove/delete plugin from WordPress. + 3. Re-install and activate plugin. +- Assertions: + - Plugin-owned tables are preserved. + - Existing plugin data remains. + +## LIF-004 Removal With Remove Mode +- Steps: + 1. Set Setup -> Uninstall Cleanup = `remove`. + 2. Remove/delete plugin from WordPress. + 3. Inspect database. +- Assertions: + - Plugin-owned tables are dropped. + - Plugin-owned options are removed. + - No non-plugin tables are affected. + +## LIF-005 Idempotence/Safety +- Steps: + 1. Run remove flow twice in `remove` mode. +- Assertions: + - No fatal errors on repeated cleanup. + - Cleanup remains scoped to plugin-owned objects only. diff --git a/tests/remote_coverage_review.md b/tests/remote_coverage_review.md new file mode 100644 index 0000000..7d06c8b --- /dev/null +++ b/tests/remote_coverage_review.md @@ -0,0 +1,52 @@ +# Remote Coverage Review (2026-03-31, Updated) + +## Scope +Review current API and E2E test definitions for remote-server-first validation and identify additional high-value coverage. + +## Current Coverage Strengths +- Event CRUD and recurrence exception behavior are covered. +- User lifecycle (register, verify, approve, reset) is covered. +- ICS and CalDAV interoperability paths are covered. +- Security smoke items include token leakage, rate limit, password hashing, and CalDAV resource handling. + +## Coverage Gaps Identified + +### API Gaps +- Unauthorized write-path checks were under-specified for event create/update paths. +- Validation-path checks were missing for: + - invalid event time range (`end < start`) + - invalid `preview-occurrences` payloads +- Single-use token behavior needed an explicit regression check for password reset token reuse. +- Monthly ordinal recurrence parity needed executable checks (last weekday-of-month behavior). +- Slug-path link contract in rendered shortcode output needed explicit assertions. +- Post-removal auth revocation for plugin users needed explicit checks. + +### E2E Gaps +- UI shell control presence checks were incomplete (verify/reset/future/theme/occurrence-delete controls). +- User lifecycle e2e missed explicit “remove user -> auth denied” verification. +- Event deletion e2e missed explicit not-found checks for deleted records. + +## New Coverage Added (This Update) + +### Executable Coverage +- `compatibility-layer/e2e_wp_emulation.php` now includes: + - unauthenticated event create denied (`403`) + - invalid event range returns `422` + - preview-occurrences valid + invalid-path checks + - password reset token single-use check (`422` on reuse) + - remove-user revokes login access + - monthly ordinal recurrence date assertions + - shortcode link assertions for `url_slug` pathing +- `compatibility-layer/ui_e2e_wp_emulation.php` now includes: + - UI shell control presence checks (verify/reset/future/theme/occurrence delete) + - deleted-event not-found check + - remove-user auth denial via `/users/me` + +### Test Spec Updates +- `tests/api_test_cases.md` expanded with API-EVT/API-USR/API-ICS additions for the above gaps. +- `tests/e2e_test_cases.md` expanded with E2E-ADM-008, E2E-UI-003, E2E-CDV-005, E2E-ICS-004. +- `tests/ui_e2e_cases.md` updated to reflect new automated UI cases. + +## Remaining High-Value Gaps +- Diagnostics admin-page/download flow still needs a dedicated authenticated admin-browser or WP-cookie harness test. +- Remote runner still does not exercise nonce-protected diagnostics download end-to-end. diff --git a/tests/run_remote_tests.sh b/tests/run_remote_tests.sh new file mode 100755 index 0000000..b9a7ad6 --- /dev/null +++ b/tests/run_remote_tests.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${ROOT_DIR}/credentials/.env" + +if [[ -f "${ENV_FILE}" ]]; then + # shellcheck disable=SC1090 + source "${ENV_FILE}" +fi + +BASE_URL="${BASE_URL:-${WP_URL:-}}" +AUTH_USER="${CAL_TEST_USER:-adrians@chezstephens.org.uk}" +AUTH_PASS="${CAL_TEST_PASSWORD:-brillig1}" +EXPECTED_TABLE_PREFIX="${CAL_TABLE_PREFIX_EXPECTED:-wp_cs_calendar}" +ENABLE_PREFIX_CHECK="${CAL_ENABLE_PREFIX_CHECK:-1}" +REMOTE_WP_PATH="${REMOTE_WP_PATH:-}" +if [[ -z "${REMOTE_WP_PATH}" ]] && [[ -n "${REMOTE_APP_DIR:-}" ]]; then + REMOTE_WP_PATH="$(dirname "$(dirname "${REMOTE_APP_DIR}")")" +fi +if [[ -z "${REMOTE_WP_PATH}" ]]; then + REMOTE_WP_PATH="/var/www/wordpress" +fi +FAILURES=0 +CREATED_EVENT_ID="" +CREATED_REC_EVENT_ID="" + +usage() { + cat <<'TXT' +Remote API+E2E smoke runner for calendar plugin. + +Usage: + tests/run_remote_tests.sh [--base-url URL] [--user EMAIL] [--password PASS] + +Env overrides: + BASE_URL + CAL_TEST_USER + CAL_TEST_PASSWORD + CAL_TABLE_PREFIX_EXPECTED + CAL_ENABLE_PREFIX_CHECK + +Defaults: + BASE_URL -> credentials/.env:WP_URL + user/password -> seeded smoke account +TXT +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --base-url) + BASE_URL="${2:-}" + shift 2 + ;; + --user) + AUTH_USER="${2:-}" + shift 2 + ;; + --password) + AUTH_PASS="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "[remote-tests] unknown argument: $1" >&2 + usage + exit 2 + ;; + esac +done + +if [[ -z "${BASE_URL}" ]]; then + echo "[remote-tests] BASE_URL is required (set WP_URL in credentials/.env or pass --base-url)" >&2 + exit 2 +fi + +BASE_URL="${BASE_URL%/}" + +step() { + printf '\n[remote-tests] %s\n' "$1" +} + +record_fail() { + FAILURES=$((FAILURES + 1)) + echo "[remote-tests] FAIL: $1" >&2 +} + +json_assert() { + local file="$1" + local expr="$2" + python3 - "$file" "$expr" <<'PY' +import json, sys +path, expr = sys.argv[1], sys.argv[2] +data = json.load(open(path)) +safe = {"bool": bool, "isinstance": isinstance, "list": list, "dict": dict, "str": str, "int": int} +ok = eval(expr, {"__builtins__": safe}, {"data": data}) +if not ok: + raise SystemExit(1) +PY +} + +cleanup() { + if [[ -n "${CREATED_EVENT_ID}" ]]; then + curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \ + "${BASE_URL}/wp-json/calendar/v1/events/${CREATED_EVENT_ID}" >/dev/null || true + fi + if [[ -n "${CREATED_REC_EVENT_ID}" ]]; then + curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \ + "${BASE_URL}/wp-json/calendar/v1/events/${CREATED_REC_EVENT_ID}" >/dev/null || true + fi +} +trap cleanup EXIT + +step "health" +if curl -fsS "${BASE_URL}/wp-json/calendar/v1/health" >/tmp/remote_test_health.json; then + if ! json_assert /tmp/remote_test_health.json "data.get('status') == 'ok'"; then + record_fail "health payload invalid" + fi +else + record_fail "health endpoint unreachable" +fi + +if [[ "${ENABLE_PREFIX_CHECK}" == "1" ]] && [[ -n "${REMOTE_HOST:-}" ]] && [[ -n "${REMOTE_USER:-}" ]] && [[ -n "${REMOTE_SSH_KEY_PATH:-}" ]] && [[ -n "${REMOTE_PORT:-}" ]] && [[ -n "${REMOTE_WP_CLI:-}" ]]; then + step "table prefix configuration" + SSH_KEY_PATH="${REMOTE_SSH_KEY_PATH}" + if [[ "${SSH_KEY_PATH}" != /* ]]; then + SSH_KEY_PATH="${ROOT_DIR}/${SSH_KEY_PATH}" + fi + SSH_PREFIX=(ssh -F /dev/null -i "${SSH_KEY_PATH}" -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new "${REMOTE_USER}@${REMOTE_HOST}") + if ! "${SSH_PREFIX[@]}" "echo ok" >/dev/null 2>&1; then + echo "[remote-tests] WARN: SSH unavailable, skipping table prefix check" + else + STEM="$("${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} option get calendar_plugin_table_stem --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)" + if [[ -n "${STEM}" ]] && [[ "${STEM}" != "cs_calendar" ]] && [[ "${STEM}" != "calendar" ]]; then + record_fail "table stem option expected cs_calendar/calendar got '${STEM}'" + fi + TABLE_LIST="$("${SSH_PREFIX[@]}" "cd ${REMOTE_WP_PATH} && ${REMOTE_WP_CLI} db query \"SHOW TABLES;\" --allow-root --silent --skip-column-names 2>/dev/null || true" | tr -d '\r')" + TABLE_EXISTS="$(printf '%s\n' "${TABLE_LIST}" | grep -Fx "${EXPECTED_TABLE_PREFIX}_events" | head -n1 || true)" + LEGACY_EXISTS="$(printf '%s\n' "${TABLE_LIST}" | grep -Fx "wp_calendar_events" | head -n1 || true)" + if [[ "${TABLE_EXISTS}" != "${EXPECTED_TABLE_PREFIX}_events" ]] && [[ "${LEGACY_EXISTS}" != "wp_calendar_events" ]]; then + record_fail "expected table prefix '${EXPECTED_TABLE_PREFIX}' (or legacy wp_calendar) not found" + fi + fi +fi + +step "public read month" +if curl -fsS "${BASE_URL}/wp-json/calendar/v1/public/events?view=month&date=2026-04-01" >/tmp/remote_test_public.json; then + if ! json_assert /tmp/remote_test_public.json "isinstance(data.get('data'), list)"; then + record_fail "public events payload invalid" + fi +else + record_fail "public events endpoint failed" +fi + +step "authenticated /users/me" +if curl -fsS -u "${AUTH_USER}:${AUTH_PASS}" "${BASE_URL}/wp-json/calendar/v1/users/me" >/tmp/remote_test_me.json; then + if ! json_assert /tmp/remote_test_me.json "bool(data.get('data', {}).get('email'))"; then + record_fail "auth user context invalid" + fi +else + record_fail "auth /users/me failed" +fi + +step "event CRUD" +EVENT_UID="remote-api-e2e-$(date +%s)@calendar-plugin" +CREATE_JSON=$(cat <`) and supported component set (`VEVENT`). +- Legacy local harness check is archived at `fixture-tests/fixture_caldav_client_compat_smoke.sh`. + +### SMK-010 Lifecycle Controls (Staging Only) +- Verify Setup exposes `Uninstall Cleanup` setting with: + - `keep` (default) + - `remove` +- Verify deactivation does not remove tables/data. +- Verify uninstall behavior follows selected mode (`keep` preserves tables, `remove` drops plugin-owned tables/options). +- Full destructive flow is defined in `tests/lifecycle_test_cases.md`. + +## Failure Handling +- Any smoke failure blocks merge/deploy. +- Capture artifact bundle: logs, request/response snippets, and failing fixture payload. + +## Suggested Run Schedule +- Per commit: SMK-001..007 +- Pre-release: smoke + full E2E + full API suite + SMK-009 +- Nightly: full E2E + compatibility checks with target CalDAV/ICS clients + +## Remote Runner +- Automated remote API+E2E smoke runner: `./tests/run_remote_tests.sh` +- Optional overrides: + - `./tests/run_remote_tests.sh --base-url https://chezstephens.org.uk --user --password ` diff --git a/tests/ui_e2e_cases.md b/tests/ui_e2e_cases.md new file mode 100644 index 0000000..897e7a3 --- /dev/null +++ b/tests/ui_e2e_cases.md @@ -0,0 +1,23 @@ +# UI E2E Cases + +Focused UI contract checks for the plugin calendar page and related user flows. + +## Scope +- Calendar page controls render (`/calendar`) +- Account actions used by UI (register, login, reset) +- Event CRUD and single-occurrence delete semantics used by UI +- Admin user approval/removal flow that unblocks UI write access + +## Automated Script +- `compatibility-layer/ui_e2e.sh` + +## Cases +1. Calendar page renders login/event controls. +2. Calendar page renders verify/reset/future-only/theme and recurrence-delete controls. +3. Register user and verify email token flow. +4. Admin approves user. +5. Approved user login succeeds via `/users/me`. +6. User creates recurring event. +7. Single occurrence delete removes only selected occurrence. +8. User deletes event and resource becomes not found. +9. Admin removes user and `/users/me` authentication fails afterward. diff --git a/tests/user_ui_caldav_1hr_spec.md b/tests/user_ui_caldav_1hr_spec.md new file mode 100644 index 0000000..8053830 --- /dev/null +++ b/tests/user_ui_caldav_1hr_spec.md @@ -0,0 +1,77 @@ +# User Functional Test Spec (UI + CalDAV, ~1 Hour) + +## Goal +Exercise the highest-value user flows across web UI, admin setup/users pages, recurrence logic, ICS, and CalDAV synchronization within 60 minutes. + +## Preconditions (5 minutes) +- Tester has: + - WordPress admin account + - one approved calendar user account (for UI + CalDAV write tests) + - one pending user account (for approval flow checks) +- Test calendar client available (for example Thunderbird). +- Plugin active and calendar page with `[calendar]` exists. + +## Test Plan + +### 1. Setup Page (8 minutes) +- Open Setup. +- Verify controls are present and usable: + - CalDAV/ICS calendar name + - URL slug + - plugin table prefix + - diagnostics enabled + - uninstall cleanup mode + - buttons: Save Settings, Seed Events, Delete All Events Data +- Click `Seed Events`. +- Confirm success message appears. + +### 2. Users Admin Flow (6 minutes) +- Open Users page. +- Verify pending user appears. +- Approve pending user. +- Remove a defunct test user (if present) and verify list refreshes. + +### 3. Calendar UI Auth + CRUD (12 minutes) +- Open calendar page in logged-out mode; verify read-only behavior. +- Log in as approved user. +- Create a single timed event. +- Edit same event (title/category/location/description). +- Delete the event. +- Confirm list/month views update after each operation. + +### 4. Recurrence + Exception UX (12 minutes) +- Create a recurring event (daily or weekly). +- Re-open event and confirm recurrence preview pane is visible. +- Select one occurrence in preview (verify visual highlight). +- Deselect and reselect to verify toggle behavior. +- Click `Delete Occurrence` and confirm selected occurrence disappears while series remains. + +### 5. CalDAV Round-Trip (10 minutes) +- In CalDAV client: + - subscribe/connect using approved account. + - create one event. + - edit one event. + - delete one single occurrence from a recurring series. +- In web UI: + - refresh and confirm all CalDAV changes are reflected. + +### 6. ICS + Diagnostics (7 minutes) +- Open ICS URL and confirm file downloads and contains seeded + user-created events. +- Open Diagnostics page: + - verify diagnostics snapshot renders. + - click `Download Diagnostics` and confirm JSON file downloads. + +## Pass Criteria +- No fatal errors. +- CRUD works from UI and CalDAV. +- Single-occurrence delete creates exception without splitting series. +- Admin controls behave as expected. +- Diagnostics download works as attachment. + +## Failure Notes Template +- Step: +- Expected: +- Actual: +- URL/page/client: +- Timestamp (local): +- Screenshot/log reference: