feat: automated appointment creation

Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
This commit is contained in:
SebastianKrupinski 2025-12-08 17:26:30 -05:00
parent 0ff8b35578
commit 16198d2b37
3 changed files with 281 additions and 2 deletions

View file

@ -316,7 +316,29 @@ class Manager implements IManager {
$this->logger->warning('iMip message could not be processed because no writable calendar was found');
return false;
}
$calendar->handleIMipMessage($userId, $vObject->serialize());
if (!empty($options['absentCreateStatus'])) {
$status = strtoupper($options['absentCreateStatus']);
if (in_array($status, ['TENTATIVE', 'CONFIRMED', 'CANCELLED'], true) === false) {
$this->logger->warning('iMip message could not be processed because an invalid status was provided for the event');
return false;
}
if (isset($vObject->VEVENT->STATUS)) {
$vObject->VEVENT->STATUS->setValue($status);
} else {
$vObject->VEVENT->add('STATUS', $status);
}
}
try {
$calendar->handleIMipMessage($userId, $vObject->serialize());
} catch (CalendarException $e) {
$this->logger->error('iMip message could not be processed because an error occurred', ['exception' => $e]);
return false;
}
return true;
}
$this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar');

View file

@ -143,7 +143,7 @@ interface IManager {
/**
* Handles a iMip message
*
* @param array{absent?: "create", recipient?: string} $options
* @param array{absent?: "create"|"ignore", absentCreateStatus?: "cancelled"|"confirmed"|"tentative", recipient?: string, ...} $options
*
* @throws \OCP\DB\Exception
*

View file

@ -582,6 +582,262 @@ class ManagerTest extends TestCase {
$result = $manager->handleIMip($userId, $calendar->serialize());
}
public function testHandleImipWithAbsentCreateOption(): void {
// construct mock user calendar (no matching event found)
$userCalendar = $this->createMock(ITestCalendar::class);
$userCalendar->expects(self::exactly(2))
->method('isDeleted')
->willReturn(false);
$userCalendar->expects(self::exactly(2))
->method('isWritable')
->willReturn(true);
$userCalendar->expects(self::once())
->method('search')
->willReturn([]);
// construct mock calendar manager and returns
/** @var Manager&MockObject $manager */
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->coordinator,
$this->container,
$this->logger,
$this->time,
$this->secureRandom,
$this->userManager,
$this->serverFactory,
$this->propertyMapper,
])
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
->getMock();
$manager->expects(self::once())
->method('getCalendarsForPrincipal')
->willReturn([$userCalendar]);
$manager->expects(self::once())
->method('getPrimaryCalendar')
->willReturn(null);
// construct parameters
$userId = 'attendee1';
$calendar = $this->vCalendar1a;
$calendar->add('METHOD', 'REQUEST');
// construct user calendar returns - should create new event
$userCalendar->expects(self::once())
->method('handleIMipMessage')
->with($userId, self::callback(function ($data) {
return str_contains($data, 'STATUS:TENTATIVE');
}));
// test method with absent=create option
$result = $manager->handleIMip($userId, $calendar->serialize(), [
'absent' => 'create',
'absentCreateStatus' => 'tentative',
]);
// Assert
$this->assertTrue($result);
}
public function testHandleImipWithAbsentIgnoreOption(): void {
// construct mock user calendar (no matching event found)
$userCalendar = $this->createMock(ITestCalendar::class);
$userCalendar->expects(self::once())
->method('isDeleted')
->willReturn(false);
$userCalendar->expects(self::once())
->method('isWritable')
->willReturn(true);
$userCalendar->expects(self::once())
->method('search')
->willReturn([]);
// construct mock calendar manager and returns
/** @var Manager&MockObject $manager */
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->coordinator,
$this->container,
$this->logger,
$this->time,
$this->secureRandom,
$this->userManager,
$this->serverFactory,
$this->propertyMapper,
])
->onlyMethods(['getCalendarsForPrincipal'])
->getMock();
$manager->expects(self::once())
->method('getCalendarsForPrincipal')
->willReturn([$userCalendar]);
// construct logger returns - should log warning since event not found and absent=ignore
$this->logger->expects(self::once())->method('warning')
->with('iMip message could not be processed because no corresponding event was found in any calendar');
// construct parameters
$userId = 'attendee1';
$calendar = $this->vCalendar1a;
$calendar->add('METHOD', 'REQUEST');
// test method with absent=ignore option
$result = $manager->handleIMip($userId, $calendar->serialize(), [
'absent' => 'ignore',
]);
// Assert
$this->assertFalse($result);
}
public function testHandleImipWithAbsentCreateNoWritableCalendar(): void {
// construct mock user calendar (not writable)
$userCalendar = $this->createMock(ITestCalendar::class);
$userCalendar->expects(self::exactly(2))
->method('isDeleted')
->willReturn(false);
$userCalendar->expects(self::exactly(2))
->method('isWritable')
->willReturn(false);
// construct mock calendar manager and returns
/** @var Manager&MockObject $manager */
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->coordinator,
$this->container,
$this->logger,
$this->time,
$this->secureRandom,
$this->userManager,
$this->serverFactory,
$this->propertyMapper,
])
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
->getMock();
$manager->expects(self::once())
->method('getCalendarsForPrincipal')
->willReturn([$userCalendar]);
$manager->expects(self::once())
->method('getPrimaryCalendar')
->willReturn(null);
// construct logger returns
$this->logger->expects(self::once())->method('warning')
->with('iMip message could not be processed because no writable calendar was found');
// construct parameters
$userId = 'attendee1';
$calendar = $this->vCalendar1a;
$calendar->add('METHOD', 'REQUEST');
// test method with absent=create option but no writable calendar
$result = $manager->handleIMip($userId, $calendar->serialize(), [
'absent' => 'create',
'absentCreateStatus' => 'tentative',
]);
// Assert
$this->assertFalse($result);
}
public function testHandleImipWithAbsentCreateUsesPrimaryCalendar(): void {
// construct mock user calendar (no matching event found)
$userCalendar = $this->createMock(ITestCalendar::class);
$userCalendar->expects(self::once())
->method('isDeleted')
->willReturn(false);
$userCalendar->expects(self::once())
->method('isWritable')
->willReturn(true);
$userCalendar->expects(self::once())
->method('search')
->willReturn([]);
// construct mock primary calendar
$primaryCalendar = $this->createMock(ITestCalendar::class);
$primaryCalendar->expects(self::once())
->method('isDeleted')
->willReturn(false);
$primaryCalendar->expects(self::once())
->method('isWritable')
->willReturn(true);
// construct mock calendar manager and returns
/** @var Manager&MockObject $manager */
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->coordinator,
$this->container,
$this->logger,
$this->time,
$this->secureRandom,
$this->userManager,
$this->serverFactory,
$this->propertyMapper,
])
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
->getMock();
$manager->expects(self::once())
->method('getCalendarsForPrincipal')
->willReturn([$userCalendar]);
$manager->expects(self::once())
->method('getPrimaryCalendar')
->willReturn($primaryCalendar);
// construct parameters
$userId = 'attendee1';
$calendar = $this->vCalendar1a;
$calendar->add('METHOD', 'REQUEST');
// primary calendar should receive the event
$primaryCalendar->expects(self::once())
->method('handleIMipMessage')
->with($userId, self::callback(function ($data) {
return str_contains($data, 'STATUS:TENTATIVE');
}));
// test method with absent=create option
$result = $manager->handleIMip($userId, $calendar->serialize(), [
'absent' => 'create',
'absentCreateStatus' => 'tentative',
]);
// Assert
$this->assertTrue($result);
}
public function testHandleImipWithAbsentCreateOverwritesExistingStatus(): void {
// construct mock user calendar (no matching event found)
$userCalendar = $this->createMock(ITestCalendar::class);
$userCalendar->expects(self::exactly(2))
->method('isDeleted')
->willReturn(false);
$userCalendar->expects(self::exactly(2))
->method('isWritable')
->willReturn(true);
$userCalendar->expects(self::once())
->method('search')
->willReturn([]);
// construct mock calendar manager and returns
/** @var Manager&MockObject $manager */
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->coordinator,
$this->container,
$this->logger,
$this->time,
$this->secureRandom,
$this->userManager,
$this->serverFactory,
$this->propertyMapper,
])
->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar'])
->getMock();
$manager->expects(self::once())
->method('getCalendarsForPrincipal')
->willReturn([$userCalendar]);
$manager->expects(self::once())
->method('getPrimaryCalendar')
->willReturn(null);
// construct parameters - calendar already has CONFIRMED status
$userId = 'attendee1';
$calendar = $this->vCalendar1a;
$calendar->add('METHOD', 'REQUEST');
// The original event has STATUS:CONFIRMED, but it should be overwritten to TENTATIVE
$userCalendar->expects(self::once())
->method('handleIMipMessage')
->with($userId, self::callback(function ($data) {
// Should contain TENTATIVE and not CONFIRMED
return str_contains($data, 'STATUS:TENTATIVE') && !str_contains($data, 'STATUS:CONFIRMED');
}));
// test method with absent=create option
$result = $manager->handleIMip($userId, $calendar->serialize(), [
'absent' => 'create',
'absentCreateStatus' => 'tentative',
]);
// Assert
$this->assertTrue($result);
}
public function testhandleIMipRequestWithInvalidPrincipal() {
$invalidPrincipal = 'invalid-principal-uri';
$sender = 'sender@example.com';
@ -927,4 +1183,5 @@ EOF;
];
$this->assertEquals($expected, $actual);
}
}