diff --git a/apps/dashboard/lib/Controller/DashboardApiController.php b/apps/dashboard/lib/Controller/DashboardApiController.php index 8a4599a7ec0fa..ae4b517d657be 100644 --- a/apps/dashboard/lib/Controller/DashboardApiController.php +++ b/apps/dashboard/lib/Controller/DashboardApiController.php @@ -16,6 +16,8 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\AppFramework\Services\IAppConfig; use OCP\Config\IUserConfig; @@ -26,11 +28,14 @@ use OCP\Dashboard\IManager; use OCP\Dashboard\IOptionWidget; use OCP\Dashboard\IReloadableWidget; +use OCP\Dashboard\ITeamWidget; use OCP\Dashboard\IWidget; use OCP\Dashboard\Model\WidgetButton; use OCP\Dashboard\Model\WidgetItem; use OCP\Dashboard\Model\WidgetOptions; use OCP\IRequest; +use OCP\Teams\ITeamManager; +use Psr\Log\LoggerInterface; /** * @psalm-import-type DashboardWidget from ResponseDefinitions @@ -47,6 +52,8 @@ public function __construct( private IUserConfig $userConfig, private ?string $userId, private DashboardService $service, + private ITeamManager $teamManager, + private LoggerInterface $logger, ) { parent::__construct($appName, $request); } @@ -69,6 +76,44 @@ static function (IWidget $widget) use ($widgetIds) { ); } + private function resolveTeam(?string $teamId): ?string { + if ($teamId === null) { + return null; + } + if ($teamId === '') { + throw new OCSBadRequestException('Empty team ID'); + } + if ($this->userId === null || !$this->teamManager->isMemberOfTeam($teamId, $this->userId)) { + throw new OCSNotFoundException('Team not found'); + } + return $teamId; + } + + /** + * @param list $widgetIds + * @return array + */ + private function getTeamWidgets(string $teamId, array $widgetIds): array { + $widgets = []; + foreach ($this->dashboardManager->getWidgets() as $id => $widget) { + if (!$widget instanceof ITeamWidget || ($widgetIds !== [] && !in_array($id, $widgetIds, true))) { + continue; + } + try { + if ($widget->isEnabledForTeam($teamId, $this->userId)) { + $widgets[$id] = $widget; + } + } catch (\Throwable $e) { + $this->logger->warning('Failed to check dashboard widget applicability for team', [ + 'widget' => $id, + 'team' => $teamId, + 'exception' => $e, + ]); + } + } + return $widgets; + } + /** * Get the items for the widgets * @@ -104,15 +149,34 @@ public function getWidgetItems(array $sinceIds = [], int $limit = 7, array $widg * @param int $limit Limit number of result items per widget, not more than 30 are allowed * @psalm-param int<1, 30> $limit * @param list $widgets Limit results to specific widgets + * @param ?string $teamId Team to show widgets for; omit for the personal dashboard * @return DataResponse, array{}> + * @throws OCSNotFoundException Team not found + * @throws OCSBadRequestException Empty team ID * * 200: Widget items returned */ #[NoAdminRequired] #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/api/v2/widget-items')] - public function getWidgetItemsV2(array $sinceIds = [], int $limit = 7, array $widgets = []): DataResponse { + public function getWidgetItemsV2(array $sinceIds = [], int $limit = 7, array $widgets = [], ?string $teamId = null): DataResponse { + $teamId = $this->resolveTeam($teamId); $items = []; + if ($teamId !== null) { + foreach ($this->getTeamWidgets($teamId, $widgets) as $id => $widget) { + try { + $items[$id] = $widget->getTeamItems($teamId, $this->userId, $sinceIds[$id] ?? null, $limit)->jsonSerialize(); + } catch (\Throwable $e) { + $this->logger->warning('Failed to get dashboard widget items for team', [ + 'widget' => $id, + 'team' => $teamId, + 'exception' => $e, + ]); + } + } + return new DataResponse($items); + } + $widgets = $this->getShownWidgets($widgets); foreach ($widgets as $widget) { if ($widget instanceof IAPIWidgetV2) { @@ -128,55 +192,80 @@ public function getWidgetItemsV2(array $sinceIds = [], int $limit = 7, array $wi /** * Get the widgets * + * @param ?string $teamId Team to show widgets for; omit for the personal dashboard * @return DataResponse, array{}> + * @throws OCSNotFoundException Team not found + * @throws OCSBadRequestException Empty team ID * * 200: Widgets returned */ #[NoAdminRequired] #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/api/v1/widgets')] - public function getWidgets(): DataResponse { - $widgets = $this->dashboardManager->getWidgets(); - - $items = array_map(function (IWidget $widget) { - $options = ($widget instanceof IOptionWidget) ? $widget->getWidgetOptions() : WidgetOptions::getDefault(); - $data = [ - 'id' => $widget->getId(), - 'title' => $widget->getTitle(), - 'order' => $widget->getOrder(), - 'icon_class' => $widget->getIconClass(), - 'icon_url' => ($widget instanceof IIconWidget) ? $widget->getIconUrl() : '', - 'widget_url' => $widget->getUrl(), - 'item_icons_round' => $options->withRoundItemIcons(), - 'item_api_versions' => [], - 'reload_interval' => 0, - ]; - if ($widget instanceof IButtonWidget) { - $data += [ - 'buttons' => array_map(function (WidgetButton $button) { - return [ - 'type' => $button->getType(), - 'text' => $button->getText(), - 'link' => $button->getLink(), - ]; - }, $widget->getWidgetButtons($this->userId)), - ]; + public function getWidgets(?string $teamId = null): DataResponse { + $teamId = $this->resolveTeam($teamId); + if ($teamId !== null) { + $items = []; + foreach ($this->getTeamWidgets($teamId, []) as $id => $widget) { + try { + $items[$id] = $this->serializeWidget($widget, $widget->getTeamUrl($teamId, $this->userId) ?? $widget->getUrl()); + } catch (\Throwable $e) { + $this->logger->warning('Failed to get dashboard widget metadata for team', [ + 'widget' => $id, + 'team' => $teamId, + 'exception' => $e, + ]); + } } - if ($widget instanceof IReloadableWidget) { - $data['reload_interval'] = $widget->getReloadInterval(); - } - if ($widget instanceof IAPIWidget) { - $data['item_api_versions'][] = 1; - } - if ($widget instanceof IAPIWidgetV2) { - $data['item_api_versions'][] = 2; - } - return $data; - }, $widgets); + } else { + $items = array_map( + fn (IWidget $widget) => $this->serializeWidget($widget, $widget->getUrl()), + $this->dashboardManager->getWidgets(), + ); + } return new DataResponse($items); } + /** + * @return DashboardWidget + */ + private function serializeWidget(IWidget $widget, ?string $url): array { + $options = ($widget instanceof IOptionWidget) ? $widget->getWidgetOptions() : WidgetOptions::getDefault(); + $data = [ + 'id' => $widget->getId(), + 'title' => $widget->getTitle(), + 'order' => $widget->getOrder(), + 'icon_class' => $widget->getIconClass(), + 'icon_url' => ($widget instanceof IIconWidget) ? $widget->getIconUrl() : '', + 'widget_url' => $url, + 'item_icons_round' => $options->withRoundItemIcons(), + 'item_api_versions' => [], + 'reload_interval' => 0, + ]; + if ($widget instanceof IButtonWidget) { + $data += [ + 'buttons' => array_map(function (WidgetButton $button) { + return [ + 'type' => $button->getType(), + 'text' => $button->getText(), + 'link' => $button->getLink(), + ]; + }, $widget->getWidgetButtons($this->userId)), + ]; + } + if ($widget instanceof IReloadableWidget) { + $data['reload_interval'] = $widget->getReloadInterval(); + } + if ($widget instanceof IAPIWidget) { + $data['item_api_versions'][] = 1; + } + if ($widget instanceof IAPIWidgetV2) { + $data['item_api_versions'][] = 2; + } + return $data; + } + /** * Get the layout * diff --git a/apps/dashboard/openapi.json b/apps/dashboard/openapi.json index 1960328b6c2ae..410b2276ba469 100644 --- a/apps/dashboard/openapi.json +++ b/apps/dashboard/openapi.json @@ -347,6 +347,16 @@ } } }, + { + "name": "teamId", + "in": "query", + "description": "Team to show widgets for; omit for the personal dashboard", + "schema": { + "type": "string", + "nullable": true, + "default": null + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -392,6 +402,62 @@ } } }, + "404": { + "description": "Team not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Empty team ID", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -439,6 +505,16 @@ } ], "parameters": [ + { + "name": "teamId", + "in": "query", + "description": "Team to show widgets for; omit for the personal dashboard", + "schema": { + "type": "string", + "nullable": true, + "default": null + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -484,6 +560,62 @@ } } }, + "404": { + "description": "Team not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Empty team ID", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { diff --git a/apps/dashboard/tests/DashboardApiControllerTest.php b/apps/dashboard/tests/DashboardApiControllerTest.php new file mode 100644 index 0000000000000..3165aeeb30e4e --- /dev/null +++ b/apps/dashboard/tests/DashboardApiControllerTest.php @@ -0,0 +1,282 @@ +dashboardManager = $this->createMock(IManager::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->userConfig = $this->createMock(IUserConfig::class); + $this->teamManager = $this->createMock(ITeamManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->controller = $this->createController('alice'); + } + + private function createController(?string $userId): DashboardApiController { + return new DashboardApiController( + 'dashboard', + $this->createMock(IRequest::class), + $this->dashboardManager, + $this->appConfig, + $this->userConfig, + $userId, + $this->createMock(DashboardService::class), + $this->teamManager, + $this->logger, + ); + } + + public function testPersonalWidgetMetadata(): void { + $this->teamManager->expects($this->never())->method('isMemberOfTeam'); + $widget = $this->createMock(TestTeamWidget::class); + $widget->method('getId')->willReturn('widget'); + $widget->method('getTitle')->willReturn('Widget'); + $widget->method('getOrder')->willReturn(10); + $widget->method('getIconClass')->willReturn('icon-widget'); + $widget->method('getIconUrl')->willReturn('/icon.svg'); + $widget->method('getUrl')->willReturn('/personal'); + $widget->method('getWidgetOptions')->willReturn(new WidgetOptions(true)); + $widget->method('getReloadInterval')->willReturn(60); + $widget->expects($this->once())->method('getWidgetButtons')->with('alice') + ->willReturn([new WidgetButton(WidgetButton::TYPE_MORE, '/more', 'More')]); + $widget->expects($this->never())->method('isEnabledForTeam'); + $widget->expects($this->never())->method('getTeamUrl'); + $plain = $this->createMock(IWidget::class); + $this->dashboardManager->method('getWidgets')->willReturn(['widget' => $widget, 'plain' => $plain]); + + $data = $this->controller->getWidgets()->getData(); + + $this->assertSame([ + 'id' => 'widget', + 'title' => 'Widget', + 'order' => 10, + 'icon_class' => 'icon-widget', + 'icon_url' => '/icon.svg', + 'widget_url' => '/personal', + 'item_icons_round' => true, + 'item_api_versions' => [1, 2], + 'reload_interval' => 60, + 'buttons' => [['type' => WidgetButton::TYPE_MORE, 'text' => 'More', 'link' => '/more']], + ], $data['widget']); + $this->assertSame(['widget', 'plain'], array_keys($data)); + $this->assertSame('', $data['plain']['icon_url']); + $this->assertSame(0, $data['plain']['reload_interval']); + $this->assertSame([], $data['plain']['item_api_versions']); + $this->assertSame(WidgetOptions::getDefault()->withRoundItemIcons(), $data['plain']['item_icons_round']); + $this->assertArrayNotHasKey('buttons', $data['plain']); + } + + public static function personalItemsProvider(): array { + return [ + 'saved layout' => [[]], + 'explicit widgets' => [['widget']], + ]; + } + + #[DataProvider('personalItemsProvider')] + public function testPersonalWidgetItemsV2(array $widgets): void { + $this->teamManager->expects($this->never())->method('isMemberOfTeam'); + if ($widgets === []) { + $this->appConfig->expects($this->once())->method('getAppValueString') + ->with('layout', 'recommendations,spreed,mail,calendar')->willReturn('default'); + $this->userConfig->expects($this->once())->method('getValueString') + ->with('alice', 'dashboard', 'layout', 'default')->willReturn('widget'); + } else { + $this->userConfig->expects($this->never())->method('getValueString'); + } + $items = new WidgetItems([new WidgetItem('Title', 'Subtitle', '/link', '/icon.svg', 'cursor')], 'Empty', 'Half empty'); + $widget = $this->createMock(ITeamWidget::class); + $widget->method('getId')->willReturn('widget'); + $widget->expects($this->once())->method('getItemsV2')->with('alice', 'cursor', 5)->willReturn($items); + $widget->expects($this->never())->method('getTeamItems'); + $widget->expects($this->never())->method('isEnabledForTeam'); + $unselected = $this->createMock(IAPIWidgetV2::class); + $unselected->method('getId')->willReturn('unselected'); + $unselected->expects($this->never())->method('getItemsV2'); + $this->dashboardManager->method('getWidgets')->willReturn(['widget' => $widget, 'unselected' => $unselected]); + + $this->assertSame(['widget' => $items->jsonSerialize()], $this->controller->getWidgetItemsV2(['widget' => 'cursor'], 5, $widgets)->getData()); + } + + public static function rejectedTeamProvider(): array { + $cases = []; + foreach (['getWidgets', 'getWidgetItemsV2'] as $method) { + $cases[$method . ' empty team'] = [$method, '', 'alice', false, OCSBadRequestException::class]; + $cases[$method . ' non-member'] = [$method, 'team', 'alice', false, OCSNotFoundException::class]; + $cases[$method . ' no session user'] = [$method, 'team', null, false, OCSNotFoundException::class]; + } + return $cases; + } + + #[DataProvider('rejectedTeamProvider')] + public function testRejectsTeamBeforeLoadingWidgets(string $method, string $teamId, ?string $userId, bool $isMember, string $exceptionClass): void { + $this->dashboardManager->expects($this->never())->method('getWidgets'); + $this->userConfig->expects($this->never())->method('getValueString'); + if ($teamId !== '' && $userId !== null) { + $this->teamManager->expects($this->once())->method('isMemberOfTeam')->with($teamId, $userId)->willReturn($isMember); + } else { + $this->teamManager->expects($this->never())->method('isMemberOfTeam'); + } + $this->expectException($exceptionClass); + $this->expectExceptionMessage($teamId === '' ? 'Empty team ID' : 'Team not found'); + $this->createController($userId)->$method(teamId: $teamId); + } + + public function testTeamWidgetMetadataFiltersAndUsesTeamUrls(): void { + $this->teamManager->expects($this->once())->method('isMemberOfTeam')->with('team', 'alice')->willReturn(true); + $this->userConfig->expects($this->never())->method('getValueString'); + $team = $this->createMock(ITeamWidget::class); + $team->method('getId')->willReturn('team-widget'); + $team->expects($this->once())->method('isEnabledForTeam')->with('team', 'alice')->willReturn(true); + $team->expects($this->once())->method('getTeamUrl')->with('team', 'alice')->willReturn('/team'); + $team->expects($this->never())->method('getUrl'); + $fallback = $this->createMock(ITeamWidget::class); + $fallback->method('getId')->willReturn('fallback'); + $fallback->method('isEnabledForTeam')->willReturn(true); + $fallback->method('getTeamUrl')->willReturn(null); + $fallback->method('getUrl')->willReturn('/personal'); + $disabled = $this->createMock(ITeamWidget::class); + $disabled->method('isEnabledForTeam')->willReturn(false); + $disabled->expects($this->never())->method('getTeamUrl'); + $this->dashboardManager->method('getWidgets')->willReturn([ + 'personal' => $this->createMock(IAPIWidgetV2::class), + 'team-widget' => $team, + 'fallback' => $fallback, + 'disabled' => $disabled, + ]); + + $data = $this->controller->getWidgets('team')->getData(); + + $this->assertSame(['team-widget', 'fallback'], array_keys($data)); + $this->assertSame('/team', $data['team-widget']['widget_url']); + $this->assertSame('/personal', $data['fallback']['widget_url']); + $this->assertSame([2], $data['team-widget']['item_api_versions']); + } + + public static function teamItemsProvider(): array { + return [ + 'all applicable with default cursor' => [[], [], 7, null], + 'filter before applicability' => [['widget', 'unknown', 'personal', 'disabled'], ['widget' => 'cursor'], 3, 'cursor'], + ]; + } + + #[DataProvider('teamItemsProvider')] + public function testTeamWidgetItems(array $widgets, array $sinceIds, int $limit, ?string $cursor): void { + $this->teamManager->expects($this->once())->method('isMemberOfTeam')->with('team', 'alice')->willReturn(true); + $this->userConfig->expects($this->never())->method('getValueString'); + $this->appConfig->expects($this->never())->method('getAppValueString'); + $items = new WidgetItems([new WidgetItem('Team page', '', '/page', '/icon.svg')], 'Empty team'); + $widget = $this->createMock(ITeamWidget::class); + $widget->method('getId')->willReturn('widget'); + $widget->expects($this->once())->method('isEnabledForTeam')->with('team', 'alice')->willReturn(true); + $widget->expects($this->once())->method('getTeamItems')->with('team', 'alice', $cursor, $limit)->willReturn($items); + $widget->expects($this->never())->method('getItemsV2'); + $other = $this->createMock(ITeamWidget::class); + $other->method('getId')->willReturn('other'); + $other->expects($widgets === [] ? $this->once() : $this->never())->method('isEnabledForTeam')->willReturn(true); + $other->expects($widgets === [] ? $this->once() : $this->never())->method('getTeamItems') + ->with('team', 'alice', null, $limit)->willReturn(new WidgetItems()); + $disabled = $this->createMock(ITeamWidget::class); + $disabled->expects($this->once())->method('isEnabledForTeam')->willReturn(false); + $disabled->expects($this->never())->method('getTeamItems'); + $this->dashboardManager->method('getWidgets')->willReturn([ + 'widget' => $widget, + 'other' => $other, + 'personal' => $this->createMock(IAPIWidgetV2::class), + 'disabled' => $disabled, + ]); + + $expected = ['widget' => $items->jsonSerialize()]; + if ($widgets === []) { + $expected['other'] = (new WidgetItems())->jsonSerialize(); + } + $this->assertSame($expected, $this->controller->getWidgetItemsV2($sinceIds, $limit, $widgets, 'team')->getData()); + } + + public static function teamWidgetFailureProvider(): array { + $cases = []; + foreach ([\RuntimeException::class, \Error::class] as $exceptionClass) { + foreach (['getWidgets', 'getWidgetItemsV2'] as $endpoint) { + $cases[$exceptionClass . ' applicability ' . $endpoint] = [ + $endpoint, 'isEnabledForTeam', 'Failed to check dashboard widget applicability for team', $exceptionClass, + ]; + } + $cases[$exceptionClass . ' URL'] = [ + 'getWidgets', 'getTeamUrl', 'Failed to get dashboard widget metadata for team', $exceptionClass, + ]; + $cases[$exceptionClass . ' items'] = [ + 'getWidgetItemsV2', 'getTeamItems', 'Failed to get dashboard widget items for team', $exceptionClass, + ]; + } + return $cases; + } + + #[DataProvider('teamWidgetFailureProvider')] + public function testBrokenProviderDoesNotHideOtherWidgets(string $endpoint, string $providerMethod, string $message, string $exceptionClass): void { + $this->teamManager->method('isMemberOfTeam')->willReturn(true); + $exception = new $exceptionClass('Broken provider'); + $broken = $this->createMock(ITeamWidget::class); + $broken->expects($this->once())->method($providerMethod)->willThrowException($exception); + if ($providerMethod === 'isEnabledForTeam') { + $broken->expects($this->never())->method('getTeamItems'); + $broken->expects($this->never())->method('getTeamUrl'); + } else { + $broken->method('isEnabledForTeam')->willReturn(true); + } + $working = $this->createMock(ITeamWidget::class); + $working->method('getId')->willReturn('working'); + $working->method('isEnabledForTeam')->willReturn(true); + $working->method('getTeamItems')->willReturn(new WidgetItems()); + $this->dashboardManager->method('getWidgets')->willReturn(['before' => $working, 'broken' => $broken, 'after' => $working]); + $this->logger->expects($this->once())->method('warning')->with( + $message, + ['widget' => 'broken', 'team' => 'team', 'exception' => $exception], + ); + + $this->assertSame(['before', 'after'], array_keys($this->controller->$endpoint(teamId: 'team')->getData())); + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 09f53637de36f..9ed23766270c0 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -419,6 +419,7 @@ 'OCP\\Dashboard\\IManager' => $baseDir . '/lib/public/Dashboard/IManager.php', 'OCP\\Dashboard\\IOptionWidget' => $baseDir . '/lib/public/Dashboard/IOptionWidget.php', 'OCP\\Dashboard\\IReloadableWidget' => $baseDir . '/lib/public/Dashboard/IReloadableWidget.php', + 'OCP\\Dashboard\\ITeamWidget' => $baseDir . '/lib/public/Dashboard/ITeamWidget.php', 'OCP\\Dashboard\\IWidget' => $baseDir . '/lib/public/Dashboard/IWidget.php', 'OCP\\Dashboard\\Model\\WidgetButton' => $baseDir . '/lib/public/Dashboard/Model/WidgetButton.php', 'OCP\\Dashboard\\Model\\WidgetItem' => $baseDir . '/lib/public/Dashboard/Model/WidgetItem.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d3be8de3e78d0..79b0578517ebd 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -460,6 +460,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Dashboard\\IManager' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IManager.php', 'OCP\\Dashboard\\IOptionWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IOptionWidget.php', 'OCP\\Dashboard\\IReloadableWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IReloadableWidget.php', + 'OCP\\Dashboard\\ITeamWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/ITeamWidget.php', 'OCP\\Dashboard\\IWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IWidget.php', 'OCP\\Dashboard\\Model\\WidgetButton' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetButton.php', 'OCP\\Dashboard\\Model\\WidgetItem' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetItem.php', diff --git a/lib/private/Teams/TeamManager.php b/lib/private/Teams/TeamManager.php index d17aefe36223b..6117b63952c15 100644 --- a/lib/private/Teams/TeamManager.php +++ b/lib/private/Teams/TeamManager.php @@ -83,15 +83,20 @@ public function getTeamFolderProvider(): ?ITeamFolderProvider { } #[\Override] - public function getSharedWith(string $teamId, string $userId): array { + public function isMemberOfTeam(string $teamId, string $userId): bool { if (!$this->hasTeamSupport()) { - return []; + return false; } $probe = new CircleProbe(); $probe->mustBeMember(); - if ($this->getTeamInternal($teamId, $userId, $probe) === null) { + return $this->getTeamInternal($teamId, $userId, $probe) !== null; + } + + #[\Override] + public function getSharedWith(string $teamId, string $userId): array { + if (!$this->isMemberOfTeam($teamId, $userId)) { return []; } diff --git a/lib/public/Dashboard/ITeamWidget.php b/lib/public/Dashboard/ITeamWidget.php new file mode 100644 index 0000000000000..663408a97d11f --- /dev/null +++ b/lib/public/Dashboard/ITeamWidget.php @@ -0,0 +1,42 @@ +assertFalse($this->createTeamManager()->isMemberOfTeam('team', 'user')); + } + + public function testIsMemberOfTeamUsesMemberOnlyProbe(): void { + $circlesManager = $this->createCirclesManager(); + $federatedUser = $this->createMock(FederatedUser::class); + $probe = new CircleProbe(); + $probe->mustBeMember(); + $circlesManager->expects($this->once())->method('getFederatedUser') + ->with('user', Member::TYPE_USER)->willReturn($federatedUser); + $circlesManager->expects($this->once())->method('startSession')->with($federatedUser); + $circlesManager->expects($this->once())->method('getCircle') + ->with('team', $this->equalTo($probe))->willReturn($this->createMock(Circle::class)); + + $this->assertTrue($this->createTeamManagerWithCircles($circlesManager)->isMemberOfTeam('team', 'user')); + } + + public function testIsMemberOfTeamReturnsFalseForMissingTeamOrNonMember(): void { + $circlesManager = $this->createCirclesManager(); + $circlesManager->method('getFederatedUser')->willReturn($this->createMock(FederatedUser::class)); + $circlesManager->expects($this->once())->method('getCircle')->willThrowException(new CircleNotFoundException()); + + $this->assertFalse($this->createTeamManagerWithCircles($circlesManager)->isMemberOfTeam('team', 'user')); + } + + public function testGetSharedWithWithoutTeamSupport(): void { + $this->assertSame([], $this->createTeamManager()->getSharedWith('team', 'user')); + } + + public function testGetSharedWithRejectsNonMembers(): void { + $circlesManager = $this->createCirclesManager(); + $circlesManager->method('getFederatedUser')->willReturn($this->createMock(FederatedUser::class)); + $circlesManager->method('getCircle')->willThrowException(new CircleNotFoundException()); + $provider = $this->createMock(ITeamResourceProvider::class); + $provider->expects($this->never())->method('getSharedWith'); + $teamManager = $this->createTeamManagerWithCircles($circlesManager); + $this->setProviders($teamManager, ['provider' => $provider]); + + $this->assertSame([], $teamManager->getSharedWith('team', 'user')); + } + + public function testGetSharedWithReturnsResourcesForMembers(): void { + $circlesManager = $this->createCirclesManager(); + $circlesManager->method('getFederatedUser')->willReturn($this->createMock(FederatedUser::class)); + $circlesManager->method('getCircle')->willReturn($this->createMock(Circle::class)); + $resource = $this->createMock(TeamResource::class); + $provider = $this->createMock(ITeamResourceProvider::class); + $provider->expects($this->once())->method('getSharedWith')->with('team')->willReturn([$resource]); + $teamManager = $this->createTeamManagerWithCircles($circlesManager); + $this->setProviders($teamManager, ['provider' => $provider]); + + $this->assertSame([$resource], $teamManager->getSharedWith('team', 'user')); + } + + private function createCirclesManager(): CirclesManager&MockObject { + if (!class_exists(CirclesManager::class)) { + $this->markTestSkipped('Circles is not installed'); + } + + return $this->createMock(CirclesManager::class); + } + + private function createTeamManagerWithCircles(CirclesManager $circlesManager): TeamManager { + return new TeamManager($this->createMock(Coordinator::class), $this->createMock(IURLGenerator::class), $circlesManager); + } + public function testGetTeamFolderProviderReturnsNullWithoutTeamSupport(): void { $teamManager = $this->createTeamManager();