From 0c3a7398d24c1114972b3742c4246fb8d44fb511 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 4 Sep 2026 00:35:56 +0200 Subject: [PATCH 1/2] feat: add `coalesce` to db function builder Signed-off-by: Robin Appelman --- .../DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php | 5 +++++ lib/public/DB/QueryBuilder/IFunctionBuilder.php | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php index 4653ee0c489fe..56f62733afe8d 100644 --- a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php +++ b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php @@ -118,4 +118,9 @@ public function least($x, $y): IQueryFunction { public function now(): IQueryFunction { return new QueryFunction('NOW()'); } + + #[Override] + public function coalesce($value, $default): IQueryFunction { + return new QueryFunction('COALESCE(' . $this->helper->quoteColumnName($value) . ', ' . $this->helper->quoteColumnName($default) . ')'); + } } diff --git a/lib/public/DB/QueryBuilder/IFunctionBuilder.php b/lib/public/DB/QueryBuilder/IFunctionBuilder.php index f792e05cf6046..c91e2d2331624 100644 --- a/lib/public/DB/QueryBuilder/IFunctionBuilder.php +++ b/lib/public/DB/QueryBuilder/IFunctionBuilder.php @@ -178,4 +178,12 @@ public function least($x, $y): IQueryFunction; * @since 34.0.0 */ public function now(): IQueryFunction; + + /** + * @param string|ILiteral|IParameter|IQueryFunction $value The value that might be null + * @param string|ILiteral|IParameter|IQueryFunction $default The value to use if the first one is null + * @return IQueryFunction + * @since 35.0.0 + */ + public function coalesce($value, $default): IQueryFunction; } From c0c712fc1e3bb094e999c3d2ba995d687e932d0f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 2 Sep 2026 00:55:35 +0200 Subject: [PATCH 2/2] feat: implement recipient api for unified sharing Signed-off-by: Robin Appelman --- .../lib/Controller/ApiV1Controller.php | 16 ++- apps/sharing/openapi.json | 2 +- apps/sharing/tests/Command/CommandTest.php | 15 +++ .../tests/Controller/ApiV1ControllerTest.php | 26 +++- lib/private/Sharing/SharingBackend.php | 106 ++++++++++++++- lib/private/Sharing/SharingManager.php | 11 ++ lib/unstable/Sharing/ISharingBackend.php | 20 +++ lib/unstable/Sharing/ISharingManager.php | 19 +++ openapi.json | 2 +- .../Sharing/AbstractSharingManagerTests.php | 126 +++++++++++++++++- tests/lib/Sharing/SharingManagerTest.php | 14 ++ 11 files changed, 348 insertions(+), 9 deletions(-) diff --git a/apps/sharing/lib/Controller/ApiV1Controller.php b/apps/sharing/lib/Controller/ApiV1Controller.php index 4af3e0b208171..370a4ba62aed4 100644 --- a/apps/sharing/lib/Controller/ApiV1Controller.php +++ b/apps/sharing/lib/Controller/ApiV1Controller.php @@ -26,6 +26,7 @@ use NCU\Sharing\Share; use NCU\Sharing\ShareAccessContext; use NCU\Sharing\ShareState; +use NCU\Sharing\ShareUser; use NCU\Sharing\ShareUserStatus; use NCU\Sharing\Source\IShareSourceType; use NCU\Sharing\Source\ShareSource; @@ -42,6 +43,7 @@ use OCP\IDBConnection; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; @@ -80,7 +82,7 @@ public function __construct( * Search for recipients that can be added to a share. * * @param ?list> $filterRecipientTypeClasses Type classes of recipients to filter by - * @param string $query The query to search for + * @param string $query The query to search for, if the query is empty, recommended recipients will be returned * @param int<1, 100> $limit The maximum number of participants * @param non-negative-int $offset The offset of the participants * @param ?string $id If provided, recipients that are already part of the share will not be returned. @@ -109,11 +111,21 @@ public function searchRecipients(?array $filterRecipientTypeClasses, string $que return new DataResponse('The offset is too low.', Http::STATUS_BAD_REQUEST); } + if (!$this->accessContext->currentUser instanceof IUser) { + throw new \RuntimeException('No user in session for endpoint that requires authentication'); + } + try { try { $this->dbConnection->beginTransaction(); $forShare = ($id === null) ? null : $this->manager->getShare($this->accessContext, $id); - $recipients = $this->manager->searchRecipients($this->accessContext, $filterRecipientTypeClasses, $query, $limit, $offset, $forShare); + if ($query !== '') { + $recipients = $this->manager->searchRecipients($this->accessContext, $filterRecipientTypeClasses, $query, $limit, $offset, $forShare); + } else { + $user = new ShareUser($this->accessContext->currentUser->getUID(), null); + $recipients = $this->manager->getRecipientsForUser($user, $filterRecipientTypeClasses, $forShare?->id, $limit, $offset); + } + $this->dbConnection->commit(); return new DataResponse(ShareRecipient::formatMultiple($this->registry, $this->l10nFactory, $this->urlGenerator, $this->userManager, $recipients)); } catch (Exception $exception) { diff --git a/apps/sharing/openapi.json b/apps/sharing/openapi.json index ea594b23fac58..089195d6b4879 100644 --- a/apps/sharing/openapi.json +++ b/apps/sharing/openapi.json @@ -655,7 +655,7 @@ { "name": "query", "in": "query", - "description": "The query to search for", + "description": "The query to search for, if the query is empty, recommended recipients will be returned", "required": true, "schema": { "type": "string" diff --git a/apps/sharing/tests/Command/CommandTest.php b/apps/sharing/tests/Command/CommandTest.php index 0abfac47dfb1f..6501703c5dd60 100644 --- a/apps/sharing/tests/Command/CommandTest.php +++ b/apps/sharing/tests/Command/CommandTest.php @@ -18,6 +18,7 @@ use NCU\Sharing\Share; use NCU\Sharing\ShareAccessContext; use NCU\Sharing\ShareState; +use NCU\Sharing\ShareUser; use NCU\Sharing\ShareUserStatus; use NCU\Sharing\Source\ShareSource; use OC\Core\Command\Base; @@ -455,4 +456,18 @@ protected function getShares(ShareAccessContext $accessContext, ?string $filterS /** @var SharingShare[] */ return json_decode($stdout, true, 512, JSON_THROW_ON_ERROR); } + + #[Override] + protected function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array { + // We don't have a command for this, so we just call the real manager to make the test pass. + /** @psalm-suppress ArgumentTypeCoercion */ + $recipients = $this->manager->getRecipientsForUser($user, $filterRecipientTypeClasses, $notInShare, $count, $offset); + return ShareRecipient::formatMultiple($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $recipients); + } } diff --git a/apps/sharing/tests/Controller/ApiV1ControllerTest.php b/apps/sharing/tests/Controller/ApiV1ControllerTest.php index ef15c6c01d350..adf12d9c553cd 100644 --- a/apps/sharing/tests/Controller/ApiV1ControllerTest.php +++ b/apps/sharing/tests/Controller/ApiV1ControllerTest.php @@ -17,6 +17,7 @@ use NCU\Sharing\Share; use NCU\Sharing\ShareAccessContext; use NCU\Sharing\ShareState; +use NCU\Sharing\ShareUser; use NCU\Sharing\ShareUserStatus; use NCU\Sharing\Source\ShareSource; use OCA\Sharing\Controller\ApiV1Controller; @@ -25,6 +26,7 @@ use OCP\IDBConnection; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; @@ -37,6 +39,7 @@ /** * @psalm-import-type SharingShare from Share + * @psalm-import-type SharingRecipient from Share */ #[Group(name: 'DB')] final class ApiV1ControllerTest extends AbstractSharingManagerTests { @@ -97,7 +100,10 @@ private function executeRequest(ShareAccessContext $accessContext, Closure $clos #[Override] protected function searchRecipients(ShareAccessContext $accessContext, ?array $filterRecipientTypeClasses, string $query, int $limit, int $offset, ?Share $forShare = null): array { - /** @psalm-suppress ArgumentTypeCoercion */ + /** + * @psalm-suppress ArgumentTypeCoercion + * @var SharingRecipient[] + */ return $this->executeRequest($accessContext, fn (ApiV1Controller $controller): DataResponse => $controller->searchRecipients($filterRecipientTypeClasses, $query, $limit, $offset, $forShare?->id)); } @@ -201,4 +207,22 @@ protected function getShares(ShareAccessContext $accessContext, ?string $filterS return $controller->getShares($filterSourceTypeClass, $filterSourceTypeValue, $filterState?->value, $filterUserStatus?->value, $lastShareID); }); } + + #[Override] + protected function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array { + $accessUser = $this->createMock(IUser::class); + $accessUser->method('getUID')->willReturn($user->userId); + + $accessContext = new ShareAccessContext($accessUser); + /** @var SharingRecipient[] */ + return $this->executeRequest($accessContext, + /** @psalm-suppress ArgumentTypeCoercion */ + fn (ApiV1Controller $controller): DataResponse => $controller->searchRecipients($filterRecipientTypeClasses, '', $count, $offset, $notInShare)); + } } diff --git a/lib/private/Sharing/SharingBackend.php b/lib/private/Sharing/SharingBackend.php index bc62456b12a2d..1b0338f70a340 100644 --- a/lib/private/Sharing/SharingBackend.php +++ b/lib/private/Sharing/SharingBackend.php @@ -1063,7 +1063,9 @@ private function list( $shareRecipientPermissions[$shareId] ??= []; $shareRecipientPermissions[$shareId][$recipientId] ??= []; - $shareRecipientPermissions[$shareId][$recipientId][$permissionTypeClass] = new SharePermission($permissionTypeClass, (bool)$row['permission_enabled']); + $shareRecipientPermissions[$shareId][$recipientId][$permissionTypeClass] = new SharePermission( + $permissionTypeClass, (bool)$row['permission_enabled'] + ); } } @@ -1165,7 +1167,9 @@ private function list( continue; } - if (array_intersect($registryPropertyTypeCompatibleSourceTypeClasses[$propertyTypeClass], array_keys($shareSourceTypeClasses[$shareId])) === []) { + if (array_intersect( + $registryPropertyTypeCompatibleSourceTypeClasses[$propertyTypeClass], array_keys($shareSourceTypeClasses[$shareId]) + ) === []) { // Skip properties that are currently not compatible, but don't remove them. continue; } @@ -1408,6 +1412,104 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi ); } + #[\Override] + public function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array { + $query = $this->connection->getTypedQueryBuilder(); + + // Add an `eq` constraint, or an `is null` constraint, depending on if the value is null + $eqOrNull = fn (string $table, ?string $value): string => ($value === null) + ? $query->expr()->isNull($table) + : $query->expr()->eq($table, $query->createNamedParameter($value)); + + $query->selectColumns('recipient_class_id', 'recipient_value', 'recipient_instance', 'initiator_user_id', 'initiator_instance') + ->selectAlias($query->func()->count('*'), 'count') + ->from('sharing_share_recipients', 'r') + ->innerJoin('r', 'sharing_share', 's', $query->expr()->eq('r.share_id', 's.id')) + ->where( + $query->expr()->orX( + $query->expr()->andX( + $query->expr()->eq('s.owner_user_id', $query->createNamedParameter($user->userId)), + $eqOrNull('s.owner_instance', $user->instance), + ), + $query->expr()->andX( + $query->expr()->eq('r.initiator_user_id', $query->createNamedParameter($user->userId)), + $eqOrNull('r.initiator_instance', $user->instance), + ), + ) + ) + ->groupBy('r.recipient_class_id', 'r.recipient_value', 'r.recipient_instance', 'r.initiator_user_id', 'r.initiator_instance') + ->orderBy('count', \SortDirection::Descending) + // sort by recipient to get a stable output, and allow "after" to be deterministic + ->addOrderBy( + 'recipient_instance', \SortDirection::Ascending + ) + ->addOrderBy( + 'recipient_value', \SortDirection::Ascending + ) + ->addOrderBy('recipient_class_id', \SortDirection::Ascending); + + if ($filterRecipientTypeClasses !== null) { + $filterRecipientTypeClassIds = array_map($this->classMapper->getClassId(...), $filterRecipientTypeClasses); + $query = $query->andWhere( + $query->expr()->in('recipient_class_id', $query->createNamedParameter($filterRecipientTypeClassIds, IQueryBuilder::PARAM_INT_ARRAY)) + ); + } + + if ($notInShare !== null) { + $fullRecipientId = $query->func()->concat( + 'recipient_class_id', + $query->func()->coalesce('recipient_instance', $query->expr()->literal('-')), + 'recipient_value' + ); + + $subQuery = $this->connection->getTypedQueryBuilder(); + $subQuery->selectAlias($fullRecipientId, 'recipient') + ->from('sharing_share_recipients') + ->where($query->expr()->eq('share_id', $query->createNamedParameter($notInShare))); + + $query = $query->andWhere( + $query->expr()->notIn( + $fullRecipientId, + $query->createFunction('(' . $subQuery->getSQL() . ')') + ) + ); + } + + $query->setMaxResults($count); + + if ($offset) { + $query = $query->setFirstResult($offset); + } + + $rows = $query->executeQuery()->fetchAll(); + + return array_map(function (array $row): \NCU\Sharing\Recipient\ShareRecipient { + /** @var array{recipient_class_id: int|non-empty-string, recipient_value: non-empty-string, recipient_instance: ?non-empty-string, initiator_user_id: non-empty-string, initiator_instance: ?non-empty-string} $row */ + $class = $this->classMapper->getClassName((int)$row['recipient_class_id']); + if (!isset($this->registry->getRecipientTypes()[$class])) { + throw new RuntimeException('The recipient type is not registered: ' . $class); + } + + /** @var class-string $class */ + return new ShareRecipient( + $class, + $row['recipient_value'], + $row['recipient_instance'], + null, + new ShareUser( + $row['initiator_user_id'], + $row['initiator_instance'], + ) + ); + }, $rows); + } + private static function parseTimestamp(string $timestampMs): \DateTimeImmutable { if (method_exists(\DateTimeImmutable::class, 'createFromTimestamp')) { // with php 8.3 the method doesn't exist and psalm doesn't know the return type diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php index add248bcb934e..028d67dd271d3 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -769,6 +769,17 @@ public function getShares( return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit); } + #[\Override] + public function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array { + return $this->backend->getRecipientsForUser($user, $filterRecipientTypeClasses, $notInShare, $count, $offset); + } + #[\Override] public function handle(Event $event): void { if ($event instanceof SharesDefaultSetEvent) { diff --git a/lib/unstable/Sharing/ISharingBackend.php b/lib/unstable/Sharing/ISharingBackend.php index 1d1f820886969..f00f0addb571b 100644 --- a/lib/unstable/Sharing/ISharingBackend.php +++ b/lib/unstable/Sharing/ISharingBackend.php @@ -14,6 +14,7 @@ use NCU\Sharing\Permission\ISharePermissionPreset; use NCU\Sharing\Permission\SharePermission; use NCU\Sharing\Property\ShareProperty; +use NCU\Sharing\Recipient\IShareRecipientType; use NCU\Sharing\Recipient\ShareRecipient; use NCU\Sharing\Source\IShareSourceType; use NCU\Sharing\Source\ShareSource; @@ -212,4 +213,23 @@ public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): voi * @experimental 35.0.0 */ public function ensureDefaults(array $shares): array; + + /** + * Get a list of recipients a user has shared with, ordered by share count + * + * "shared with" includes both shares owned by the user, and reshares initiated by the user + * + * @param ?list> $filterRecipientTypeClasses + * @param null|non-empty-string $notInShare + * @param non-negative-int $count + * @return list + * @experimental 35.0.0 + */ + public function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array; } diff --git a/lib/unstable/Sharing/ISharingManager.php b/lib/unstable/Sharing/ISharingManager.php index 044cf78f0202b..3eb218ddc0532 100644 --- a/lib/unstable/Sharing/ISharingManager.php +++ b/lib/unstable/Sharing/ISharingManager.php @@ -225,4 +225,23 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share; * @experimental 35.0.0 */ public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?ShareState $filterState, ?ShareUserStatus $filterUserStatus, ?string $lastShareID, ?int $limit): array; + + /** + * Get a list of recipients a user has shared with, ordered by share count + * + * "shared with" includes both shares owned by the user, and reshares initiated by the user + * + * @param ?list> $filterRecipientTypeClasses + * @param null|non-empty-string $notInShare + * @param non-negative-int $count + * @return list + * @experimental 35.0.0 + */ + public function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array; } diff --git a/openapi.json b/openapi.json index 1f862fa904b68..bc10002fd291b 100644 --- a/openapi.json +++ b/openapi.json @@ -38025,7 +38025,7 @@ { "name": "query", "in": "query", - "description": "The query to search for", + "description": "The query to search for, if the query is empty, recommended recipients will be returned", "required": true, "schema": { "type": "string" diff --git a/tests/lib/Sharing/AbstractSharingManagerTests.php b/tests/lib/Sharing/AbstractSharingManagerTests.php index 83c0d654b7011..591c213316673 100644 --- a/tests/lib/Sharing/AbstractSharingManagerTests.php +++ b/tests/lib/Sharing/AbstractSharingManagerTests.php @@ -17,6 +17,7 @@ use NCU\Sharing\Share; use NCU\Sharing\ShareAccessContext; use NCU\Sharing\ShareState; +use NCU\Sharing\ShareUser; use NCU\Sharing\ShareUserStatus; use NCU\Sharing\Source\ShareSource; use OC\Core\Sharing\Permission\ReshareSharePermissionType; @@ -42,6 +43,9 @@ * @psalm-suppress PossiblyUndefinedArrayOffset */ abstract class AbstractSharingManagerTests extends TestCase { + /** + * @return SharingRecipient[] + */ abstract protected function searchRecipients( ShareAccessContext $accessContext, ?array $filterRecipientTypeClasses, string $query, int $limit, int $offset, ?Share $forShare = null, ): array; @@ -99,7 +103,9 @@ abstract protected function updateSharePermission(ShareAccessContext $accessCont /** * @return SharingShare */ - abstract protected function updateShareRecipientPermission(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient, SharePermission $permission): array; + abstract protected function updateShareRecipientPermission( + ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient, SharePermission $permission, + ): array; /** * @return SharingShare @@ -126,6 +132,17 @@ abstract protected function getShares( ?int $limit, ): array; + /** + * @return SharingRecipient[] + */ + abstract protected function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array; + protected IDBConnection $dbConnection; protected ISharingManager $manager; @@ -1905,7 +1922,9 @@ public function testUpdateShareRecipientPermission(): void { $share1 = $this->manager->addShareRecipient($accessContext1, $share1, $recipient2); $this->dbConnection->commit(); - $formatted = $this->updateShareRecipientPermission($accessContext1, $share1, $recipient2, new SharePermission(ReshareSharePermissionType::class, false)); + $formatted = $this->updateShareRecipientPermission( + $accessContext1, $share1, $recipient2, new SharePermission(ReshareSharePermissionType::class, false) + ); $this->assertEquals([ [ 'class' => TestShareRecipientType1::class, @@ -4768,4 +4787,107 @@ public function testEditWithDirectAccess(): void { ] ], $formatted['properties']); } + + /** + * @return array{ + * shares: array{recipients: ShareRecipient[], owner: string}[], + * user: ShareUser, + * filterRecipientTypeClasses?: ?array, + * notInShare?: ?string, + * count?: int, + * offset?: int, + * expected?: string[] + * }[] + */ + public static function getRecipientsForUsersData(): array { + $initiator = new ShareUser('user1', null); + $initiator2 = new ShareUser('user2', null); + $recipient1 = new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null, null, $initiator); + $recipient2 = new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null, null, $initiator); + $owner = new ShareUser('owner', null); + + return [ + // querying for the owner, with no further filters + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'expected' => ['recipient1', 'recipient2']], + // querying for the owner, limit + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'count' => 1, 'expected' => ['recipient1']], + // querying for the owner, offset + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'offset' => 1, 'expected' => ['recipient2']], + // querying for the owner, offset past end + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'offset' => 2, 'expected' => []], + // querying for the initiator, with no further filters + ['shares' => [ + ['recipients' => [$recipient1], 'owner' => 'owner'] + ], 'user' => $initiator, 'expected' => ['recipient1']], + // querying for the wrong initiator + ['shares' => [ + ['recipients' => [$recipient1], 'owner' => 'owner'] + ], 'user' => $initiator2, 'expected' => []], + // querying for the owner, filter type + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'filterRecipientTypeClasses' => [TestShareRecipientType2::class], 'expected' => ['recipient2']], + // querying for the owner, multiple shares + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'], + ['recipients' => [$recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'expected' => ['recipient2', 'recipient1']], + // querying for the owner, multiple shares, not in share + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'], + 'excluded' => ['recipients' => [$recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'notInShare' => 'excluded', 'expected' => ['recipient1']], + // querying for the owner, multiple shares, offset + ['shares' => [ + ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'], + ['recipients' => [$recipient2], 'owner' => 'owner'] + ], 'user' => $owner, 'offset' => 1, 'expected' => ['recipient1']], + ]; + } + + /** + * @param array{recipients: ShareRecipient[], owner: string}[] $shares + * @param string[] $expected , + */ + #[DataProvider('getRecipientsForUsersData')] + public function testGetRecipientsForUsers( + array $shares, + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + array $expected = [], + ): void { + /** @var string[] $shareIds */ + $shareIds = []; + $this->dbConnection->beginTransaction(); + foreach ($shares as $key => $shareData) { + $owner = $this->createMock(IUser::class); + $owner->method('getUID')->willReturn($shareData['owner']); + $ownerAccessContext = new ShareAccessContext($owner); + + $share = $this->manager->createShare($ownerAccessContext); + foreach ($shareData['recipients'] as $recipient) { + $share = $this->manager->addShareRecipient($ownerAccessContext, $share, $recipient); + } + + $shareIds[$key] = $share->id; + } + + $this->dbConnection->commit(); + + $notInShareId = ($notInShare !== null) ? $shareIds[$notInShare] : null; + $recipients = $this->getRecipientsForUser($user, $filterRecipientTypeClasses, $notInShareId, $count, $offset); + $recipientValues = array_map(fn (array $recipient): string => $recipient['value'], $recipients); + $this->assertEquals($expected, $recipientValues); + } } diff --git a/tests/lib/Sharing/SharingManagerTest.php b/tests/lib/Sharing/SharingManagerTest.php index b25a4f7610909..9fda86cb53e89 100644 --- a/tests/lib/Sharing/SharingManagerTest.php +++ b/tests/lib/Sharing/SharingManagerTest.php @@ -16,6 +16,7 @@ use NCU\Sharing\Share; use NCU\Sharing\ShareAccessContext; use NCU\Sharing\ShareState; +use NCU\Sharing\ShareUser; use NCU\Sharing\ShareUserStatus; use NCU\Sharing\Source\ShareSource; use OC\Sharing\SharingManager; @@ -303,4 +304,17 @@ protected function getShares(ShareAccessContext $accessContext, ?string $filterS throw $exception; } } + + #[\Override] + protected function getRecipientsForUser( + ShareUser $user, + ?array $filterRecipientTypeClasses = null, + ?string $notInShare = null, + int $count = 5, + int $offset = 0, + ): array { + /** @psalm-suppress ArgumentTypeCoercion */ + $recipients = $this->manager->getRecipientsForUser($user, $filterRecipientTypeClasses, $notInShare, $count, $offset); + return ShareRecipient::formatMultiple($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $recipients); + } }