diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 77fdebecc..95c99b09b 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -32,6 +32,7 @@ use OCA\GroupFolders\Listeners\NodeRenamedListener; use OCA\GroupFolders\Mount\FolderStorageManager; use OCA\GroupFolders\Mount\MountProvider; +use OCA\GroupFolders\Team\GroupFoldersTeamResourceProvider; use OCA\GroupFolders\Trash\TrashBackend; use OCA\GroupFolders\Trash\TrashManager; use OCA\GroupFolders\Versions\GroupVersionsExpireManager; @@ -92,6 +93,8 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(GroupDeletedEvent::class, DeleteListener::class); $context->registerEventListener(UserDeletedEvent::class, DeleteListener::class); + $context->registerTeamResourceProvider(GroupFoldersTeamResourceProvider::class); + $context->registerService(MountProvider::class, function (ContainerInterface $c): MountProvider { /** @var IAppConfig $config */ $config = $c->get(IAppConfig::class); diff --git a/lib/Team/GroupFoldersTeamResourceProvider.php b/lib/Team/GroupFoldersTeamResourceProvider.php new file mode 100644 index 000000000..d3a7ae8b8 --- /dev/null +++ b/lib/Team/GroupFoldersTeamResourceProvider.php @@ -0,0 +1,104 @@ +'; + + /** Per-resource icon: a plain folder, matching the files team resource provider. */ + private const FOLDER_ICON_SVG = ''; + + public function __construct( + private readonly IL10N $l, + private readonly IURLGenerator $url, + private readonly FolderManager $folderManager, + ) { + } + + public function getId(): string { + return Application::APP_ID; + } + + public function getName(): string { + return $this->l->t('Team folders'); + } + + public function getIconSvg(): string { + return self::PROVIDER_ICON_SVG; + } + + public function getSharedWith(string $teamId): array { + // Membership is gated by TeamManager before this runs, so we return every + // team folder mapped to this circle without re-checking the current user. + if ($this->folderManager->getCirclesManager() === null) { + return []; + } + + $resources = []; + foreach ($this->folderManager->getAllFolders() as $folder) { + $applicable = $folder->groups[$teamId] ?? null; + if ($applicable === null || $applicable['type'] !== 'circle') { + continue; + } + + $resources[] = new TeamResource( + $this, + (string)$folder->id, + $folder->mountPoint, + $this->url->linkToRouteAbsolute('files.view.index', ['dir' => '/' . $folder->mountPoint]), + iconSvg: self::FOLDER_ICON_SVG, + ); + } + + return $resources; + } + + public function isSharedWithTeam(string $teamId, string $resourceId): bool { + if ($this->folderManager->getCirclesManager() === null) { + return false; + } + + $folder = $this->folderManager->getAllFolders()[(int)$resourceId] ?? null; + if ($folder === null) { + return false; + } + + $applicable = $folder->groups[$teamId] ?? null; + return $applicable !== null && $applicable['type'] === 'circle'; + } + + public function getTeamsForResource(string $resourceId): array { + if ($this->folderManager->getCirclesManager() === null) { + return []; + } + + $folder = $this->folderManager->getAllFolders()[(int)$resourceId] ?? null; + if ($folder === null) { + return []; + } + + $teamIds = []; + foreach ($folder->groups as $entityId => $applicable) { + if ($applicable['type'] === 'circle') { + $teamIds[] = (string)$entityId; + } + } + + return $teamIds; + } +} diff --git a/tests/Team/GroupFoldersTeamResourceProviderTest.php b/tests/Team/GroupFoldersTeamResourceProviderTest.php new file mode 100644 index 000000000..76b7fae3b --- /dev/null +++ b/tests/Team/GroupFoldersTeamResourceProviderTest.php @@ -0,0 +1,142 @@ +folderManager = $this->createMock(FolderManager::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->l10n->method('t')->willReturnArgument(0); + $this->urlGenerator->method('linkToRouteAbsolute') + ->willReturnCallback(function (string $route, array $args = []): string { + $dir = $args['dir'] ?? ''; + return 'https://cloud.example/' . $route . '?dir=' . (is_string($dir) ? $dir : ''); + }); + + $this->provider = new GroupFoldersTeamResourceProvider( + $this->l10n, + $this->urlGenerator, + $this->folderManager, + ); + } + + /** + * @param array $groups + */ + private function folder(int $id, string $mountPoint, array $groups): FolderDefinitionWithMappings { + return new FolderDefinitionWithMappings($id, $mountPoint, 0, false, false, 1, 1, [], $groups, []); + } + + /** + * @return array{displayName: string, permissions: int, type: 'circle'} + */ + private function circleApplicable(string $displayName = 'Team'): array { + return ['displayName' => $displayName, 'permissions' => 31, 'type' => 'circle']; + } + + /** + * @return array{displayName: string, permissions: int, type: 'group'} + */ + private function groupApplicable(string $displayName = 'Group'): array { + return ['displayName' => $displayName, 'permissions' => 31, 'type' => 'group']; + } + + private function withCircles(): void { + $this->folderManager->method('getCirclesManager')->willReturn($this->createMock(CirclesManager::class)); + } + + public function testStaticIdentity(): void { + $this->assertSame('groupfolders', $this->provider->getId()); + $this->assertSame('Team folders', $this->provider->getName()); + $this->assertStringContainsString('provider->getIconSvg()); + } + + public function testGetSharedWithReturnsOnlyFoldersMappedToTheTeam(): void { + $this->withCircles(); + $this->folderManager->method('getAllFolders')->willReturn([ + 1 => $this->folder(1, 'Design', ['team-1' => $this->circleApplicable('Design Team')]), + 2 => $this->folder(2, 'Marketing', ['group-x' => $this->groupApplicable()]), + 3 => $this->folder(3, 'Other', ['team-2' => $this->circleApplicable()]), + ]); + + $resources = $this->provider->getSharedWith('team-1'); + + $this->assertCount(1, $resources); + $resource = $resources[0]; + $this->assertInstanceOf(TeamResource::class, $resource); + $this->assertSame('1', $resource->getId()); + $this->assertSame('Design', $resource->getLabel()); + $this->assertStringContainsString('dir=/Design', $resource->getUrl()); + $this->assertStringContainsString('getIconSvg()); + } + + public function testGetSharedWithReturnsEmptyWhenCirclesDisabled(): void { + $this->folderManager->method('getCirclesManager')->willReturn(null); + $this->folderManager->expects($this->never())->method('getAllFolders'); + + $this->assertSame([], $this->provider->getSharedWith('team-1')); + } + + public function testIsSharedWithTeam(): void { + $this->withCircles(); + $this->folderManager->method('getAllFolders')->willReturn([ + 5 => $this->folder(5, 'Design', ['team-1' => $this->circleApplicable()]), + ]); + + $this->assertTrue($this->provider->isSharedWithTeam('team-1', '5')); + $this->assertFalse($this->provider->isSharedWithTeam('team-2', '5')); + $this->assertFalse($this->provider->isSharedWithTeam('team-1', '99')); + } + + public function testIsSharedWithTeamReturnsFalseWhenCirclesDisabled(): void { + $this->folderManager->method('getCirclesManager')->willReturn(null); + + $this->assertFalse($this->provider->isSharedWithTeam('team-1', '5')); + } + + public function testGetTeamsForResourceReturnsCircleIdsOnly(): void { + $this->withCircles(); + $this->folderManager->method('getAllFolders')->willReturn([ + 7 => $this->folder(7, 'Shared', [ + 'team-1' => $this->circleApplicable(), + 'group-x' => $this->groupApplicable(), + 'team-2' => $this->circleApplicable(), + ]), + ]); + + $this->assertEqualsCanonicalizing(['team-1', 'team-2'], $this->provider->getTeamsForResource('7')); + } + + public function testGetTeamsForResourceReturnsEmptyForUnknownFolder(): void { + $this->withCircles(); + $this->folderManager->method('getAllFolders')->willReturn([]); + + $this->assertSame([], $this->provider->getTeamsForResource('123')); + } +}