diff --git a/apps/dav/lib/CalDAV/TipBroker.php b/apps/dav/lib/CalDAV/TipBroker.php index a0b3c904493d3..43eff124f0bb1 100644 --- a/apps/dav/lib/CalDAV/TipBroker.php +++ b/apps/dav/lib/CalDAV/TipBroker.php @@ -126,7 +126,7 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, $message->significantChange = $attendee['forceSend'] === 'REQUEST' || - count($oldAttendeeInstances) != count($newAttendeeInstances) || + count($oldAttendeeInstances) !== count($newAttendeeInstances) || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; diff --git a/apps/dav/tests/unit/CalDAV/TipBrokerTest.php b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php new file mode 100644 index 0000000000000..ae855628d9b1b --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php @@ -0,0 +1,185 @@ +broker = new TipBroker(); + // construct calendar with a 1 hour event and same start/end time zones + $this->vCalendar1a = new VCalendar(); + /** @var VEvent $vEvent */ + $vEvent = $this->vCalendar1a->add('VEVENT', []); + $vEvent->add('UID', '96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTAMP', '20240701T000000Z'); + $vEvent->add('CREATED', '20240701T000000Z'); + $vEvent->add('LAST-MODIFIED', '20240701T000000Z'); + $vEvent->add('SEQUENCE', '1'); + $vEvent->add('STATUS', 'CONFIRMED'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + } + + public function testParseEventForOrganizerOnCreate(): void { + + // construct calendar and generate event info for newly created event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = [ + 'organizer' => null, + 'significantChangeHash' => '', + 'attendees' => [], + ]; + $currentEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->callMethod($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnModify(): void { + + // construct calendar and generate event info for modified event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->SUMMARY->setValue('Test Event Modified'); + $currentEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->callMethod($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnDelete(): void { + + // construct calendar and generate event info for modified event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + $currentEventInfo = $previousEventInfo; + $currentEventInfo['attendees'] = []; + ++$currentEventInfo['sequence']; + // test iTip generation + $messages = $this->callMethod($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('CANCEL', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnStatusCancelled(): void { + + // construct calendar and generate event info for modified event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->STATUS->setValue('CANCELLED'); + $currentEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->callMethod($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('CANCEL', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnAddAttendee(): void { + + // construct calendar and generate event info for modified event with two attendees + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [ + 'CN' => 'Attendee Two', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $currentEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->callMethod($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(2, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + $this->assertEquals('REQUEST', $messages[1]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient); + + } + + public function testParseEventForOrganizerOnRemoveAttendee(): void { + + // construct calendar and generate event info for modified event with two attendees + $calendar = clone $this->vCalendar1a; + $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [ + 'CN' => 'Attendee Two', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $previousEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->remove('ATTENDEE'); + $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $currentEventInfo = $this->callMethod($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->callMethod($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(2, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + $this->assertEquals('CANCEL', $messages[1]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender); + $this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient); + + } + + public static function callMethod($obj, $name, array $args) { + $class = new \ReflectionClass($obj); + $method = $class->getMethod($name); + return $method->invokeArgs($obj, $args); + } +}