Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions lib/Folder/FolderManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,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')
Expand All @@ -1296,6 +1300,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<array{group_id: ?string, circle_id: ?string}> $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.
Expand Down
25 changes: 25 additions & 0 deletions lib/TeamSpace/TeamSpaceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ public function createTeamFolder(Team $team, int $quota = 0): TeamFolder {
return $folder;
}

/**
* @return list<TeamFolder>
*/
#[\Override]
public function getLinkableTeamFolders(string $teamId): array {
return $this->service->getLinkableGroupFoldersForCircle($teamId);
}

#[\Override]
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 updateTeamFolderQuota(string $teamId, int $quota): TeamFolder {
return $this->service->updateTeamSpaceQuota($teamId, $quota);
}

#[\Override]
public function unlinkTeamFolder(string $teamId): ?TeamFolder {
$folder = $this->getTeamFolder($teamId);
Expand Down
65 changes: 65 additions & 0 deletions lib/TeamSpace/TeamSpaceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,30 @@ public function getTeamSpaceForCircle(string $circleId): ?TeamFolder {
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');
}

return new TeamFolder($folder->id, $folder->mountPoint);
}

/**
* Return all group folders directly accessible to the given team.
*
Expand All @@ -251,6 +275,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<TeamFolder>
*/
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.
*/
Expand Down
35 changes: 35 additions & 0 deletions tests/Folder/FolderManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,41 @@ 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 testTeamCircleIdIsHydratedAsNullableString(): void {
$this->config->expects($this->any())
->method('getSystemValueInt')
Expand Down
33 changes: 33 additions & 0 deletions tests/TeamSpace/TeamSpaceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,39 @@ 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 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')
Expand Down
68 changes: 68 additions & 0 deletions tests/TeamSpace/TeamSpaceServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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($this->createTeamSpaceFolder());

$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')
Expand All @@ -150,6 +169,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)));
}
Expand Down
Loading