Skip to content

Commit 8f9bd94

Browse files
feat: automated appointment creation
Signed-off-by: SebastianKrupinski <[email protected]>
1 parent 189e1fc commit 8f9bd94

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed

lib/private/Calendar/Manager.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,18 @@ public function handleIMip(
316316
$this->logger->warning('iMip message could not be processed because no writable calendar was found');
317317
return false;
318318
}
319+
if (isset($options['absentCreateStatus']) && !empty($options['absentCreateStatus'])) {
320+
$status = strtoupper($options['absentCreateStatus']);
321+
if (isset($vObject->VEVENT->STATUS)) {
322+
$vObject->VEVENT->STATUS->setValue($status);
323+
} else {
324+
$vObject->VEVENT->add('STATUS', $status);
325+
}
326+
}
327+
319328
$calendar->handleIMipMessage($userId, $vObject->serialize());
329+
330+
return true;
320331
}
321332

322333
$this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar');

tests/lib/Calendar/ManagerTest.php

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,262 @@ public function testHandleImip(): void {
582582
$result = $manager->handleIMip($userId, $calendar->serialize());
583583
}
584584

585+
public function testHandleImipWithAbsentCreateOption(): void {
586+
// construct mock user calendar (no matching event found)
587+
$userCalendar = $this->createMock(ITestCalendar::class);
588+
$userCalendar->expects(self::once())
589+
->method('isDeleted')
590+
->willReturn(false);
591+
$userCalendar->expects(self::exactly(2))
592+
->method('isWritable')
593+
->willReturn(true);
594+
$userCalendar->expects(self::once())
595+
->method('search')
596+
->willReturn([]);
597+
// construct mock calendar manager and returns
598+
/** @var Manager&MockObject $manager */
599+
$manager = $this->getMockBuilder(Manager::class)
600+
->setConstructorArgs([
601+
$this->coordinator,
602+
$this->container,
603+
$this->logger,
604+
$this->time,
605+
$this->secureRandom,
606+
$this->userManager,
607+
$this->serverFactory,
608+
$this->propertyMapper,
609+
])
610+
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
611+
->getMock();
612+
$manager->expects(self::once())
613+
->method('getCalendarsForPrincipal')
614+
->willReturn([$userCalendar]);
615+
$manager->expects(self::once())
616+
->method('getPrimaryCalendar')
617+
->willReturn(null);
618+
// construct parameters
619+
$userId = 'attendee1';
620+
$calendar = $this->vCalendar1a;
621+
$calendar->add('METHOD', 'REQUEST');
622+
// construct user calendar returns - should create new event
623+
$userCalendar->expects(self::once())
624+
->method('handleIMipMessage')
625+
->with($userId, self::callback(function ($data) {
626+
return str_contains($data, 'STATUS:TENTATIVE');
627+
}));
628+
// test method with absent=create option
629+
$result = $manager->handleIMip($userId, $calendar->serialize(), [
630+
'absent' => 'create',
631+
'absentCreateStatus' => 'tentative',
632+
]);
633+
// Assert
634+
$this->assertTrue($result);
635+
}
636+
637+
public function testHandleImipWithAbsentIgnoreOption(): void {
638+
// construct mock user calendar (no matching event found)
639+
$userCalendar = $this->createMock(ITestCalendar::class);
640+
$userCalendar->expects(self::once())
641+
->method('isDeleted')
642+
->willReturn(false);
643+
$userCalendar->expects(self::once())
644+
->method('isWritable')
645+
->willReturn(true);
646+
$userCalendar->expects(self::once())
647+
->method('search')
648+
->willReturn([]);
649+
// construct mock calendar manager and returns
650+
/** @var Manager&MockObject $manager */
651+
$manager = $this->getMockBuilder(Manager::class)
652+
->setConstructorArgs([
653+
$this->coordinator,
654+
$this->container,
655+
$this->logger,
656+
$this->time,
657+
$this->secureRandom,
658+
$this->userManager,
659+
$this->serverFactory,
660+
$this->propertyMapper,
661+
])
662+
->onlyMethods(['getCalendarsForPrincipal'])
663+
->getMock();
664+
$manager->expects(self::once())
665+
->method('getCalendarsForPrincipal')
666+
->willReturn([$userCalendar]);
667+
// construct logger returns - should log warning since event not found and absent=ignore
668+
$this->logger->expects(self::once())->method('warning')
669+
->with('iMip message could not be processed because no corresponding event was found in any calendar');
670+
// construct parameters
671+
$userId = 'attendee1';
672+
$calendar = $this->vCalendar1a;
673+
$calendar->add('METHOD', 'REQUEST');
674+
// test method with absent=ignore option
675+
$result = $manager->handleIMip($userId, $calendar->serialize(), [
676+
'absent' => 'ignore',
677+
]);
678+
// Assert
679+
$this->assertFalse($result);
680+
}
681+
682+
public function testHandleImipWithAbsentCreateNoWritableCalendar(): void {
683+
// construct mock user calendar (not writable)
684+
$userCalendar = $this->createMock(ITestCalendar::class);
685+
$userCalendar->expects(self::exactly(2))
686+
->method('isDeleted')
687+
->willReturn(false);
688+
$userCalendar->expects(self::exactly(2))
689+
->method('isWritable')
690+
->willReturn(false);
691+
// construct mock calendar manager and returns
692+
/** @var Manager&MockObject $manager */
693+
$manager = $this->getMockBuilder(Manager::class)
694+
->setConstructorArgs([
695+
$this->coordinator,
696+
$this->container,
697+
$this->logger,
698+
$this->time,
699+
$this->secureRandom,
700+
$this->userManager,
701+
$this->serverFactory,
702+
$this->propertyMapper,
703+
])
704+
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
705+
->getMock();
706+
$manager->expects(self::once())
707+
->method('getCalendarsForPrincipal')
708+
->willReturn([$userCalendar]);
709+
$manager->expects(self::once())
710+
->method('getPrimaryCalendar')
711+
->willReturn(null);
712+
// construct logger returns
713+
$this->logger->expects(self::once())->method('warning')
714+
->with('iMip message could not be processed because no writable calendar was found');
715+
// construct parameters
716+
$userId = 'attendee1';
717+
$calendar = $this->vCalendar1a;
718+
$calendar->add('METHOD', 'REQUEST');
719+
// test method with absent=create option but no writable calendar
720+
$result = $manager->handleIMip($userId, $calendar->serialize(), [
721+
'absent' => 'create',
722+
'absentCreateStatus' => 'tentative',
723+
]);
724+
// Assert
725+
$this->assertFalse($result);
726+
}
727+
728+
public function testHandleImipWithAbsentCreateUsesPrimaryCalendar(): void {
729+
// construct mock user calendar (no matching event found)
730+
$userCalendar = $this->createMock(ITestCalendar::class);
731+
$userCalendar->expects(self::once())
732+
->method('isDeleted')
733+
->willReturn(false);
734+
$userCalendar->expects(self::once())
735+
->method('isWritable')
736+
->willReturn(true);
737+
$userCalendar->expects(self::once())
738+
->method('search')
739+
->willReturn([]);
740+
// construct mock primary calendar
741+
$primaryCalendar = $this->createMock(ITestCalendar::class);
742+
$primaryCalendar->expects(self::once())
743+
->method('isDeleted')
744+
->willReturn(false);
745+
$primaryCalendar->expects(self::once())
746+
->method('isWritable')
747+
->willReturn(true);
748+
// construct mock calendar manager and returns
749+
/** @var Manager&MockObject $manager */
750+
$manager = $this->getMockBuilder(Manager::class)
751+
->setConstructorArgs([
752+
$this->coordinator,
753+
$this->container,
754+
$this->logger,
755+
$this->time,
756+
$this->secureRandom,
757+
$this->userManager,
758+
$this->serverFactory,
759+
$this->propertyMapper,
760+
])
761+
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
762+
->getMock();
763+
$manager->expects(self::once())
764+
->method('getCalendarsForPrincipal')
765+
->willReturn([$userCalendar]);
766+
$manager->expects(self::once())
767+
->method('getPrimaryCalendar')
768+
->willReturn($primaryCalendar);
769+
// construct parameters
770+
$userId = 'attendee1';
771+
$calendar = $this->vCalendar1a;
772+
$calendar->add('METHOD', 'REQUEST');
773+
// primary calendar should receive the event
774+
$primaryCalendar->expects(self::once())
775+
->method('handleIMipMessage')
776+
->with($userId, self::callback(function ($data) {
777+
return str_contains($data, 'STATUS:TENTATIVE');
778+
}));
779+
// test method with absent=create option
780+
$result = $manager->handleIMip($userId, $calendar->serialize(), [
781+
'absent' => 'create',
782+
'absentCreateStatus' => 'tentative',
783+
]);
784+
// Assert
785+
$this->assertTrue($result);
786+
}
787+
788+
public function testHandleImipWithAbsentCreateOverwritesExistingStatus(): void {
789+
// construct mock user calendar (no matching event found)
790+
$userCalendar = $this->createMock(ITestCalendar::class);
791+
$userCalendar->expects(self::once())
792+
->method('isDeleted')
793+
->willReturn(false);
794+
$userCalendar->expects(self::exactly(2))
795+
->method('isWritable')
796+
->willReturn(true);
797+
$userCalendar->expects(self::once())
798+
->method('search')
799+
->willReturn([]);
800+
// construct mock calendar manager and returns
801+
/** @var Manager&MockObject $manager */
802+
$manager = $this->getMockBuilder(Manager::class)
803+
->setConstructorArgs([
804+
$this->coordinator,
805+
$this->container,
806+
$this->logger,
807+
$this->time,
808+
$this->secureRandom,
809+
$this->userManager,
810+
$this->serverFactory,
811+
$this->propertyMapper,
812+
])
813+
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
814+
->getMock();
815+
$manager->expects(self::once())
816+
->method('getCalendarsForPrincipal')
817+
->willReturn([$userCalendar]);
818+
$manager->expects(self::once())
819+
->method('getPrimaryCalendar')
820+
->willReturn(null);
821+
// construct parameters - calendar already has CONFIRMED status
822+
$userId = 'attendee1';
823+
$calendar = $this->vCalendar1a;
824+
$calendar->add('METHOD', 'REQUEST');
825+
// The original event has STATUS:CONFIRMED, but it should be overwritten to TENTATIVE
826+
$userCalendar->expects(self::once())
827+
->method('handleIMipMessage')
828+
->with($userId, self::callback(function ($data) {
829+
// Should contain TENTATIVE and not CONFIRMED
830+
return str_contains($data, 'STATUS:TENTATIVE') && !str_contains($data, 'STATUS:CONFIRMED');
831+
}));
832+
// test method with absent=create option
833+
$result = $manager->handleIMip($userId, $calendar->serialize(), [
834+
'absent' => 'create',
835+
'absentCreateStatus' => 'tentative',
836+
]);
837+
// Assert
838+
$this->assertTrue($result);
839+
}
840+
585841
public function testhandleIMipRequestWithInvalidPrincipal() {
586842
$invalidPrincipal = 'invalid-principal-uri';
587843
$sender = '[email protected]';
@@ -927,4 +1183,5 @@ public function testCheckAvailabilityWithMailtoPrefix(): void {
9271183
];
9281184
$this->assertEquals($expected, $actual);
9291185
}
1186+
9301187
}

0 commit comments

Comments
 (0)