From 2dceb8d126a7ce3e2e7c16e4e3baab008d8df73b Mon Sep 17 00:00:00 2001 From: Fin Clausen Date: Fri, 21 Aug 2026 13:41:55 +0200 Subject: [PATCH 1/2] feat(teamspaces): support linking existing group folders for team folder table Co-authored-by: Copilot GPT-5.6 Terra Signed-off-by: Fin Clausen --- lib/Controller/FolderController.php | 38 +++++++ lib/Folder/FolderManager.php | 68 ++++++++++++ lib/TeamSpace/TeamSpaceProvider.php | 15 +++ lib/TeamSpace/TeamSpaceService.php | 41 +++++++ openapi.json | 129 ++++++++++++++++++++++ src/settings/Api.ts | 17 +++ src/types/openapi/openapi.ts | 71 ++++++++++++ tests/Folder/FolderManagerTest.php | 66 +++++++++++ tests/TeamSpace/TeamSpaceProviderTest.php | 23 ++++ tests/TeamSpace/TeamSpaceServiceTest.php | 49 ++++++++ 10 files changed, 517 insertions(+) diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index a2634b0d3..b2eaabbc2 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -642,4 +642,42 @@ public function aclMappingSearch(int $id, string $search = ''): DataResponse { public function getFoldersCount(): DataResponse { return new DataResponse(['count' => $this->manager->countAllFolders()]); } + + /** + * Gets all Groupfolders assigned to a circle with quota and size information + * + * @param string $circleId The circle single id to look up folders for. + * @return DataResponse, array{}> + * + * 200: Groupfolders for circle returned + */ + #[NoAdminRequired] + #[FrontpageRoute(verb: 'GET', url: '/circles/{circleId}/folders', requirements: ['circleId' => '.+'])] + public function getFoldersForCircle(string $circleId): DataResponse { + $storageId = $this->getRootFolderStorageId(); + if ($storageId === null) { + throw new OCSNotFoundException(); + } + + $folders = []; + foreach ($this->manager->getFoldersWithSizeForCircle($circleId) as $folder) { + $folders[(string)$folder->id] = $this->formatFolder($folder); + } + + if ($this->delegationService->hasOnlyApiAccess()) { + $folders = $this->foldersFilter->getForApiUser($folders); + } + + if (!$this->delegationService->hasApiAccess()) { + $folders = array_values(array_filter(array_map($this->filterNonAdminFolder(...), $folders))); + } + + return new DataResponse(array_values(array_map(static fn (array $folder): array => [ + 'id' => $folder['id'], + 'mount_point' => $folder['mount_point'], + 'quota' => $folder['quota'], + 'size' => (int)$folder['size'], + 'is_team_space' => $folder['team_circle_id'] !== null, + ], $folders))); + } } diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index 52a8a2079..8c0924ee1 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -830,6 +830,46 @@ public function getFoldersForCircle(string $circleId): array { return array_map($this->rowToFolder(...), $rows); } + /** + * Return all group folders directly assigned to or owned by a circle, + * including their current size from the file cache. + * + * @return list + * @throws Exception + */ + public function getFoldersWithSizeForCircle(string $circleId): array { + if ($circleId === '') { + throw new \InvalidArgumentException('circleId cannot be empty'); + } + + $query = $this->selectWithFileCache(); + $query->leftJoin('f', 'group_folders_groups', 'g', $query->expr()->eq('f.folder_id', 'g.folder_id')) + ->where($query->expr()->orX( + $query->expr()->eq('g.circle_id', $query->createNamedParameter($circleId)), + $query->expr()->eq('f.team_circle_id', $query->createNamedParameter($circleId)), + )); + + /** @var list $rows */ + $rows = $query->executeQuery()->fetchAll(); + + $folderIds = array_map(static fn (array $row): int => (int)$row['folder_id'], $rows); + $applicableMap = $this->getAllApplicable($folderIds); + $folderMappings = $this->getAllFolderMappings($folderIds); + + return array_map(function (array $row) use ($applicableMap, $folderMappings): FolderWithMappingsAndCache { + $folder = $this->rowToFolder($row); + $id = $folder->id; + return FolderWithMappingsAndCache::fromFolderWithMapping( + FolderDefinitionWithMappings::fromFolder( + $folder, + $applicableMap[$id] ?? [], + $this->getManageAcl($folderMappings[$id] ?? []), + ), + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + ); + }, $rows); + } + /** * @param list $paths * @return list @@ -1288,6 +1328,10 @@ public function setTeamCircleId(int $folderId, string $circleId): void { if ($existingFolderId !== null && $existingFolderId !== $folderId) { throw new Exception('This team already has a team space'); } + $existingCircleId = $this->getTeamCircleId($folderId); + if ($existingCircleId !== null && $existingCircleId !== $circleId) { + throw new Exception('This folder already belongs to another team'); + } $query = $this->connection->getQueryBuilder(); $query->update('group_folders') @@ -1296,6 +1340,30 @@ public function setTeamCircleId(int $folderId, string $circleId): void { $query->executeStatement(); } + /** + * Whether the folder is assigned exclusively to the given circle. A folder + * must have precisely one applicable mapping, and that mapping must be the + * target team; group mappings and additional teams make it ineligible. + * + * @throws Exception + */ + public function isExclusivelyAssignedToCircle(int $folderId, string $circleId): bool { + if ($circleId === '') { + return false; + } + + $query = $this->connection->getQueryBuilder(); + $query->select('group_id', 'circle_id') + ->from('group_folders_groups') + ->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))); + /** @var list $rows */ + $rows = $query->executeQuery()->fetchAll(); + + return count($rows) === 1 + && ($rows[0]['group_id'] ?? '') === '' + && ($rows[0]['circle_id'] ?? '') === $circleId; + } + /** * Clear the team ownership of a team space by resetting the * `team_circle_id` column to null. diff --git a/lib/TeamSpace/TeamSpaceProvider.php b/lib/TeamSpace/TeamSpaceProvider.php index 09c3aba22..d0f3e862a 100644 --- a/lib/TeamSpace/TeamSpaceProvider.php +++ b/lib/TeamSpace/TeamSpaceProvider.php @@ -56,6 +56,21 @@ public function createTeamFolder(Team $team, int $quota = 0): TeamFolder { return $folder; } + public function getLinkableTeamFolders(string $teamId): array { + return $this->service->getLinkableGroupFoldersForCircle($teamId); + } + + public function linkTeamFolder(string $teamId, int $folderId): TeamFolder { + $this->service->linkExistingTeamSpace($teamId, $folderId); + + $folder = $this->getTeamFolder($teamId); + if ($folder === null) { + throw new \RuntimeException('Linked team space could not be found'); + } + + return $folder; + } + #[\Override] public function unlinkTeamFolder(string $teamId): ?TeamFolder { $folder = $this->getTeamFolder($teamId); diff --git a/lib/TeamSpace/TeamSpaceService.php b/lib/TeamSpace/TeamSpaceService.php index 6d5fc2cae..bd23cc218 100644 --- a/lib/TeamSpace/TeamSpaceService.php +++ b/lib/TeamSpace/TeamSpaceService.php @@ -251,6 +251,47 @@ public function getGroupFoldersForCircle(string $circleId): array { ); } + /** + * Return existing folders directly available to a team that have not been + * made an exclusive team folder for another team. + * + * @return list + */ + public function getLinkableGroupFoldersForCircle(string $circleId): array { + return array_values(array_map( + static fn (FolderDefinition $folder): TeamFolder => new TeamFolder($folder->id, $folder->mountPoint), + array_filter( + $this->folderManager->getFoldersForCircle($circleId), + fn (FolderDefinition $folder): bool => !$folder->isTeamSpace() + && $this->folderManager->isExclusivelyAssignedToCircle($folder->id, $circleId), + ), + )); + } + + /** + * Mark an existing, directly available folder as the team's exclusive + * team folder. The ownership and eligibility checks are repeated at the + * mutation boundary to preserve the one-to-one relationship. + * + * @throws \InvalidArgumentException When the folder is not eligible. + */ + public function linkExistingTeamSpace(string $circleId, int $folderId): void { + if ($this->folderManager->getFolderIdByTeamCircleId($circleId) !== null) { + return; + } + + foreach ($this->folderManager->getFoldersForCircle($circleId) as $folder) { + if ($folder->id === $folderId + && !$folder->isTeamSpace() + && $this->folderManager->isExclusivelyAssignedToCircle($folderId, $circleId)) { + $this->folderManager->setTeamCircleId($folderId, $circleId); + return; + } + } + + throw new \InvalidArgumentException('The folder is not available for this team'); + } + /** * Whether the given circle (looked up by single id) owns a team space. */ diff --git a/openapi.json b/openapi.json index 2510c8db4..d0d2c580f 100644 --- a/openapi.json +++ b/openapi.json @@ -2833,6 +2833,135 @@ } } } + }, + "/index.php/apps/groupfolders/circles/{circleId}/folders": { + "get": { + "operationId": "folder-get-folders-for-circle", + "summary": "Gets all Groupfolders assigned to a circle with quota and size information", + "tags": [ + "folder" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "circleId", + "in": "path", + "description": "The circle single id to look up folders for.", + "required": true, + "schema": { + "type": "string", + "pattern": "^.+$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Groupfolders for circle returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "mount_point", + "quota", + "size", + "is_team_space" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "mount_point": { + "type": "string" + }, + "quota": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "is_team_space": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } } }, "tags": [] diff --git a/src/settings/Api.ts b/src/settings/Api.ts index 6c300edec..d0d2f90be 100644 --- a/src/settings/Api.ts +++ b/src/settings/Api.ts @@ -29,6 +29,23 @@ export class Api { return Object.values(response.data.ocs.data) } + async listFoldersForCircle(circleId: string): Promise> { + const response = await axios.get>>(this.getUrl(`circles/${circleId}/folders`)) + return response.data.ocs.data + } + // Returns all NC groups async listGroups(): Promise { const response = await axios.get>(this.getUrl('delegation/groups')) diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 75cfe740d..aaf6a39dc 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -259,6 +259,23 @@ export type paths = { patch?: never; trace?: never; }; + "/index.php/apps/groupfolders/circles/{circleId}/folders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Gets all Groupfolders assigned to a circle with quota and size information */ + get: operations["folder-get-folders-for-circle"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; }; export type webhooks = Record; export type components = { @@ -1479,4 +1496,58 @@ export interface operations { }; }; }; + "folder-get-folders-for-circle": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description The circle single id to look up folders for. */ + circleId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Groupfolders for circle returned */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** Format: int64 */ + id: number; + mount_point: string; + /** Format: int64 */ + quota: number; + /** Format: int64 */ + size: number; + is_team_space: boolean; + }[]; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; } diff --git a/tests/Folder/FolderManagerTest.php b/tests/Folder/FolderManagerTest.php index ed16d2424..9ac51c74c 100644 --- a/tests/Folder/FolderManagerTest.php +++ b/tests/Folder/FolderManagerTest.php @@ -773,6 +773,72 @@ public function testSetTeamCircleIdRejectsDuplicateOwner(): void { $this->manager->setTeamCircleId($secondFolderId, 'circle-owner'); } + public function testSetTeamCircleIdRejectsDifferentOwnerForFolder(): void { + $folderId = $this->manager->createFolder('team-space-owned-folder'); + $this->manager->setTeamCircleId($folderId, 'circle-owner'); + + $this->expectException(\Exception::class); + $this->manager->setTeamCircleId($folderId, 'different-circle'); + } + + public function testIsExclusivelyAssignedToCircleRejectsAdditionalMappings(): void { + $folderId = $this->manager->createFolder('exclusive-circle-folder'); + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->insert('group_folders_groups') + ->values([ + 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + 'group_id' => $query->createNamedParameter(''), + 'circle_id' => $query->createNamedParameter('circle-owner'), + 'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL), + ]); + $query->executeStatement(); + + $this->assertTrue($this->manager->isExclusivelyAssignedToCircle($folderId, 'circle-owner')); + + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->insert('group_folders_groups') + ->values([ + 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + 'group_id' => $query->createNamedParameter('additional-group'), + 'circle_id' => $query->createNamedParameter(''), + 'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL), + ]); + $query->executeStatement(); + + $this->assertFalse($this->manager->isExclusivelyAssignedToCircle($folderId, 'circle-owner')); + } + + public function testGetFoldersWithSizeForCircleIncludesAssignedAndOwnedFolders(): void { + $assignedFolderId = $this->manager->createFolder('circle-assigned-folder'); + $ownedFolderId = $this->manager->createFolder('circle-owned-folder'); + $otherFolderId = $this->manager->createFolder('other-circle-folder'); + $this->manager->setTeamCircleId($ownedFolderId, 'circle-owner'); + + foreach ([ + [$assignedFolderId, 'circle-owner'], + [$otherFolderId, 'other-circle'], + ] as [$folderId, $circleId]) { + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->insert('group_folders_groups') + ->values([ + 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + 'group_id' => $query->createNamedParameter(''), + 'circle_id' => $query->createNamedParameter($circleId), + 'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL), + ]); + $query->executeStatement(); + } + + $folders = $this->manager->getFoldersWithSizeForCircle('circle-owner'); + $folderIds = array_map(static fn (FolderDefinition $folder): int => $folder->id, $folders); + sort($folderIds); + $expectedFolderIds = [$assignedFolderId, $ownedFolderId]; + sort($expectedFolderIds); + + $this->assertSame($expectedFolderIds, $folderIds); + $this->assertNotContains($otherFolderId, $folderIds); + } + public function testTeamCircleIdIsHydratedAsNullableString(): void { $this->config->expects($this->any()) ->method('getSystemValueInt') diff --git a/tests/TeamSpace/TeamSpaceProviderTest.php b/tests/TeamSpace/TeamSpaceProviderTest.php index f95b1849b..5ba149997 100644 --- a/tests/TeamSpace/TeamSpaceProviderTest.php +++ b/tests/TeamSpace/TeamSpaceProviderTest.php @@ -65,6 +65,29 @@ public function testGetSharedWithReturnsAllFoldersAssignedToTeam(): void { $this->assertSame('https://cloud.example/apps/files/?dir=/Shared%20projects', $resources[1]->getUrl()); } + public function testGetLinkableTeamFoldersDelegatesToService(): void { + $folders = [new TeamFolder(42, 'Engineering')]; + $this->service->expects($this->once()) + ->method('getLinkableGroupFoldersForCircle') + ->with('team-1') + ->willReturn($folders); + + $this->assertSame($folders, $this->provider->getLinkableTeamFolders('team-1')); + } + + public function testLinkTeamFolderLinksAndReturnsFolder(): void { + $folder = new TeamFolder(42, 'Engineering'); + $this->service->expects($this->once()) + ->method('linkExistingTeamSpace') + ->with('team-1', 42); + $this->service->expects($this->once()) + ->method('getTeamSpaceForCircle') + ->with('team-1') + ->willReturn($folder); + + $this->assertSame($folder, $this->provider->linkTeamFolder('team-1', 42)); + } + public function testIsSharedWithTeamChecksAllFoldersAssignedToTeam(): void { $this->service->expects($this->exactly(2)) ->method('getGroupFoldersForCircle') diff --git a/tests/TeamSpace/TeamSpaceServiceTest.php b/tests/TeamSpace/TeamSpaceServiceTest.php index f2990a2ba..8de0e9002 100644 --- a/tests/TeamSpace/TeamSpaceServiceTest.php +++ b/tests/TeamSpace/TeamSpaceServiceTest.php @@ -150,6 +150,55 @@ public function testGetGroupFoldersForCircleReturnsAllAssignedFolders(): void { $this->assertSame('Shared projects', $folders[1]->getMountPoint()); } + public function testGetLinkableGroupFoldersRequiresExclusiveTeamAssignment(): void { + $this->folderManager->expects($this->once()) + ->method('getFoldersForCircle') + ->with('team-1') + ->willReturn([ + new FolderDefinition(42, 'Engineering', 0, false, false, 1, 2, []), + new FolderDefinition(43, 'Shared projects', 0, false, false, 3, 4, []), + ]); + $this->folderManager->expects($this->exactly(2)) + ->method('isExclusivelyAssignedToCircle') + ->willReturnCallback(static fn (int $folderId, string $circleId): bool => $folderId === 42 && $circleId === 'team-1'); + + $folders = $this->service->getLinkableGroupFoldersForCircle('team-1'); + + $this->assertCount(1, $folders); + $this->assertSame(42, $folders[0]->getId()); + } + + public function testLinkExistingTeamSpaceRequiresExclusiveTeamAssignment(): void { + $this->folderManager->method('getFolderIdByTeamCircleId')->with('team-1')->willReturn(null); + $this->folderManager->method('getFoldersForCircle')->with('team-1')->willReturn([ + new FolderDefinition(42, 'Engineering', 0, false, false, 1, 2, []), + ]); + $this->folderManager->expects($this->once()) + ->method('isExclusivelyAssignedToCircle') + ->with(42, 'team-1') + ->willReturn(true); + $this->folderManager->expects($this->once()) + ->method('setTeamCircleId') + ->with(42, 'team-1'); + + $this->service->linkExistingTeamSpace('team-1', 42); + } + + public function testLinkExistingTeamSpaceRejectsNonExclusiveAssignment(): void { + $this->folderManager->method('getFolderIdByTeamCircleId')->with('team-1')->willReturn(null); + $this->folderManager->method('getFoldersForCircle')->with('team-1')->willReturn([ + new FolderDefinition(42, 'Engineering', 0, false, false, 1, 2, []), + ]); + $this->folderManager->expects($this->once()) + ->method('isExclusivelyAssignedToCircle') + ->with(42, 'team-1') + ->willReturn(false); + $this->folderManager->expects($this->never())->method('setTeamCircleId'); + + $this->expectException(\InvalidArgumentException::class); + $this->service->linkExistingTeamSpace('team-1', 42); + } + public function testPickBaseNameUsesDisplayName(): void { $this->assertSame('Engineering', $this->service->pickBaseName(new Team('team-1', 'Engineering', null))); } From 4e94a1775c69dfd9a5a1d49e1dab402f96c94955 Mon Sep 17 00:00:00 2001 From: Fin Clausen Date: Tue, 25 Aug 2026 15:48:57 +0200 Subject: [PATCH 2/2] feat: add updateTeamFolderQuota method and enhance TeamFolder with quota support in admin Settings Signed-off-by: Fin Clausen --- lib/Controller/FolderController.php | 4 ++-- lib/TeamSpace/TeamSpaceProvider.php | 7 +++++++ lib/TeamSpace/TeamSpaceService.php | 23 +++++++++++++++++++++++ tests/TeamSpace/TeamSpaceProviderTest.php | 10 ++++++++++ tests/TeamSpace/TeamSpaceServiceTest.php | 19 +++++++++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index b2eaabbc2..c67d02015 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -661,7 +661,7 @@ public function getFoldersForCircle(string $circleId): DataResponse { $folders = []; foreach ($this->manager->getFoldersWithSizeForCircle($circleId) as $folder) { - $folders[(string)$folder->id] = $this->formatFolder($folder); + $folders['folder_' . $folder->id] = $this->formatFolder($folder); } if ($this->delegationService->hasOnlyApiAccess()) { @@ -677,7 +677,7 @@ public function getFoldersForCircle(string $circleId): DataResponse { 'mount_point' => $folder['mount_point'], 'quota' => $folder['quota'], 'size' => (int)$folder['size'], - 'is_team_space' => $folder['team_circle_id'] !== null, + 'is_team_space' => ($folder['team_circle_id'] ?? null) !== null, ], $folders))); } } diff --git a/lib/TeamSpace/TeamSpaceProvider.php b/lib/TeamSpace/TeamSpaceProvider.php index d0f3e862a..e34608927 100644 --- a/lib/TeamSpace/TeamSpaceProvider.php +++ b/lib/TeamSpace/TeamSpaceProvider.php @@ -56,6 +56,9 @@ public function createTeamFolder(Team $team, int $quota = 0): TeamFolder { return $folder; } + /** + * @return list + */ public function getLinkableTeamFolders(string $teamId): array { return $this->service->getLinkableGroupFoldersForCircle($teamId); } @@ -71,6 +74,10 @@ public function linkTeamFolder(string $teamId, int $folderId): TeamFolder { return $folder; } + public function updateTeamFolderQuota(string $teamId, int $quota): TeamFolder { + return $this->service->updateTeamSpaceQuota($teamId, $quota); + } + #[\Override] public function unlinkTeamFolder(string $teamId): ?TeamFolder { $folder = $this->getTeamFolder($teamId); diff --git a/lib/TeamSpace/TeamSpaceService.php b/lib/TeamSpace/TeamSpaceService.php index bd23cc218..380fc7929 100644 --- a/lib/TeamSpace/TeamSpaceService.php +++ b/lib/TeamSpace/TeamSpaceService.php @@ -230,6 +230,29 @@ public function getTeamSpaceForCircle(string $circleId): ?TeamFolder { if ($folder === null) { return null; } + return new TeamFolder($folder->id, $folder->mountPoint); + } + + /** + * Update the storage quota of the team space belonging to the given team. + * + * @param string $circleId The circle single id. + * @param int $quota Quota in bytes; zero means unlimited. + * @return TeamFolder The updated folder. + * @throws \RuntimeException if no team space is linked to the team. + */ + public function updateTeamSpaceQuota(string $circleId, int $quota): TeamFolder { + $folderId = $this->folderManager->getFolderIdByTeamCircleId($circleId); + if ($folderId === null) { + throw new \RuntimeException('No team space linked to this team'); + } + + $this->folderManager->setFolderQuota($folderId, $quota); + + $folder = $this->folderManager->getFolder($folderId); + if ($folder === null) { + throw new \RuntimeException('Team space could not be found after updating quota'); + } $this->createAppDirectory($folderId); diff --git a/tests/TeamSpace/TeamSpaceProviderTest.php b/tests/TeamSpace/TeamSpaceProviderTest.php index 5ba149997..b02da2256 100644 --- a/tests/TeamSpace/TeamSpaceProviderTest.php +++ b/tests/TeamSpace/TeamSpaceProviderTest.php @@ -88,6 +88,16 @@ public function testLinkTeamFolderLinksAndReturnsFolder(): void { $this->assertSame($folder, $this->provider->linkTeamFolder('team-1', 42)); } + public function testUpdateTeamFolderQuotaDelegatesToService(): void { + $folder = new TeamFolder(42, 'Engineering'); + $this->service->expects($this->once()) + ->method('updateTeamSpaceQuota') + ->with('team-1', 1024) + ->willReturn($folder); + + $this->assertSame($folder, $this->provider->updateTeamFolderQuota('team-1', 1024)); + } + public function testIsSharedWithTeamChecksAllFoldersAssignedToTeam(): void { $this->service->expects($this->exactly(2)) ->method('getGroupFoldersForCircle') diff --git a/tests/TeamSpace/TeamSpaceServiceTest.php b/tests/TeamSpace/TeamSpaceServiceTest.php index 8de0e9002..859e2622a 100644 --- a/tests/TeamSpace/TeamSpaceServiceTest.php +++ b/tests/TeamSpace/TeamSpaceServiceTest.php @@ -133,6 +133,25 @@ public function testUnlinkKeepsFolderAndClearsTeamLink(): void { $this->assertSame(42, $this->service->unlinkTeamSpace('team-1')); } + public function testUpdateTeamSpaceQuotaReturnsUpdatedFolder(): void { + $this->folderManager->expects($this->once()) + ->method('getFolderIdByTeamCircleId') + ->with('team-1') + ->willReturn(42); + $this->folderManager->expects($this->once()) + ->method('setFolderQuota') + ->with(42, 1024); + $this->folderManager->expects($this->once()) + ->method('getFolder') + ->with(42) + ->willReturn(new FolderDefinition(42, 'Engineering', 1024, false, false, 1, 2, [], 'team-1')); + + $folder = $this->service->updateTeamSpaceQuota('team-1', 1024); + + $this->assertSame(42, $folder->getId()); + $this->assertSame('Engineering', $folder->getMountPoint()); + } + public function testGetGroupFoldersForCircleReturnsAllAssignedFolders(): void { $this->folderManager->expects($this->once()) ->method('getFoldersForCircle')