From 69e0158030e49b3d16276b8488d5dd1c6ffa023b Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Thu, 16 May 2024 21:27:07 +0200 Subject: [PATCH] feat: implement public OCP api to update resources and rooms Signed-off-by: Richard Steinmetz --- ...ateCalendarResourcesRoomsBackgroundJob.php | 415 +---------------- ...alendarResourcesRoomsBackgroundJobTest.php | 393 +--------------- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Calendar/Resource/Manager.php | 6 + .../Calendar/ResourcesRoomsUpdater.php | 413 ++++++++++++++++ lib/private/Calendar/Room/Manager.php | 6 + lib/public/Calendar/Resource/IManager.php | 8 +- lib/public/Calendar/Room/IManager.php | 8 +- tests/lib/Calendar/Resource/ManagerTest.php | 14 + .../Calendar/ResourcesRoomsUpdaterTest.php | 439 ++++++++++++++++++ tests/lib/Calendar/Room/ManagerTest.php | 14 + 12 files changed, 920 insertions(+), 798 deletions(-) create mode 100644 lib/private/Calendar/ResourcesRoomsUpdater.php create mode 100644 tests/lib/Calendar/ResourcesRoomsUpdaterTest.php diff --git a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php index b4571e2509d06..670abd2662ac7 100644 --- a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php @@ -28,427 +28,26 @@ namespace OCA\DAV\BackgroundJob; -use OCA\DAV\CalDAV\CalDavBackend; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; -use OCP\Calendar\BackendTemporarilyUnavailableException; -use OCP\Calendar\IMetadataProvider; -use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Resource\IManager as IResourceManager; -use OCP\Calendar\Resource\IResource; use OCP\Calendar\Room\IManager as IRoomManager; -use OCP\Calendar\Room\IRoom; -use OCP\IDBConnection; class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob { - - /** @var IResourceManager */ - private $resourceManager; - - /** @var IRoomManager */ - private $roomManager; - - /** @var IDBConnection */ - private $dbConnection; - - /** @var CalDavBackend */ - private $calDavBackend; - - public function __construct(ITimeFactory $time, - IResourceManager $resourceManager, - IRoomManager $roomManager, - IDBConnection $dbConnection, - CalDavBackend $calDavBackend) { + public function __construct( + ITimeFactory $time, + private IResourceManager $resourceManager, + private IRoomManager $roomManager, + ) { parent::__construct($time); - $this->resourceManager = $resourceManager; - $this->roomManager = $roomManager; - $this->dbConnection = $dbConnection; - $this->calDavBackend = $calDavBackend; // Run once an hour $this->setInterval(60 * 60); $this->setTimeSensitivity(self::TIME_SENSITIVE); } - /** - * @param $argument - */ public function run($argument): void { - $this->runForBackend( - $this->resourceManager, - 'calendar_resources', - 'calendar_resources_md', - 'resource_id', - 'principals/calendar-resources' - ); - $this->runForBackend( - $this->roomManager, - 'calendar_rooms', - 'calendar_rooms_md', - 'room_id', - 'principals/calendar-rooms' - ); - } - - /** - * Run background-job for one specific backendManager - * either ResourceManager or RoomManager - * - * @param IResourceManager|IRoomManager $backendManager - * @param string $dbTable - * @param string $dbTableMetadata - * @param string $foreignKey - * @param string $principalPrefix - */ - private function runForBackend($backendManager, - string $dbTable, - string $dbTableMetadata, - string $foreignKey, - string $principalPrefix): void { - $backends = $backendManager->getBackends(); - - foreach ($backends as $backend) { - $backendId = $backend->getBackendIdentifier(); - - try { - if ($backend instanceof IResourceBackend) { - $list = $backend->listAllResources(); - } else { - $list = $backend->listAllRooms(); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); - $newIds = array_diff($list, $cachedList); - $deletedIds = array_diff($cachedList, $list); - $editedIds = array_intersect($list, $cachedList); - - foreach ($newIds as $newId) { - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($newId); - } else { - $resource = $backend->getRoom($newId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $id = $this->addToCache($dbTable, $backendId, $resource); - $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); - // we don't create the calendar here, it is created lazily - // when an event is actually scheduled with this resource / room - } - - foreach ($deletedIds as $deletedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); - $this->deleteFromCache($dbTable, $id); - $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); - - $principalName = implode('-', [$backendId, $deletedId]); - $this->deleteCalendarDataForResource($principalPrefix, $principalName); - } - - foreach ($editedIds as $editedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); - - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($editedId); - } else { - $resource = $backend->getRoom($editedId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $this->updateCache($dbTable, $id, $resource); - - if ($resource instanceof IMetadataProvider) { - $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); - $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); - } - } - } - } - - /** - * add entry to cache that exists remotely but not yet in cache - * - * @param string $table - * @param string $backendId - * @param IResource|IRoom $remote - * - * @return int Insert id - */ - private function addToCache(string $table, - string $backendId, - $remote): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - 'backend_id' => $query->createNamedParameter($backendId), - 'resource_id' => $query->createNamedParameter($remote->getId()), - 'email' => $query->createNamedParameter($remote->getEMail()), - 'displayname' => $query->createNamedParameter($remote->getDisplayName()), - 'group_restrictions' => $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - )) - ]) - ->executeStatement(); - return $query->getLastInsertId(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $foreignId - * @param array $metadata - */ - private function addMetadataToCache(string $table, - string $foreignKey, - int $foreignId, - array $metadata): void { - foreach ($metadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - $foreignKey => $query->createNamedParameter($foreignId), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - } - - /** - * delete entry from cache that does not exist anymore remotely - * - * @param string $table - * @param int $id - */ - private function deleteFromCache(string $table, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - */ - private function deleteMetadataFromCache(string $table, - string $foreignKey, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * update an existing entry in cache - * - * @param string $table - * @param int $id - * @param IResource|IRoom $remote - */ - private function updateCache(string $table, - int $id, - $remote): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($table) - ->set('email', $query->createNamedParameter($remote->getEMail())) - ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) - ->set('group_restrictions', $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - ))) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $dbTable - * @param string $foreignKey - * @param int $id - * @param array $metadata - * @param array $cachedMetadata - */ - private function updateMetadataCache(string $dbTable, - string $foreignKey, - int $id, - array $metadata, - array $cachedMetadata): void { - $newMetadata = array_diff_key($metadata, $cachedMetadata); - $deletedMetadata = array_diff_key($cachedMetadata, $metadata); - - foreach ($newMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($dbTable) - ->values([ - $foreignKey => $query->createNamedParameter($id), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - - foreach ($deletedMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($dbTable) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) - ->executeStatement(); - } - - $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); - foreach ($existingKeys as $existingKey) { - if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($dbTable) - ->set('value', $query->createNamedParameter($metadata[$existingKey])) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) - ->executeStatement(); - } - } - } - - /** - * serialize array of group restrictions to store them in database - * - * @param array $groups - * - * @return string - */ - private function serializeGroupRestrictions(array $groups): string { - return \json_encode($groups, JSON_THROW_ON_ERROR); - } - - /** - * Gets all metadata of a backend - * - * @param IResource|IRoom $resource - * - * @return array - */ - private function getAllMetadataOfBackend($resource): array { - if (!($resource instanceof IMetadataProvider)) { - return []; - } - - $keys = $resource->getAllAvailableMetadataKeys(); - $metadata = []; - foreach ($keys as $key) { - $metadata[$key] = $resource->getMetadataForKey($key); - } - - return $metadata; - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - * - * @return array - */ - private function getAllMetadataOfCache(string $table, - string $foreignKey, - int $id): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select(['key', 'value']) - ->from($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - $metadata = []; - foreach ($rows as $row) { - $metadata[$row['key']] = $row['value']; - } - - return $metadata; - } - - /** - * Gets all cached rooms / resources by backend - * - * @param $tableName - * @param $backendId - * - * @return array - */ - private function getAllCachedByBackend(string $tableName, - string $backendId): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('resource_id') - ->from($tableName) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - return array_map(function ($row): string { - return $row['resource_id']; - }, $rows); - } - - /** - * @param $principalPrefix - * @param $principalUri - */ - private function deleteCalendarDataForResource(string $principalPrefix, - string $principalUri): void { - $calendar = $this->calDavBackend->getCalendarByUri( - implode('/', [$principalPrefix, $principalUri]), - CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); - - if ($calendar !== null) { - $this->calDavBackend->deleteCalendar( - $calendar['id'], - true // Because this wasn't deleted by a user - ); - } - } - - /** - * @param $table - * @param $backendId - * @param $resourceId - * - * @return int - */ - private function getIdForBackendAndResource(string $table, - string $backendId, - string $resourceId): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('id') - ->from($table) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) - ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); - $result = $query->executeQuery(); - - $id = (int) $result->fetchOne(); - $result->closeCursor(); - return $id; + $this->resourceManager->update(); + $this->roomManager->update(); } } diff --git a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php index ccd847970794c..8dc226095ec5c 100644 --- a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php +++ b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php @@ -31,21 +31,14 @@ use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob; -use OCA\DAV\CalDAV\CalDavBackend; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Calendar\BackendTemporarilyUnavailableException; -use OCP\Calendar\IMetadataProvider; -use OCP\Calendar\Resource\IBackend; use OCP\Calendar\Resource\IManager as IResourceManager; -use OCP\Calendar\Resource\IResource; use OCP\Calendar\Room\IManager as IRoomManager; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; -interface tmpI extends IResource, IMetadataProvider { -} - class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase { + private UpdateCalendarResourcesRoomsBackgroundJob $backgroundJob; /** @var ITimeFactory|MockObject */ private $time; @@ -56,402 +49,26 @@ class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase { /** @var IRoomManager|MockObject */ private $roomManager; - /** @var CalDavBackend|MockObject */ - private $calDavBackend; - - /** @var UpdateCalendarResourcesRoomsBackgroundJob */ - private $backgroundJob; - protected function setUp(): void { parent::setUp(); $this->time = $this->createMock(ITimeFactory::class); $this->resourceManager = $this->createMock(IResourceManager::class); $this->roomManager = $this->createMock(IRoomManager::class); - $this->calDavBackend = $this->createMock(CalDavBackend::class); $this->backgroundJob = new UpdateCalendarResourcesRoomsBackgroundJob( $this->time, $this->resourceManager, $this->roomManager, - self::$realDatabase, - $this->calDavBackend ); } - protected function tearDown(): void { - $query = self::$realDatabase->getQueryBuilder(); - $query->delete('calendar_resources')->execute(); - $query->delete('calendar_resources_md')->execute(); - $query->delete('calendar_rooms')->execute(); - $query->delete('calendar_rooms_md')->execute(); - } - - /** - * Data in Cache: - * resources: - * [backend1, res1, Beamer1, {}] - [] - * [backend1, res2, TV1, {}] - [] - * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2'] - * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old'] - * [backend3, res5, Beamer3, {}] - [] - * [backend3, res6, Pointer, {foo, bar}] - ['meta99' => 'value99'] - * - * Data in Backend: - * backend1 gone - * backend2 throws BackendTemporarilyUnavailableException - * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456'] - * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1'] - * [backend4, res8, Beamer, {}] - ['meta2' => 'value2'] - * [backend4, res9, Beamer2, {}] - [] - * - * Expected after run: - * [backend1, res1, Beamer1, {}] - [] - * [backend1, res2, TV1, {}] - [] - * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2'] - * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old'] - * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456'] - * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1'] - * [backend4, res8, Beamer, {}] - ['meta2' => 'value2'] - * [backend4, res9, Beamer2, {}] - [] - */ - public function testRun(): void { - $this->createTestResourcesInCache(); - - $backend2 = $this->createMock(IBackend::class); - $backend3 = $this->createMock(IBackend::class); - $backend4 = $this->createMock(IBackend::class); - - $res6 = $this->createMock(tmpI::class); - $res7 = $this->createMock(tmpI::class); - $res8 = $this->createMock(tmpI::class); - $res9 = $this->createMock(IResource::class); - - $backend2->method('getBackendIdentifier') - ->willReturn('backend2'); - $backend2->method('listAllResources') - ->will($this->throwException(new BackendTemporarilyUnavailableException())); - $backend2->method('getResource') - ->will($this->throwException(new BackendTemporarilyUnavailableException())); - $backend2->method('getAllResources') - ->will($this->throwException(new BackendTemporarilyUnavailableException())); - $backend3->method('getBackendIdentifier') - ->willReturn('backend3'); - $backend3->method('listAllResources') - ->willReturn(['res6', 'res7']); - $backend3->method('getResource') - ->willReturnMap([ - ['res6', $res6], - ['res7', $res7], - ]); - $backend4->method('getBackendIdentifier') - ->willReturn('backend4'); - $backend4->method('listAllResources') - ->willReturn(['res8', 'res9']); - $backend4->method('getResource') - ->willReturnMap([ - ['res8', $res8], - ['res9', $res9], - ]); - - $res6->method('getId')->willReturn('res6'); - $res6->method('getDisplayName')->willReturn('Pointer123'); - $res6->method('getGroupRestrictions')->willReturn(['foo', 'biz']); - $res6->method('getEMail')->willReturn('res6@foo.bar'); - $res6->method('getBackend')->willReturn($backend3); - - $res6->method('getAllAvailableMetadataKeys')->willReturn(['meta99', 'meta123']); - $res6->method('getMetadataForKey')->willReturnCallback(function ($key) { - switch ($key) { - case 'meta99': - return 'value99-new'; - - case 'meta123': - return 'meta456'; - - default: - return null; - } - }); - - $res7->method('getId')->willReturn('res7'); - $res7->method('getDisplayName')->willReturn('Resource4'); - $res7->method('getGroupRestrictions')->willReturn(['biz']); - $res7->method('getEMail')->willReturn('res7@foo.bar'); - $res7->method('getBackend')->willReturn($backend3); - $res7->method('getAllAvailableMetadataKeys')->willReturn(['meta1']); - $res7->method('getMetadataForKey')->willReturnCallback(function ($key) { - switch ($key) { - case 'meta1': - return 'value1'; - - default: - return null; - } - }); - - $res8->method('getId')->willReturn('res8'); - $res8->method('getDisplayName')->willReturn('Beamer'); - $res8->method('getGroupRestrictions')->willReturn([]); - $res8->method('getEMail')->willReturn('res8@foo.bar'); - $res8->method('getBackend')->willReturn($backend4); - $res8->method('getAllAvailableMetadataKeys')->willReturn(['meta2']); - $res8->method('getMetadataForKey')->willReturnCallback(function ($key) { - switch ($key) { - case 'meta2': - return 'value2'; - - default: - return null; - } - }); - - $res9->method('getId')->willReturn('res9'); - $res9->method('getDisplayName')->willReturn('Beamer2'); - $res9->method('getGroupRestrictions')->willReturn([]); - $res9->method('getEMail')->willReturn('res9@foo.bar'); - $res9->method('getBackend')->willReturn($backend4); - - $this->resourceManager - ->method('getBackends') - ->willReturn([ - $backend2, $backend3, $backend4 - ]); - $this->resourceManager - ->method('getBackend') - ->willReturnMap([ - ['backend2', $backend2], - ['backend3', $backend3], - ['backend4', $backend4], - ]); + $this->resourceManager->expects(self::once()) + ->method('update'); + $this->roomManager->expects(self::once()) + ->method('update'); $this->backgroundJob->run([]); - - $query = self::$realDatabase->getQueryBuilder(); - $query->select('*')->from('calendar_resources'); - - $rows = []; - $ids = []; - $stmt = $query->execute(); - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - $ids[$row['backend_id'] . '::' . $row['resource_id']] = $row['id']; - unset($row['id']); - $rows[] = $row; - } - - $this->assertEquals([ - [ - 'backend_id' => 'backend1', - 'resource_id' => 'res1', - 'displayname' => 'Beamer1', - 'email' => 'res1@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend1', - 'resource_id' => 'res2', - 'displayname' => 'TV1', - 'email' => 'res2@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend2', - 'resource_id' => 'res3', - 'displayname' => 'Beamer2', - 'email' => 'res3@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend2', - 'resource_id' => 'res4', - 'displayname' => 'TV2', - 'email' => 'res4@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend3', - 'resource_id' => 'res6', - 'displayname' => 'Pointer123', - 'email' => 'res6@foo.bar', - 'group_restrictions' => '["foo","biz"]', - ], - [ - 'backend_id' => 'backend3', - 'resource_id' => 'res7', - 'displayname' => 'Resource4', - 'email' => 'res7@foo.bar', - 'group_restrictions' => '["biz"]', - ], - [ - 'backend_id' => 'backend4', - 'resource_id' => 'res8', - 'displayname' => 'Beamer', - 'email' => 'res8@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend4', - 'resource_id' => 'res9', - 'displayname' => 'Beamer2', - 'email' => 'res9@foo.bar', - 'group_restrictions' => '[]', - ], - ], $rows); - - $query2 = self::$realDatabase->getQueryBuilder(); - $query2->select('*')->from('calendar_resources_md'); - - $rows2 = []; - $stmt = $query2->execute(); - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - unset($row['id']); - $rows2[] = $row; - } - - $this->assertEquals([ - [ - 'resource_id' => $ids['backend2::res3'], - 'key' => 'meta1', - 'value' => 'value1', - ], - [ - 'resource_id' => $ids['backend2::res3'], - 'key' => 'meta2', - 'value' => 'value2', - ], - [ - 'resource_id' => $ids['backend2::res4'], - 'key' => 'meta1', - 'value' => 'value1', - ], - [ - 'resource_id' => $ids['backend2::res4'], - 'key' => 'meta3', - 'value' => 'value3-old', - ], - [ - 'resource_id' => $ids['backend3::res6'], - 'key' => 'meta99', - 'value' => 'value99-new', - ], - [ - 'resource_id' => $ids['backend3::res7'], - 'key' => 'meta1', - 'value' => 'value1', - ], - [ - 'resource_id' => $ids['backend3::res6'], - 'key' => 'meta123', - 'value' => 'meta456', - ], - [ - 'resource_id' => $ids['backend4::res8'], - 'key' => 'meta2', - 'value' => 'value2', - ] - ], $rows2); - } - - protected function createTestResourcesInCache() { - $query = self::$realDatabase->getQueryBuilder(); - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend1'), - 'resource_id' => $query->createNamedParameter('res1'), - 'email' => $query->createNamedParameter('res1@foo.bar'), - 'displayname' => $query->createNamedParameter('Beamer1'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->execute(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend1'), - 'resource_id' => $query->createNamedParameter('res2'), - 'email' => $query->createNamedParameter('res2@foo.bar'), - 'displayname' => $query->createNamedParameter('TV1'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->execute(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend2'), - 'resource_id' => $query->createNamedParameter('res3'), - 'email' => $query->createNamedParameter('res3@foo.bar'), - 'displayname' => $query->createNamedParameter('Beamer2'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->execute(); - $id3 = $query->getLastInsertId(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend2'), - 'resource_id' => $query->createNamedParameter('res4'), - 'email' => $query->createNamedParameter('res4@foo.bar'), - 'displayname' => $query->createNamedParameter('TV2'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->execute(); - $id4 = $query->getLastInsertId(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend3'), - 'resource_id' => $query->createNamedParameter('res5'), - 'email' => $query->createNamedParameter('res5@foo.bar'), - 'displayname' => $query->createNamedParameter('Beamer3'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->execute(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend3'), - 'resource_id' => $query->createNamedParameter('res6'), - 'email' => $query->createNamedParameter('res6@foo.bar'), - 'displayname' => $query->createNamedParameter('Pointer'), - 'group_restrictions' => $query->createNamedParameter('["foo", "bar"]'), - ]) - ->execute(); - $id6 = $query->getLastInsertId(); - - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id3), - 'key' => $query->createNamedParameter('meta1'), - 'value' => $query->createNamedParameter('value1') - ]) - ->execute(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id3), - 'key' => $query->createNamedParameter('meta2'), - 'value' => $query->createNamedParameter('value2') - ]) - ->execute(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id4), - 'key' => $query->createNamedParameter('meta1'), - 'value' => $query->createNamedParameter('value1') - ]) - ->execute(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id4), - 'key' => $query->createNamedParameter('meta3'), - 'value' => $query->createNamedParameter('value3-old') - ]) - ->execute(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id6), - 'key' => $query->createNamedParameter('meta99'), - 'value' => $query->createNamedParameter('value99') - ]) - ->execute(); } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8e1408e121ef6..b577cde3c0d21 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1018,6 +1018,7 @@ 'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\Manager' => $baseDir . '/lib/private/Calendar/Manager.php', 'OC\\Calendar\\Resource\\Manager' => $baseDir . '/lib/private/Calendar/Resource/Manager.php', + 'OC\\Calendar\\ResourcesRoomsUpdater' => $baseDir . '/lib/private/Calendar/ResourcesRoomsUpdater.php', 'OC\\Calendar\\Room\\Manager' => $baseDir . '/lib/private/Calendar/Room/Manager.php', 'OC\\CapabilitiesManager' => $baseDir . '/lib/private/CapabilitiesManager.php', 'OC\\Collaboration\\AutoComplete\\Manager' => $baseDir . '/lib/private/Collaboration/AutoComplete/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d6939ae36ce8b..c87b11bd401f4 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1059,6 +1059,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Manager.php', 'OC\\Calendar\\Resource\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Resource/Manager.php', + 'OC\\Calendar\\ResourcesRoomsUpdater' => __DIR__ . '/../../..' . '/lib/private/Calendar/ResourcesRoomsUpdater.php', 'OC\\Calendar\\Room\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Room/Manager.php', 'OC\\CapabilitiesManager' => __DIR__ . '/../../..' . '/lib/private/CapabilitiesManager.php', 'OC\\Collaboration\\AutoComplete\\Manager' => __DIR__ . '/../../..' . '/lib/private/Collaboration/AutoComplete/Manager.php', diff --git a/lib/private/Calendar/Resource/Manager.php b/lib/private/Calendar/Resource/Manager.php index 870409c9ba51e..db04e6a648a74 100644 --- a/lib/private/Calendar/Resource/Manager.php +++ b/lib/private/Calendar/Resource/Manager.php @@ -9,6 +9,7 @@ namespace OC\Calendar\Resource; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Calendar\ResourcesRoomsUpdater; use OCP\Calendar\Resource\IBackend; use OCP\Calendar\Resource\IManager; use OCP\IServerContainer; @@ -28,6 +29,7 @@ class Manager implements IManager { public function __construct( private Coordinator $bootstrapCoordinator, private IServerContainer $server, + private ResourcesRoomsUpdater $updater, ) { } @@ -108,4 +110,8 @@ public function clear(): void { $this->backends = []; $this->initializedBackends = []; } + + public function update(): void { + $this->updater->updateResources(); + } } diff --git a/lib/private/Calendar/ResourcesRoomsUpdater.php b/lib/private/Calendar/ResourcesRoomsUpdater.php new file mode 100644 index 0000000000000..ae2a2f3a65064 --- /dev/null +++ b/lib/private/Calendar/ResourcesRoomsUpdater.php @@ -0,0 +1,413 @@ +updateFromBackend( + $this->container->get(IResourceManager::class), + 'calendar_resources', + 'calendar_resources_md', + 'resource_id', + 'principals/calendar-resources' + ); + } + + /** + * Update room cache from backends + */ + public function updateRooms(): void { + $this->updateFromBackend( + $this->container->get(IRoomManager::class), + 'calendar_rooms', + 'calendar_rooms_md', + 'room_id', + 'principals/calendar-rooms' + ); + } + + /** + * Update cache from one specific backend manager, either ResourceManager or RoomManager + * + * @param IResourceManager|IRoomManager $backendManager + */ + private function updateFromBackend($backendManager, + string $dbTable, + string $dbTableMetadata, + string $foreignKey, + string $principalPrefix): void { + $backends = $backendManager->getBackends(); + + foreach ($backends as $backend) { + $backendId = $backend->getBackendIdentifier(); + + try { + if ($backend instanceof IResourceBackend) { + $list = $backend->listAllResources(); + } else { + $list = $backend->listAllRooms(); + } + } catch (BackendTemporarilyUnavailableException $ex) { + continue; + } + + $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); + $newIds = array_diff($list, $cachedList); + $deletedIds = array_diff($cachedList, $list); + $editedIds = array_intersect($list, $cachedList); + + foreach ($newIds as $newId) { + try { + if ($backend instanceof IResourceBackend) { + $resource = $backend->getResource($newId); + } else { + $resource = $backend->getRoom($newId); + } + + $metadata = []; + if ($resource instanceof IMetadataProvider) { + $metadata = $this->getAllMetadataOfBackend($resource); + } + } catch (BackendTemporarilyUnavailableException $ex) { + continue; + } + + $id = $this->addToCache($dbTable, $backendId, $resource); + $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); + // we don't create the calendar here, it is created lazily + // when an event is actually scheduled with this resource / room + } + + foreach ($deletedIds as $deletedId) { + $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); + $this->deleteFromCache($dbTable, $id); + $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); + + $principalName = implode('-', [$backendId, $deletedId]); + $this->deleteCalendarDataForResource($principalPrefix, $principalName); + } + + foreach ($editedIds as $editedId) { + $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); + + try { + if ($backend instanceof IResourceBackend) { + $resource = $backend->getResource($editedId); + } else { + $resource = $backend->getRoom($editedId); + } + + $metadata = []; + if ($resource instanceof IMetadataProvider) { + $metadata = $this->getAllMetadataOfBackend($resource); + } + } catch (BackendTemporarilyUnavailableException $ex) { + continue; + } + + $this->updateCache($dbTable, $id, $resource); + + if ($resource instanceof IMetadataProvider) { + $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); + $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); + } + } + } + } + + /** + * add entry to cache that exists remotely but not yet in cache + * + * @param string $table + * @param string $backendId + * @param IResource|IRoom $remote + * + * @return int Insert id + */ + private function addToCache(string $table, + string $backendId, + $remote): int { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert($table) + ->values([ + 'backend_id' => $query->createNamedParameter($backendId), + 'resource_id' => $query->createNamedParameter($remote->getId()), + 'email' => $query->createNamedParameter($remote->getEMail()), + 'displayname' => $query->createNamedParameter($remote->getDisplayName()), + 'group_restrictions' => $query->createNamedParameter( + $this->serializeGroupRestrictions( + $remote->getGroupRestrictions() + )) + ]) + ->executeStatement(); + return $query->getLastInsertId(); + } + + /** + * @param string $table + * @param string $foreignKey + * @param int $foreignId + * @param array $metadata + */ + private function addMetadataToCache(string $table, + string $foreignKey, + int $foreignId, + array $metadata): void { + foreach ($metadata as $key => $value) { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert($table) + ->values([ + $foreignKey => $query->createNamedParameter($foreignId), + 'key' => $query->createNamedParameter($key), + 'value' => $query->createNamedParameter($value), + ]) + ->executeStatement(); + } + } + + /** + * delete entry from cache that does not exist anymore remotely + * + * @param string $table + * @param int $id + */ + private function deleteFromCache(string $table, + int $id): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete($table) + ->where($query->expr()->eq('id', $query->createNamedParameter($id))) + ->executeStatement(); + } + + /** + * @param string $table + * @param string $foreignKey + * @param int $id + */ + private function deleteMetadataFromCache(string $table, + string $foreignKey, + int $id): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete($table) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) + ->executeStatement(); + } + + /** + * update an existing entry in cache + * + * @param string $table + * @param int $id + * @param IResource|IRoom $remote + */ + private function updateCache(string $table, + int $id, + $remote): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->update($table) + ->set('email', $query->createNamedParameter($remote->getEMail())) + ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) + ->set('group_restrictions', $query->createNamedParameter( + $this->serializeGroupRestrictions( + $remote->getGroupRestrictions() + ))) + ->where($query->expr()->eq('id', $query->createNamedParameter($id))) + ->executeStatement(); + } + + /** + * @param string $dbTable + * @param string $foreignKey + * @param int $id + * @param array $metadata + * @param array $cachedMetadata + */ + private function updateMetadataCache(string $dbTable, + string $foreignKey, + int $id, + array $metadata, + array $cachedMetadata): void { + $newMetadata = array_diff_key($metadata, $cachedMetadata); + $deletedMetadata = array_diff_key($cachedMetadata, $metadata); + + foreach ($newMetadata as $key => $value) { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert($dbTable) + ->values([ + $foreignKey => $query->createNamedParameter($id), + 'key' => $query->createNamedParameter($key), + 'value' => $query->createNamedParameter($value), + ]) + ->executeStatement(); + } + + foreach ($deletedMetadata as $key => $value) { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete($dbTable) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) + ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) + ->executeStatement(); + } + + $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); + foreach ($existingKeys as $existingKey) { + if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { + $query = $this->dbConnection->getQueryBuilder(); + $query->update($dbTable) + ->set('value', $query->createNamedParameter($metadata[$existingKey])) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) + ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) + ->executeStatement(); + } + } + } + + /** + * serialize array of group restrictions to store them in database + * + * @param array $groups + * + * @return string + */ + private function serializeGroupRestrictions(array $groups): string { + return \json_encode($groups, JSON_THROW_ON_ERROR); + } + + /** + * Gets all metadata of a backend + * + * @param IResource|IRoom $resource + * + * @return array + */ + private function getAllMetadataOfBackend($resource): array { + if (!($resource instanceof IMetadataProvider)) { + return []; + } + + $keys = $resource->getAllAvailableMetadataKeys(); + $metadata = []; + foreach ($keys as $key) { + $metadata[$key] = $resource->getMetadataForKey($key); + } + + return $metadata; + } + + /** + * @param string $table + * @param string $foreignKey + * @param int $id + * + * @return array + */ + private function getAllMetadataOfCache(string $table, + string $foreignKey, + int $id): array { + $query = $this->dbConnection->getQueryBuilder(); + $query->select(['key', 'value']) + ->from($table) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); + $result = $query->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + $metadata = []; + foreach ($rows as $row) { + $metadata[$row['key']] = $row['value']; + } + + return $metadata; + } + + /** + * Gets all cached rooms / resources by backend + * + * @param $tableName + * @param $backendId + * + * @return array + */ + private function getAllCachedByBackend(string $tableName, + string $backendId): array { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('resource_id') + ->from($tableName) + ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); + $result = $query->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + return array_map(function ($row): string { + return $row['resource_id']; + }, $rows); + } + + /** + * @param $principalPrefix + * @param $principalUri + */ + private function deleteCalendarDataForResource(string $principalPrefix, + string $principalUri): void { + $calendar = $this->calDavBackend->getCalendarByUri( + implode('/', [$principalPrefix, $principalUri]), + CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); + + if ($calendar !== null) { + $this->calDavBackend->deleteCalendar( + $calendar['id'], + true // Because this wasn't deleted by a user + ); + } + } + + /** + * @param $table + * @param $backendId + * @param $resourceId + * + * @return int + */ + private function getIdForBackendAndResource(string $table, + string $backendId, + string $resourceId): int { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('id') + ->from($table) + ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) + ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); + $result = $query->executeQuery(); + + $id = (int) $result->fetchOne(); + $result->closeCursor(); + return $id; + } +} diff --git a/lib/private/Calendar/Room/Manager.php b/lib/private/Calendar/Room/Manager.php index e5a6076591926..65897010f2a6a 100644 --- a/lib/private/Calendar/Room/Manager.php +++ b/lib/private/Calendar/Room/Manager.php @@ -9,6 +9,7 @@ namespace OC\Calendar\Room; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Calendar\ResourcesRoomsUpdater; use OCP\Calendar\Room\IBackend; use OCP\Calendar\Room\IManager; use OCP\IServerContainer; @@ -28,6 +29,7 @@ class Manager implements IManager { public function __construct( private Coordinator $bootstrapCoordinator, private IServerContainer $server, + private ResourcesRoomsUpdater $updater, ) { } @@ -115,4 +117,8 @@ public function clear(): void { $this->backends = []; $this->initializedBackends = []; } + + public function update(): void { + $this->updater->updateRooms(); + } } diff --git a/lib/public/Calendar/Resource/IManager.php b/lib/public/Calendar/Resource/IManager.php index 7b2fe2e230cc8..d3a6b2449e35e 100644 --- a/lib/public/Calendar/Resource/IManager.php +++ b/lib/public/Calendar/Resource/IManager.php @@ -10,7 +10,6 @@ /** * @since 14.0.0 - * @deprecated 24.0.0 */ interface IManager { /** @@ -55,4 +54,11 @@ public function getBackend($backendId); * @deprecated 24.0.0 */ public function clear(); + + /** + * Update all resources from all backends right now. + * + * @since 30.0.0 + */ + public function update(): void; } diff --git a/lib/public/Calendar/Room/IManager.php b/lib/public/Calendar/Room/IManager.php index 7c750f4e45733..b1136ede3fe77 100644 --- a/lib/public/Calendar/Room/IManager.php +++ b/lib/public/Calendar/Room/IManager.php @@ -10,7 +10,6 @@ /** * @since 14.0.0 - * @deprecated 24.0.0 */ interface IManager { /** @@ -55,4 +54,11 @@ public function getBackend($backendId); * @deprecated 24.0.0 */ public function clear(); + + /** + * Update all rooms from all backends right now. + * + * @since 30.0.0 + */ + public function update(): void; } diff --git a/tests/lib/Calendar/Resource/ManagerTest.php b/tests/lib/Calendar/Resource/ManagerTest.php index d9d6669971001..15977765da761 100644 --- a/tests/lib/Calendar/Resource/ManagerTest.php +++ b/tests/lib/Calendar/Resource/ManagerTest.php @@ -13,6 +13,7 @@ use OC\AppFramework\Bootstrap\RegistrationContext; use OC\AppFramework\Bootstrap\ServiceRegistration; use OC\Calendar\Resource\Manager; +use OC\Calendar\ResourcesRoomsUpdater; use OCP\Calendar\Resource\IBackend; use OCP\IServerContainer; use PHPUnit\Framework\MockObject\MockObject; @@ -25,6 +26,9 @@ class ManagerTest extends TestCase { /** @var IServerContainer|MockObject */ private $server; + /** @var ResourcesRoomsUpdater|MockObject */ + private $resourcesRoomsUpdater; + /** @var Manager */ private $manager; @@ -33,9 +37,12 @@ protected function setUp(): void { $this->coordinator = $this->createMock(Coordinator::class); $this->server = $this->createMock(IServerContainer::class); + $this->resourcesRoomsUpdater = $this->createMock(ResourcesRoomsUpdater::class); + $this->manager = new Manager( $this->coordinator, $this->server, + $this->resourcesRoomsUpdater, ); } @@ -126,4 +133,11 @@ public function testClear(): void { self::assertEquals([], $this->manager->getBackends()); } + + public function testUpdate(): void { + $this->resourcesRoomsUpdater->expects(self::once()) + ->method('updateResources'); + + $this->manager->update(); + } } diff --git a/tests/lib/Calendar/ResourcesRoomsUpdaterTest.php b/tests/lib/Calendar/ResourcesRoomsUpdaterTest.php new file mode 100644 index 0000000000000..7776ba8cd3a3a --- /dev/null +++ b/tests/lib/Calendar/ResourcesRoomsUpdaterTest.php @@ -0,0 +1,439 @@ +resourceManager = $this->createMock(IResourceManager::class); + $this->roomManager = $this->createMock(IRoomManager::class); + $this->container = $this->createMock(ContainerInterface::class); + $this->calDavBackend = $this->createMock(CalDavBackend::class); + + $this->container->method('get') + ->willReturnMap([ + [IResourceManager::class, $this->resourceManager], + [IRoomManager::class, $this->roomManager], + ]); + + $this->updater = new ResourcesRoomsUpdater( + $this->container, + self::$realDatabase, + $this->calDavBackend + ); + } + + protected function tearDown(): void { + $query = self::$realDatabase->getQueryBuilder(); + $query->delete('calendar_resources')->execute(); + $query->delete('calendar_resources_md')->execute(); + $query->delete('calendar_rooms')->execute(); + $query->delete('calendar_rooms_md')->execute(); + } + + /** + * Data in Cache: + * resources: + * [backend1, res1, Beamer1, {}] - [] + * [backend1, res2, TV1, {}] - [] + * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2'] + * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old'] + * [backend3, res5, Beamer3, {}] - [] + * [backend3, res6, Pointer, {foo, bar}] - ['meta99' => 'value99'] + * + * Data in Backend: + * backend1 gone + * backend2 throws BackendTemporarilyUnavailableException + * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456'] + * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1'] + * [backend4, res8, Beamer, {}] - ['meta2' => 'value2'] + * [backend4, res9, Beamer2, {}] - [] + * + * Expected after run: + * [backend1, res1, Beamer1, {}] - [] + * [backend1, res2, TV1, {}] - [] + * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2'] + * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old'] + * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456'] + * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1'] + * [backend4, res8, Beamer, {}] - ['meta2' => 'value2'] + * [backend4, res9, Beamer2, {}] - [] + */ + + public function testUpdateBoth(): void { + $this->createTestResourcesInCache(); + + $backend2 = $this->createMock(IBackend::class); + $backend3 = $this->createMock(IBackend::class); + $backend4 = $this->createMock(IBackend::class); + + $res6 = $this->createMock(tmpI::class); + $res7 = $this->createMock(tmpI::class); + $res8 = $this->createMock(tmpI::class); + $res9 = $this->createMock(IResource::class); + + $backend2->method('getBackendIdentifier') + ->willReturn('backend2'); + $backend2->method('listAllResources') + ->will($this->throwException(new BackendTemporarilyUnavailableException())); + $backend2->method('getResource') + ->will($this->throwException(new BackendTemporarilyUnavailableException())); + $backend2->method('getAllResources') + ->will($this->throwException(new BackendTemporarilyUnavailableException())); + $backend3->method('getBackendIdentifier') + ->willReturn('backend3'); + $backend3->method('listAllResources') + ->willReturn(['res6', 'res7']); + $backend3->method('getResource') + ->willReturnMap([ + ['res6', $res6], + ['res7', $res7], + ]); + $backend4->method('getBackendIdentifier') + ->willReturn('backend4'); + $backend4->method('listAllResources') + ->willReturn(['res8', 'res9']); + $backend4->method('getResource') + ->willReturnMap([ + ['res8', $res8], + ['res9', $res9], + ]); + + $res6->method('getId')->willReturn('res6'); + $res6->method('getDisplayName')->willReturn('Pointer123'); + $res6->method('getGroupRestrictions')->willReturn(['foo', 'biz']); + $res6->method('getEMail')->willReturn('res6@foo.bar'); + $res6->method('getBackend')->willReturn($backend3); + + $res6->method('getAllAvailableMetadataKeys')->willReturn(['meta99', 'meta123']); + $res6->method('getMetadataForKey')->willReturnCallback(function ($key) { + switch ($key) { + case 'meta99': + return 'value99-new'; + + case 'meta123': + return 'meta456'; + + default: + return null; + } + }); + + $res7->method('getId')->willReturn('res7'); + $res7->method('getDisplayName')->willReturn('Resource4'); + $res7->method('getGroupRestrictions')->willReturn(['biz']); + $res7->method('getEMail')->willReturn('res7@foo.bar'); + $res7->method('getBackend')->willReturn($backend3); + $res7->method('getAllAvailableMetadataKeys')->willReturn(['meta1']); + $res7->method('getMetadataForKey')->willReturnCallback(function ($key) { + switch ($key) { + case 'meta1': + return 'value1'; + + default: + return null; + } + }); + + $res8->method('getId')->willReturn('res8'); + $res8->method('getDisplayName')->willReturn('Beamer'); + $res8->method('getGroupRestrictions')->willReturn([]); + $res8->method('getEMail')->willReturn('res8@foo.bar'); + $res8->method('getBackend')->willReturn($backend4); + $res8->method('getAllAvailableMetadataKeys')->willReturn(['meta2']); + $res8->method('getMetadataForKey')->willReturnCallback(function ($key) { + switch ($key) { + case 'meta2': + return 'value2'; + + default: + return null; + } + }); + + $res9->method('getId')->willReturn('res9'); + $res9->method('getDisplayName')->willReturn('Beamer2'); + $res9->method('getGroupRestrictions')->willReturn([]); + $res9->method('getEMail')->willReturn('res9@foo.bar'); + $res9->method('getBackend')->willReturn($backend4); + + $this->resourceManager + ->method('getBackends') + ->willReturn([ + $backend2, $backend3, $backend4 + ]); + $this->resourceManager + ->method('getBackend') + ->willReturnMap([ + ['backend2', $backend2], + ['backend3', $backend3], + ['backend4', $backend4], + ]); + + $this->updater->updateResources(); + $this->updater->updateRooms(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->select('*')->from('calendar_resources'); + + $rows = []; + $ids = []; + $stmt = $query->execute(); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $ids[$row['backend_id'] . '::' . $row['resource_id']] = $row['id']; + unset($row['id']); + $rows[] = $row; + } + + $this->assertEquals([ + [ + 'backend_id' => 'backend1', + 'resource_id' => 'res1', + 'displayname' => 'Beamer1', + 'email' => 'res1@foo.bar', + 'group_restrictions' => '[]', + ], + [ + 'backend_id' => 'backend1', + 'resource_id' => 'res2', + 'displayname' => 'TV1', + 'email' => 'res2@foo.bar', + 'group_restrictions' => '[]', + ], + [ + 'backend_id' => 'backend2', + 'resource_id' => 'res3', + 'displayname' => 'Beamer2', + 'email' => 'res3@foo.bar', + 'group_restrictions' => '[]', + ], + [ + 'backend_id' => 'backend2', + 'resource_id' => 'res4', + 'displayname' => 'TV2', + 'email' => 'res4@foo.bar', + 'group_restrictions' => '[]', + ], + [ + 'backend_id' => 'backend3', + 'resource_id' => 'res6', + 'displayname' => 'Pointer123', + 'email' => 'res6@foo.bar', + 'group_restrictions' => '["foo","biz"]', + ], + [ + 'backend_id' => 'backend3', + 'resource_id' => 'res7', + 'displayname' => 'Resource4', + 'email' => 'res7@foo.bar', + 'group_restrictions' => '["biz"]', + ], + [ + 'backend_id' => 'backend4', + 'resource_id' => 'res8', + 'displayname' => 'Beamer', + 'email' => 'res8@foo.bar', + 'group_restrictions' => '[]', + ], + [ + 'backend_id' => 'backend4', + 'resource_id' => 'res9', + 'displayname' => 'Beamer2', + 'email' => 'res9@foo.bar', + 'group_restrictions' => '[]', + ], + ], $rows); + + $query2 = self::$realDatabase->getQueryBuilder(); + $query2->select('*')->from('calendar_resources_md'); + + $rows2 = []; + $stmt = $query2->execute(); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + unset($row['id']); + $rows2[] = $row; + } + + $this->assertEquals([ + [ + 'resource_id' => $ids['backend2::res3'], + 'key' => 'meta1', + 'value' => 'value1', + ], + [ + 'resource_id' => $ids['backend2::res3'], + 'key' => 'meta2', + 'value' => 'value2', + ], + [ + 'resource_id' => $ids['backend2::res4'], + 'key' => 'meta1', + 'value' => 'value1', + ], + [ + 'resource_id' => $ids['backend2::res4'], + 'key' => 'meta3', + 'value' => 'value3-old', + ], + [ + 'resource_id' => $ids['backend3::res6'], + 'key' => 'meta99', + 'value' => 'value99-new', + ], + [ + 'resource_id' => $ids['backend3::res7'], + 'key' => 'meta1', + 'value' => 'value1', + ], + [ + 'resource_id' => $ids['backend3::res6'], + 'key' => 'meta123', + 'value' => 'meta456', + ], + [ + 'resource_id' => $ids['backend4::res8'], + 'key' => 'meta2', + 'value' => 'value2', + ] + ], $rows2); + } + + protected function createTestResourcesInCache() { + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_resources') + ->values([ + 'backend_id' => $query->createNamedParameter('backend1'), + 'resource_id' => $query->createNamedParameter('res1'), + 'email' => $query->createNamedParameter('res1@foo.bar'), + 'displayname' => $query->createNamedParameter('Beamer1'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + + $query->insert('calendar_resources') + ->values([ + 'backend_id' => $query->createNamedParameter('backend1'), + 'resource_id' => $query->createNamedParameter('res2'), + 'email' => $query->createNamedParameter('res2@foo.bar'), + 'displayname' => $query->createNamedParameter('TV1'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + + $query->insert('calendar_resources') + ->values([ + 'backend_id' => $query->createNamedParameter('backend2'), + 'resource_id' => $query->createNamedParameter('res3'), + 'email' => $query->createNamedParameter('res3@foo.bar'), + 'displayname' => $query->createNamedParameter('Beamer2'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + $id3 = $query->getLastInsertId(); + + $query->insert('calendar_resources') + ->values([ + 'backend_id' => $query->createNamedParameter('backend2'), + 'resource_id' => $query->createNamedParameter('res4'), + 'email' => $query->createNamedParameter('res4@foo.bar'), + 'displayname' => $query->createNamedParameter('TV2'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + $id4 = $query->getLastInsertId(); + + $query->insert('calendar_resources') + ->values([ + 'backend_id' => $query->createNamedParameter('backend3'), + 'resource_id' => $query->createNamedParameter('res5'), + 'email' => $query->createNamedParameter('res5@foo.bar'), + 'displayname' => $query->createNamedParameter('Beamer3'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + + $query->insert('calendar_resources') + ->values([ + 'backend_id' => $query->createNamedParameter('backend3'), + 'resource_id' => $query->createNamedParameter('res6'), + 'email' => $query->createNamedParameter('res6@foo.bar'), + 'displayname' => $query->createNamedParameter('Pointer'), + 'group_restrictions' => $query->createNamedParameter('["foo", "bar"]'), + ]) + ->execute(); + $id6 = $query->getLastInsertId(); + + $query->insert('calendar_resources_md') + ->values([ + 'resource_id' => $query->createNamedParameter($id3), + 'key' => $query->createNamedParameter('meta1'), + 'value' => $query->createNamedParameter('value1') + ]) + ->execute(); + $query->insert('calendar_resources_md') + ->values([ + 'resource_id' => $query->createNamedParameter($id3), + 'key' => $query->createNamedParameter('meta2'), + 'value' => $query->createNamedParameter('value2') + ]) + ->execute(); + $query->insert('calendar_resources_md') + ->values([ + 'resource_id' => $query->createNamedParameter($id4), + 'key' => $query->createNamedParameter('meta1'), + 'value' => $query->createNamedParameter('value1') + ]) + ->execute(); + $query->insert('calendar_resources_md') + ->values([ + 'resource_id' => $query->createNamedParameter($id4), + 'key' => $query->createNamedParameter('meta3'), + 'value' => $query->createNamedParameter('value3-old') + ]) + ->execute(); + $query->insert('calendar_resources_md') + ->values([ + 'resource_id' => $query->createNamedParameter($id6), + 'key' => $query->createNamedParameter('meta99'), + 'value' => $query->createNamedParameter('value99') + ]) + ->execute(); + } +} diff --git a/tests/lib/Calendar/Room/ManagerTest.php b/tests/lib/Calendar/Room/ManagerTest.php index 587503da31249..e0f2de970ed63 100644 --- a/tests/lib/Calendar/Room/ManagerTest.php +++ b/tests/lib/Calendar/Room/ManagerTest.php @@ -12,6 +12,7 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\RegistrationContext; use OC\AppFramework\Bootstrap\ServiceRegistration; +use OC\Calendar\ResourcesRoomsUpdater; use OC\Calendar\Room\Manager; use OCP\Calendar\Room\IBackend; use OCP\IServerContainer; @@ -25,6 +26,9 @@ class ManagerTest extends TestCase { /** @var IServerContainer|MockObject */ private $server; + /** @var ResourcesRoomsUpdater|MockObject */ + private $resourcesRoomsUpdater; + /** @var Manager */ private $manager; @@ -33,9 +37,12 @@ protected function setUp(): void { $this->coordinator = $this->createMock(Coordinator::class); $this->server = $this->createMock(IServerContainer::class); + $this->resourcesRoomsUpdater = $this->createMock(ResourcesRoomsUpdater::class); + $this->manager = new Manager( $this->coordinator, $this->server, + $this->resourcesRoomsUpdater, ); } @@ -128,4 +135,11 @@ public function testClear(): void { self::assertEquals([], $this->manager->getBackends()); } + + public function testUpdate(): void { + $this->resourcesRoomsUpdater->expects(self::once()) + ->method('updateRooms'); + + $this->manager->update(); + } }