(string)$e['summary'], $events); if (!in_array('Ganztag Event', $summaries, true)) { throw new \Exception('Missing all-day event summary'); } if (!in_array('Termin A', $summaries, true)) { throw new \Exception('Missing timed event summary'); } $timed = null; foreach ($events as $event) { if ((string)($event['summary'] ?? '') === 'Termin A') { $timed = $event; break; } } if (!is_array($timed)) { throw new \Exception('Timed event payload missing'); } if (trim((string)($timed['startIso'] ?? '')) === '') { throw new \Exception('Timed event should expose startIso for client-side timezone conversion'); } } public function testEventsForDateReadsMultipleConfiguredFiles(): void { $dir = TMP_DIR . '/chrono_ics/' . uniqid('multi_', true); @mkdir($dir, 0777, true); $ics1 = $dir . '/one.ics'; $ics2 = $dir . '/two.ics'; @file_put_contents($ics1, "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260218T100000\nSUMMARY:Eins\nEND:VEVENT\nEND:VCALENDAR\n"); @file_put_contents($ics2, "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260218T110000\nSUMMARY:Zwei\nEND:VEVENT\nEND:VCALENDAR\n"); $config = $ics1 . "\n" . $ics2; $events = ChronologicalIcsEvents::eventsForDate($config, '2026-02-18'); if (count($events) !== 2) { throw new \Exception('Expected 2 events from two files, got ' . count($events)); } } public function testEventsForDateSupportsWeeklyRecurrence(): void { $dir = TMP_DIR . '/chrono_ics/' . uniqid('rrule_', true); @mkdir($dir, 0777, true); $ics = $dir . '/recurring.ics'; $content = "BEGIN:VCALENDAR\n" . "BEGIN:VEVENT\n" . "UID:weekly-1\n" . "DTSTART:20260205T090000\n" . "RRULE:FREQ=WEEKLY;INTERVAL=1\n" . "SUMMARY:Wiederkehrender Termin\n" . "END:VEVENT\n" . "END:VCALENDAR\n"; @file_put_contents($ics, $content); $events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-12'); if (count($events) < 1) { throw new \Exception('Expected recurring event on 2026-02-12, got none'); } $summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events); if (!in_array('Wiederkehrender Termin', $summaries, true)) { throw new \Exception('Recurring summary not found on matching date'); } } public function testEventsForDateRespectsExdateForRecurringEvent(): void { $dir = TMP_DIR . '/chrono_ics/' . uniqid('exdate_', true); @mkdir($dir, 0777, true); $ics = $dir . '/recurring-exdate.ics'; $content = "BEGIN:VCALENDAR\n" . "BEGIN:VEVENT\n" . "UID:weekly-2\n" . "DTSTART:20260205T090000\n" . "RRULE:FREQ=WEEKLY;COUNT=4\n" . "EXDATE:20260212T090000\n" . "SUMMARY:Termin mit Ausnahme\n" . "END:VEVENT\n" . "END:VCALENDAR\n"; @file_put_contents($ics, $content); $events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-12'); $summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events); if (in_array('Termin mit Ausnahme', $summaries, true)) { throw new \Exception('Recurring event with EXDATE should not appear on excluded day'); } } public function testEventsForDateKeepsUtcDateAndTimeAsIs(): void { $previousTimezone = date_default_timezone_get(); date_default_timezone_set('Europe/Berlin'); try { $dir = TMP_DIR . '/chrono_ics/' . uniqid('tz_', true); @mkdir($dir, 0777, true); $ics = $dir . '/timezone.ics'; $content = "BEGIN:VCALENDAR\n" . "BEGIN:VEVENT\n" . "UID:utc-shift\n" . "DTSTART:20260216T233000Z\n" . "SUMMARY:UTC Spaet\n" . "END:VEVENT\n" . "END:VCALENDAR\n"; @file_put_contents($ics, $content); $eventsOn16 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16'); $eventsOn17 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-17'); $summaries16 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn16); $summaries17 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn17); if (!in_array('UTC Spaet', $summaries16, true)) { throw new \Exception('UTC event should stay on its own UTC date'); } if (in_array('UTC Spaet', $summaries17, true)) { throw new \Exception('UTC event should not be shifted to next day by server timezone'); } $utcEvent = null; foreach ($eventsOn16 as $entry) { if ((string)($entry['summary'] ?? '') === 'UTC Spaet') { $utcEvent = $entry; break; } } if (!is_array($utcEvent)) { throw new \Exception('UTC event payload missing after day match'); } if ((string)($utcEvent['time'] ?? '') !== '23:30') { throw new \Exception('UTC event time should remain unchanged (expected 23:30)'); } } finally { date_default_timezone_set($previousTimezone); } } public function testEventsForDateShowsMultiDayAllDayEventOnOverlappingDays(): void { $dir = TMP_DIR . '/chrono_ics/' . uniqid('multiday_', true); @mkdir($dir, 0777, true); $ics = $dir . '/multiday.ics'; $content = "BEGIN:VCALENDAR\n" . "BEGIN:VEVENT\n" . "UID:multi-day-1\n" . "DTSTART;VALUE=DATE:20260216\n" . "DTEND;VALUE=DATE:20260218\n" . "SUMMARY:Mehrtagesereignis\n" . "END:VEVENT\n" . "END:VCALENDAR\n"; @file_put_contents($ics, $content); $eventsOn16 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16'); $eventsOn17 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-17'); $eventsOn18 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-18'); $summaries16 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn16); $summaries17 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn17); $summaries18 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn18); if (!in_array('Mehrtagesereignis', $summaries16, true)) { throw new \Exception('Multi-day all-day event should appear on start day'); } if (!in_array('Mehrtagesereignis', $summaries17, true)) { throw new \Exception('Multi-day all-day event should appear on overlapping day'); } if (in_array('Mehrtagesereignis', $summaries18, true)) { throw new \Exception('Multi-day all-day event should respect exclusive DTEND day'); } } }