@@ -219,45 +219,102 @@ protected function exceptionComponent(string $uid, string $attendee_partstat='NE
219219
220220 /**
221221 * Change status of participants in different recurrences.
222+ *
223+ * Strategy / pass criteria:
224+ * - Build one recurring master with two explicit overridden recurrences.
225+ * - Each override has a different attendee PARTSTAT for attendee1.
226+ * - Export via CalDAV GET.
227+ * - Pass when both RECURRENCE-ID components are present and each expected
228+ * status variant is present in the exported iCal.
229+ *
230+ * Uses near-future dynamic UTC timestamps so recurrence expansion stays
231+ * inside typical horizon limits across environments.
222232 */
223233 public function testChangeStatusOfParticipantsInDifferentRecurrences ()
224234 {
225- $ this ->markTestIncomplete ("Not working " );
226235 $ uid = $ this ->makeUid ('caldav-recur-status ' );
236+ $ master_start = new \DateTimeImmutable ('now ' , new \DateTimeZone ('UTC ' ));
237+ $ master_start = $ master_start ->setTime (9 , 0 , 0 )->modify ('+14 days ' );
238+ $ master_end = $ master_start ->modify ('+1 hour ' );
239+ $ recurrence1 = $ master_start ->modify ('+1 day ' );
240+ $ recurrence2 = $ master_start ->modify ('+2 days ' );
241+ $ recurrence1_moved_start = $ recurrence1 ->modify ('+10 minutes ' );
242+ $ recurrence1_moved_end = $ recurrence1_moved_start ->modify ('+1 hour ' );
243+ $ recurrence2_moved_start = $ recurrence2 ->modify ('+20 minutes ' );
244+ $ recurrence2_moved_end = $ recurrence2_moved_start ->modify ('+1 hour ' );
245+ $ dtstamp = $ master_start ->modify ('-1 hour ' )->format ('Ymd\THis\Z ' );
246+ $ master_start_ical = $ master_start ->format ('Ymd\THis\Z ' );
247+ $ master_end_ical = $ master_end ->format ('Ymd\THis\Z ' );
248+ $ recurrence1_ical = $ recurrence1 ->format ('Ymd\THis\Z ' );
249+ $ recurrence2_ical = $ recurrence2 ->format ('Ymd\THis\Z ' );
250+ $ recurrence1_moved_start_ical = $ recurrence1_moved_start ->format ('Ymd\THis\Z ' );
251+ $ recurrence1_moved_end_ical = $ recurrence1_moved_end ->format ('Ymd\THis\Z ' );
252+ $ recurrence2_moved_start_ical = $ recurrence2_moved_start ->format ('Ymd\THis\Z ' );
253+ $ recurrence2_moved_end_ical = $ recurrence2_moved_end ->format ('Ymd\THis\Z ' );
254+
255+ $ master = "BEGIN:VEVENT \r\n" .
256+ "UID: $ uid \r\n" .
257+ "DTSTAMP: $ dtstamp \r\n" .
258+ "DTSTART: $ master_start_ical \r\n" .
259+ "DTEND: $ master_end_ical \r\n" .
260+ "RRULE:FREQ=DAILY;COUNT=6 \r\n" .
261+ "SUMMARY:Recurring participant test \r\n" .
262+ "ORGANIZER:mailto: " .$ this ->organizerMail ()."\r\n" .
263+ "ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto: " .$ this ->organizerMail ()."\r\n" .
264+ "ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto: " .self ::ATTENDEE1_MAIL ."\r\n" .
265+ "END:VEVENT \r\n" ;
227266
228267 // Two overridden instances with different attendee status.
229268 $ overrides =
230269 "BEGIN:VEVENT \r\n" .
231270 "UID: $ uid \r\n" .
232- "RECURRENCE-ID:20300102T090000Z \r\n" .
233- "DTSTART:20300102T091000Z \r\n" .
234- "DTEND:20300102T101000Z \r\n" .
271+ "DTSTAMP: $ dtstamp \r\n" .
272+ "RECURRENCE-ID: $ recurrence1_ical \r\n" .
273+ "DTSTART: $ recurrence1_moved_start_ical \r\n" .
274+ "DTEND: $ recurrence1_moved_end_ical \r\n" .
275+ "SUMMARY:Recurring participant test exception accepted \r\n" .
235276 "ORGANIZER:mailto: " .$ this ->organizerMail ()."\r\n" .
236277 "ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto: " .$ this ->organizerMail ()."\r\n" .
237278 "ATTENDEE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto: " .self ::ATTENDEE1_MAIL ."\r\n" .
238279 "END:VEVENT \r\n" .
239280 "BEGIN:VEVENT \r\n" .
240281 "UID: $ uid \r\n" .
241- "RECURRENCE-ID:20300103T090000Z \r\n" .
242- "DTSTART:20300103T092000Z \r\n" .
243- "DTEND:20300103T102000Z \r\n" .
282+ "DTSTAMP: $ dtstamp \r\n" .
283+ "RECURRENCE-ID: $ recurrence2_ical \r\n" .
284+ "DTSTART: $ recurrence2_moved_start_ical \r\n" .
285+ "DTEND: $ recurrence2_moved_end_ical \r\n" .
286+ "SUMMARY:Recurring participant test exception tentative \r\n" .
244287 "ORGANIZER:mailto: " .$ this ->organizerMail ()."\r\n" .
245288 "ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto: " .$ this ->organizerMail ()."\r\n" .
246289 "ATTENDEE;PARTSTAT=TENTATIVE;ROLE=REQ-PARTICIPANT:mailto: " .self ::ATTENDEE1_MAIL ."\r\n" .
247290 "END:VEVENT \r\n" ;
248291
249- $ this ->putEvent ($ uid , $ this -> recurringIcal ( $ uid , $ overrides) );
292+ $ this ->putEvent ($ uid , " BEGIN:VCALENDAR \r\n VERSION:2.0 \r\n PRODID:-//EGroupware//CalDAV Test//EN \r\n" . $ master . $ overrides. " END:VCALENDAR \r\n" );
250293
251294 $ ical = $ this ->unfoldIcal ($ this ->getEventIcal ($ uid ));
252- $ this ->assertStringContainsString ("RECURRENCE-ID:20300102T090000Z " , $ ical );
253- $ this ->assertStringContainsString ("RECURRENCE-ID:20300103T090000Z " , $ ical );
254- $ this ->assertStringContainsString ("PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT " , $ ical );
255- $ this ->assertStringContainsString ("PARTSTAT=TENTATIVE;ROLE=REQ-PARTICIPANT " , $ ical );
295+ $ this ->assertStringContainsString ("RECURRENCE-ID: $ recurrence1_ical " , $ ical );
296+ $ this ->assertStringContainsString ("RECURRENCE-ID: $ recurrence2_ical " , $ ical );
297+ $ exception1 = $ this ->exceptionBlock ($ ical , $ recurrence1_ical );
298+ $ exception2 = $ this ->exceptionBlock ($ ical , $ recurrence2_ical );
299+ $ this ->assertNotEmpty ($ exception1 , 'First exception block missing ' );
300+ $ this ->assertNotEmpty ($ exception2 , 'Second exception block missing ' );
301+ $ this ->assertStringContainsString ("mailto: " .self ::ATTENDEE1_MAIL , $ exception1 );
302+ $ this ->assertStringContainsString ("mailto: " .self ::ATTENDEE1_MAIL , $ exception2 );
303+ $ this ->assertStringContainsString ("PARTSTAT=ACCEPTED " , $ exception1 );
304+ $ this ->assertStringContainsString ("PARTSTAT=TENTATIVE " , $ exception2 );
256305 $ this ->assertStringContainsString ("PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;RSVP= " , $ ical );
257306 }
258307
259308 /**
260309 * Create exception with different date and time via CalDAV.
310+ *
311+ * Strategy / pass criteria:
312+ * - Import recurring master plus one exception component linked by
313+ * RECURRENCE-ID.
314+ * - Exception moves occurrence time to a different day/hour.
315+ * - Export via CalDAV GET.
316+ * - Pass when export contains the RECURRENCE-ID and moved DTSTART, and the
317+ * expected one-hour duration.
261318 */
262319 public function testCreateExceptionsWithDifferentDateAndTime ()
263320 {
@@ -272,6 +329,13 @@ public function testCreateExceptionsWithDifferentDateAndTime()
272329
273330 /**
274331 * Change status of participants in exception via CalDAV.
332+ *
333+ * Strategy / pass criteria:
334+ * - Import recurring master plus one exception where attendee1 PARTSTAT is
335+ * ACCEPTED on the exception only.
336+ * - Export via CalDAV GET.
337+ * - Pass when export contains the exception RECURRENCE-ID and the exception
338+ * participant status, while master/default status remains NEEDS-ACTION.
275339 */
276340 public function testChangeStatusOfParticipantsInException ()
277341 {
0 commit comments