From b88fc6b8163768b47ddaa07b0fbe664cc0bfb7bd Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 2 Mar 2026 11:21:50 +0100 Subject: [PATCH 1/4] fix(teamfolder): Fix mimetype detection in teamfolder trashbin and versions Signed-off-by: Joas Schilling --- lib/Operation.php | 34 +++++++++++++++++++++++++++++--- psalm.xml | 1 + tests/stubs/oca_groupfolders.php | 18 +++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 tests/stubs/oca_groupfolders.php diff --git a/lib/Operation.php b/lib/Operation.php index 769ef4d8..3daea92e 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -12,6 +12,7 @@ use OC\Files\FileInfo; use OC\Files\Node\Folder; use OC\Files\View; +use OCA\GroupFolders\Mount\GroupMountPoint; use OCA\WorkflowEngine\Entity\File; use OCP\EventDispatcher\Event; use OCP\Files\Cache\ICacheEntry; @@ -62,7 +63,7 @@ public function checkFileAccess(string $path, IMountPoint $mountPoint, bool $isD $this->nestingLevel++; - $filePath = $this->translatePath($storage, $path); + $filePath = $this->translatePath($mountPoint, $storage, $path); $ruleMatcher = $this->manager->getRuleMatcher(); $ruleMatcher->setFileInfo($storage, $filePath, $isDir); $node = $this->getNode($path, $mountPoint, $cacheEntry); @@ -119,8 +120,31 @@ protected function isBlockablePath(IMountPoint $mountPoint, string $path): bool /** * For thumbnails and versions we want to check the tags of the original file */ - protected function translatePath(IStorage $storage, string $path): string { - if (substr_count($path, '/') < 1) { + protected function translatePath(IMountPoint $mountPoint, IStorage $storage, string $path): string { + if ($mountPoint instanceof GroupMountPoint) { + /** + * Case | Mount point path | Path ($path) + * --------+---------------------------------------+-------------------------------- + * Files | /user/files/$folderName/ | Subfolder/File.txt + * Trash | /user/files_trashbin/groupfolder/$id/ | Subfolder/File.txt.v{timestamp} + * Version | /user/files_versions/groupfolder/$id/ | Subfolder/File.txt.d{timestamp} + */ + $mountPath = $mountPoint->getMountPoint(); + if (substr_count($mountPath, '/') >= 3) { + [,, $folder] = explode('/', $mountPath); + if ($folder === 'files_versions' && preg_match('/.+\.v\d{10}$/', basename($path))) { + // Remove trailing ".v{timestamp}" + return substr($path, 0, -12); + } + if ($folder === 'files_trashbin' && preg_match('/.+\.d\d{10}$/', basename($path))) { + // Remove trailing ".d{timestamp}" + return substr($path, 0, -12); + } + } + return $path; + } + + if (substr_count($path, '/') === 0) { return $path; } @@ -176,6 +200,10 @@ protected function isCreatingSkeletonFiles(): bool { isset($step['function']) && $step['function'] === 'tryLogin') { return true; } + if (isset($step['class']) && $step['class'] === \OCA\GroupFolders\Trash\TrashBackend::class + && isset($step['function']) && $step['function'] === 'setupTrashFolder') { + return true; + } } return false; diff --git a/psalm.xml b/psalm.xml index 82e912be..4ca98817 100644 --- a/psalm.xml +++ b/psalm.xml @@ -31,5 +31,6 @@ + diff --git a/tests/stubs/oca_groupfolders.php b/tests/stubs/oca_groupfolders.php new file mode 100644 index 00000000..238d2837 --- /dev/null +++ b/tests/stubs/oca_groupfolders.php @@ -0,0 +1,18 @@ + Date: Mon, 2 Mar 2026 13:51:05 +0100 Subject: [PATCH 2/4] fix(operation): Fix folder variable when handling versions in trashbin Signed-off-by: Joas Schilling --- lib/Operation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Operation.php b/lib/Operation.php index 3daea92e..a240612f 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -162,7 +162,7 @@ protected function translatePath(IMountPoint $mountPoint, IStorage $storage, str // 'versions', 'path/to/file.txt' $segments = explode('/', $innerPath, 2); if (isset($segments[1])) { - $innerPath = $segments[1]; + [$folder, $innerPath] = $segments; } if (preg_match('/.+\.d\d{10}$/', basename($innerPath))) { From a6b9ea85c958ee1c7c2641c9700aa492b2d52f7a Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 2 Mar 2026 17:08:42 +0100 Subject: [PATCH 3/4] test(integration): Add integration test for groupfolder mimetypes in trashbin Signed-off-by: Joas Schilling --- .github/workflows/integration.yml | 19 +- .github/workflows/phpunit-mariadb.yml | 13 ++ .github/workflows/phpunit-mysql.yml | 14 +- .github/workflows/phpunit-oci.yml | 13 ++ .github/workflows/phpunit-pgsql.yml | 13 ++ .github/workflows/phpunit-sqlite.yml | 13 ++ .../features/bootstrap/CommandLineTrait.php | 217 ++++++++++++++++++ .../features/bootstrap/FeatureContext.php | 70 +++++- tests/Integration/features/mimetypes.feature | 17 ++ tests/Integration/run.sh | 1 + 10 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 tests/Integration/features/bootstrap/CommandLineTrait.php diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 5476e546..1fcfd9a0 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -27,6 +27,7 @@ jobs: databases: ['sqlite', 'mysql', 'pgsql'] server-versions: ['stable31'] richdocuments-versions: ['stable31'] + groupfolders-versions: ['stable31'] primary-storage: ['local', 'minio'] name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}-${{ matrix.primary-storage}} @@ -36,11 +37,11 @@ jobs: env: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 - image: ghcr.io/nextcloud/continuous-integration-minio:latest + image: ghcr.io/nextcloud/continuous-integration-minio:latest # zizmor: ignore[unpinned-images] ports: - "9000:9000" postgres: - image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest + image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest # zizmor: ignore[unpinned-images] ports: - 4445:5432/tcp env: @@ -49,7 +50,7 @@ jobs: POSTGRES_DB: nextcloud options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 mysql: - image: ghcr.io/nextcloud/continuous-integration-mariadb-10.6:latest + image: ghcr.io/nextcloud/continuous-integration-mariadb-10.6:latest # zizmor: ignore[unpinned-images] ports: - 4444:3306/tcp env: @@ -83,6 +84,14 @@ jobs: repository: nextcloud/richdocuments ref: ${{ matrix.richdocuments-versions }} + - name: Checkout app (groupfolders) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: apps/groupfolders + repository: nextcloud/groupfolders + ref: ${{ matrix.groupfolders-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: @@ -108,6 +117,10 @@ jobs: working-directory: apps/richdocuments run: composer i --no-dev + - name: Set up dependencies (groupfolders) + working-directory: apps/groupfolders + run: composer i --no-dev + - name: Set up Nextcloud for S3 primary storage if: matrix.primary-storage == 'minio' run: | diff --git a/.github/workflows/phpunit-mariadb.yml b/.github/workflows/phpunit-mariadb.yml index 0aec893c..933a591b 100644 --- a/.github/workflows/phpunit-mariadb.yml +++ b/.github/workflows/phpunit-mariadb.yml @@ -72,6 +72,7 @@ jobs: server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} mariadb-versions: ['10.6', '11.4'] richdocuments-versions: ['stable31'] + groupfolders-versions: ['stable31'] name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -113,6 +114,14 @@ jobs: repository: nextcloud/richdocuments ref: ${{ matrix.richdocuments-versions }} + - name: Checkout app (groupfolders) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: apps/groupfolders + repository: nextcloud/groupfolders + ref: ${{ matrix.groupfolders-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 with: @@ -149,6 +158,10 @@ jobs: working-directory: apps/richdocuments run: composer i --no-dev + - name: Set up dependencies (groupfolders) + working-directory: apps/groupfolders + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/.github/workflows/phpunit-mysql.yml b/.github/workflows/phpunit-mysql.yml index f31f17d9..e9f2051f 100644 --- a/.github/workflows/phpunit-mysql.yml +++ b/.github/workflows/phpunit-mysql.yml @@ -32,7 +32,7 @@ jobs: id: versions uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 with: - matrix: '{"mysql-versions": ["8.4"], "richdocuments-versions": ["stable31"]}' + matrix: '{"mysql-versions": ["8.4"], "richdocuments-versions": ["stable31"], "groupfolders-versions": ["stable31"]}' changes: runs-on: ubuntu-latest-low @@ -110,6 +110,14 @@ jobs: repository: nextcloud/richdocuments ref: ${{ matrix.richdocuments-versions }} + - name: Checkout app (groupfolders) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: apps/groupfolders + repository: nextcloud/groupfolders + ref: ${{ matrix.groupfolders-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 with: @@ -146,6 +154,10 @@ jobs: working-directory: apps/richdocuments run: composer i --no-dev + - name: Set up dependencies (groupfolders) + working-directory: apps/groupfolders + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/.github/workflows/phpunit-oci.yml b/.github/workflows/phpunit-oci.yml index 06174594..cf3193e1 100644 --- a/.github/workflows/phpunit-oci.yml +++ b/.github/workflows/phpunit-oci.yml @@ -71,6 +71,7 @@ jobs: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} richdocuments-versions: ['stable31'] + groupfolders-versions: ['stable31'] oci-versions: ['18', '21', '23'] name: OCI ${{ matrix.oci-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -123,6 +124,14 @@ jobs: repository: nextcloud/richdocuments ref: ${{ matrix.richdocuments-versions }} + - name: Checkout app (groupfolders) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: apps/groupfolders + repository: nextcloud/groupfolders + ref: ${{ matrix.groupfolders-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 with: @@ -154,6 +163,10 @@ jobs: working-directory: apps/richdocuments run: composer i --no-dev + - name: Set up dependencies (groupfolders) + working-directory: apps/groupfolders + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 1521 diff --git a/.github/workflows/phpunit-pgsql.yml b/.github/workflows/phpunit-pgsql.yml index 67f7ce4b..f04533cd 100644 --- a/.github/workflows/phpunit-pgsql.yml +++ b/.github/workflows/phpunit-pgsql.yml @@ -71,6 +71,7 @@ jobs: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} richdocuments-versions: ['stable31'] + groupfolders-versions: ['stable31'] name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -114,6 +115,14 @@ jobs: repository: nextcloud/richdocuments ref: ${{ matrix.richdocuments-versions }} + - name: Checkout app (groupfolders) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: apps/groupfolders + repository: nextcloud/groupfolders + ref: ${{ matrix.groupfolders-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 with: @@ -145,6 +154,10 @@ jobs: working-directory: apps/richdocuments run: composer i --no-dev + - name: Set up dependencies (groupfolders) + working-directory: apps/groupfolders + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml index 7d1bafda..617edab2 100644 --- a/.github/workflows/phpunit-sqlite.yml +++ b/.github/workflows/phpunit-sqlite.yml @@ -71,6 +71,7 @@ jobs: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} richdocuments-versions: ['stable31'] + groupfolders-versions: ['stable31'] name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -103,6 +104,14 @@ jobs: repository: nextcloud/richdocuments ref: ${{ matrix.richdocuments-versions }} + - name: Checkout app (groupfolders) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + path: apps/groupfolders + repository: nextcloud/groupfolders + ref: ${{ matrix.groupfolders-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 with: @@ -134,6 +143,10 @@ jobs: working-directory: apps/richdocuments run: composer i --no-dev + - name: Set up dependencies (groupfolders) + working-directory: apps/groupfolders + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/tests/Integration/features/bootstrap/CommandLineTrait.php b/tests/Integration/features/bootstrap/CommandLineTrait.php new file mode 100644 index 00000000..574f9375 --- /dev/null +++ b/tests/Integration/features/bootstrap/CommandLineTrait.php @@ -0,0 +1,217 @@ + escapeshellarg($arg), $args); + $args[] = '--no-ansi'; + $argString = implode(' ', $args); + + $descriptor = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open('php console.php ' . $argString, $descriptor, $pipes, $this->ocPath, $env); + $this->lastStdOut = stream_get_contents($pipes[1]); + $this->lastStdErr = stream_get_contents($pipes[2]); + $this->lastCode = proc_close($process); + + if ($clearOpcodeCache) { + // Clean opcode cache + $client = new GuzzleHttp\Client(); + + if ($this->currentServer === 'REMOTE') { + $client->request('GET', $this->remoteServerUrl . 'apps/testing/clean_opcode_cache.php'); + } else { + $client->request('GET', $this->localServerUrl . 'apps/testing/clean_opcode_cache.php'); + } + } + + return $this->lastCode; + } + + /** + * @Given /^invoking occ with "([^"]*)"$/ + */ + public function invokingTheCommand(string $cmd, ?\Behat\Gherkin\Node\TableNode $table = null) { + if ($cmd !== 'table') { + if (str_contains($cmd, '{LAST_COMMAND_OUTPUT}')) { + echo 'Replacing {LAST_COMMAND_OUTPUT} with "' . trim($this->lastStdOut) . '"'; + } + $cmd = str_replace('{LAST_COMMAND_OUTPUT}', trim($this->lastStdOut), $cmd); + $args = explode(' ', $cmd); + } else { + $args = []; + foreach ($table->getRows() as $row) { + if ($row[0] === '') { + $args[] = $row[1]; + } elseif ($row[1] === '') { + $args[] = $row[0]; + } else { + $args[] = $row[0] . '=' . $row[1]; + } + } + } + + $this->runOcc($args); + } + + public function getLastStdOut(): string { + return $this->lastStdOut; + } + + /** + * Find exception texts in stderr + */ + public function findExceptions() { + $exceptions = []; + $captureNext = false; + // the exception text usually appears after an "[Exception"] row + foreach (explode("\n", $this->lastStdErr) as $line) { + if (preg_match('/\[Exception\]/', $line)) { + $captureNext = true; + continue; + } + if ($captureNext) { + $exceptions[] = trim($line); + $captureNext = false; + } + } + + return $exceptions; + } + + /** + * @Then /^the command was successful$/ + */ + public function theCommandWasSuccessful() { + $exceptions = $this->findExceptions(); + if ($this->lastCode !== 0) { + echo $this->lastStdErr; + + $msg = 'The command was not successful, exit code was ' . $this->lastCode . '.'; + if (!empty($exceptions)) { + $msg .= "\n" . ' Exceptions: ' . implode(', ', $exceptions); + } else { + $msg .= "\n" . ' ' . $this->lastStdOut; + $msg .= "\n" . ' ' . $this->lastStdErr; + } + throw new \Exception($msg); + } elseif (!empty($exceptions)) { + $msg = 'The command was successful but triggered exceptions: ' . implode(', ', $exceptions); + throw new \Exception($msg); + } + } + + /** + * @Then /^the command failed with exit code ([0-9]+)$/ + */ + public function theCommandFailedWithExitCode(int $exitCode) { + Assert::assertEquals($exitCode, $this->lastCode, 'The commands exit code did not match'); + } + + /** + * @Then /^the command failed with exception text "([^"]*)"$/ + */ + public function theCommandFailedWithException($exceptionText) { + $exceptions = $this->findExceptions(); + if (empty($exceptions)) { + throw new \Exception('The command did not throw any exceptions'); + } + + if (!in_array($exceptionText, $exceptions)) { + throw new \Exception('The command did not throw any exception with the text "' . $exceptionText . '"'); + } + } + + /** + * @Then /^the command output contains the text:$/ + * @Then /^the command output contains the text "([^"]*)"$/ + */ + public function theCommandOutputContainsTheText($text) { + if ($this->lastStdOut === '' && $this->lastStdErr !== '') { + Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stdout'); + Assert::assertTrue(false, 'The command did not output the expected text on stdout but stderr'); + } + + Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout'); + } + + /** + * @Then /^the command output is empty$/ + */ + public function theCommandOutputIsEmpty() { + Assert::assertEmpty($this->lastStdOut, 'The command did output unexpected text on stdout'); + } + + /** + * @Then /^the command output contains the list entry '([^']*)' with value '([^']*)'$/ + */ + public function theCommandOutputContainsTheListEntry(string $key, string $value): void { + if (preg_match('/^"ROOM\(([^"]+)\)"$/', $key, $matches)) { + $key = '"' . self::$identifierToToken[$matches[1]] . '"'; + } + $text = '- ' . $key . ': ' . $value; + + if ($this->lastStdOut === '' && $this->lastStdErr !== '') { + Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stdout'); + Assert::assertTrue(false, 'The command did not output the expected text on stdout but stderr'); + } + + Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout'); + } + + /** + * @Then /^the command error output contains the text "([^"]*)"$/ + */ + public function theCommandErrorOutputContainsTheText($text) { + if ($this->lastStdErr === '' && $this->lastStdOut !== '') { + Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout'); + Assert::assertTrue(false, 'The command did not output the expected text on stdout but stderr'); + } + + Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stderr'); + } +} diff --git a/tests/Integration/features/bootstrap/FeatureContext.php b/tests/Integration/features/bootstrap/FeatureContext.php index be307711..fb865133 100644 --- a/tests/Integration/features/bootstrap/FeatureContext.php +++ b/tests/Integration/features/bootstrap/FeatureContext.php @@ -13,6 +13,8 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; +use Behat\Step\Given; +use Behat\Step\When; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; @@ -26,6 +28,7 @@ */ class FeatureContext implements Context { use WebDav; + use CommandLineTrait; protected $regularUser = '123456'; protected $adminUser = ['admin', 'admin']; @@ -44,6 +47,8 @@ class FeatureContext implements Context { protected string $tagId = ''; protected array $createdUsers = []; + protected array $createdGroups = []; + protected array $createdGroupFolders = []; protected array $changedConfigs = []; @@ -78,6 +83,13 @@ public function tearDown() { foreach ($this->createdUsers as $user) { $this->deleteUser($user); } + foreach ($this->createdGroups as $group) { + $this->deleteGroup($group); + } + foreach ($this->createdGroupFolders as $folderId) { + $this->runOcc(['groupfolders:delete', '--force', $folderId]); + $this->theCommandWasSuccessful(); + } } /** @@ -155,6 +167,16 @@ public function userSharesFilePublicly(string $sharer, string $file): void { $this->lastShareData = $responseBody['ocs']['data']; } + #[Given(pattern: '/^setup group folder "([^"]*)" for group "([^"]*)"$/')] + public function createGroupFolderForGroup(string $folder, string $group): void { + $this->runOcc(['groupfolders:create', $folder]); + $this->theCommandWasSuccessful(); + $folderId = (string)(int)$this->lastStdOut; + $this->createdGroupFolders[] = $folderId; + $this->runOcc(['groupfolders:group', $folderId, $group, 'read', 'write', 'delete']); + $this->theCommandWasSuccessful(); + } + // ChecksumsContext /** * @Then The webdav response should have a status code :statusCode @@ -192,8 +214,10 @@ public function setAppConfig(string $appId, TableNode $formData): void { * @Given /^as user "([^"]*)"$/ * @param string $user */ - public function setCurrentUser(string $user): void { + public function setCurrentUser(string $user): string { + $before = $this->currentUser; $this->currentUser = $user; + return $before ?? 'admin'; } /** @@ -271,6 +295,50 @@ private function deleteUser(string $user): void { $this->currentUser = $previous_user; } + #[Given('/^group "([^"]*)" exists$/')] + public function assureGroupExists(string $group): void { + $currentUser = $this->setCurrentUser('admin'); + $this->sendingToWith('POST', '/cloud/groups', [ + 'groupid' => $group, + ]); + + $jsonBody = json_decode($this->response->getBody()->getContents(), true); + if (isset($jsonBody['ocs']['meta'])) { + // 102 = group exists + // 200 = created with success + Assert::assertContains( + $jsonBody['ocs']['meta']['statuscode'], + [102, 200], + $jsonBody['ocs']['meta']['message'] + ); + } else { + throw new \Exception('Invalid response when create group'); + } + + $this->setCurrentUser($currentUser); + + $this->createdGroups[$group] = $group; + } + + private function deleteGroup(string $group): void { + $currentUser = $this->setCurrentUser('admin'); + $this->sendingTo('DELETE', '/cloud/groups/' . $group); + $this->setCurrentUser($currentUser); + + unset($this->createdGroups[$group]); + $this->setCurrentUser($currentUser); + } + + #[When('/^user "([^"]*)" is member of group "([^"]*)"$/')] + public function addingUserToGroup(string $user, string $group): void { + $currentUser = $this->setCurrentUser('admin'); + $this->sendingToWith('POST', "/cloud/users/$user/groups", [ + 'groupid' => $group, + ]); + $this->assertStatusCode($this->response, 200); + $this->setCurrentUser($currentUser); + } + /* * Requests */ diff --git a/tests/Integration/features/mimetypes.feature b/tests/Integration/features/mimetypes.feature index ec39d98f..daa26dda 100644 --- a/tests/Integration/features/mimetypes.feature +++ b/tests/Integration/features/mimetypes.feature @@ -98,3 +98,20 @@ | checks-1 | {"class":"OCA\\\\WorkflowEngine\\\\Check\\\\FileMimeType", "operator": "!is", "value": "text/plain"} | When User "test1" deletes file "/foobar.txt" Then The webdav response should have a status code "204" + + Scenario: Blocking by mimetype works in trashbin of groupfolders + Given group "group886" exists + And user "test1" is member of group "group886" + And setup group folder "Folder886" for group "group886" + Given User "test1" uploads file "data/textfile.txt" to "/Folder886/foobar.txt" + Then The webdav response should have a status code "201" + And user "admin" creates global flow with 200 + | name | Admin flow | + | class | OCA\FilesAccessControl\Operation | + | entity | OCA\WorkflowEngine\Entity\File | + | events | [] | + | operation | deny | + | checks-0 | {"class":"OCA\\\\WorkflowEngine\\\\Check\\\\FileMimeType", "operator": "!is", "value": "httpd/unix-directory"} | + | checks-1 | {"class":"OCA\\\\WorkflowEngine\\\\Check\\\\FileMimeType", "operator": "!is", "value": "text/plain"} | + When User "test1" deletes file "/Folder886/foobar.txt" + Then The webdav response should have a status code "204" diff --git a/tests/Integration/run.sh b/tests/Integration/run.sh index e6ab34b1..bf8b2871 100755 --- a/tests/Integration/run.sh +++ b/tests/Integration/run.sh @@ -17,6 +17,7 @@ cp -R ./app "../../../${APP_NAME}_testing" ${ROOT_DIR}/occ app:enable $APP_NAME ${ROOT_DIR}/occ app:enable --force "${APP_NAME}_testing" ${ROOT_DIR}/occ app:enable --force richdocuments +${ROOT_DIR}/occ app:enable --force groupfolders ${ROOT_DIR}/occ app:list | grep $APP_NAME export TEST_SERVER_URL="http://localhost:8080/" From 379f6dfb982f6eedb7392e63c5cf6c06dbe86890 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 2 Mar 2026 17:12:33 +0100 Subject: [PATCH 4/4] chore(behat): Migrate to attributes Signed-off-by: Joas Schilling --- .../features/bootstrap/FeatureContext.php | 74 +++++-------------- 1 file changed, 19 insertions(+), 55 deletions(-) diff --git a/tests/Integration/features/bootstrap/FeatureContext.php b/tests/Integration/features/bootstrap/FeatureContext.php index fb865133..c5733f0c 100644 --- a/tests/Integration/features/bootstrap/FeatureContext.php +++ b/tests/Integration/features/bootstrap/FeatureContext.php @@ -13,7 +13,10 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; +use Behat\Hook\AfterScenario; +use Behat\Hook\BeforeScenario; use Behat\Step\Given; +use Behat\Step\Then; use Behat\Step\When; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; @@ -60,10 +63,8 @@ public function __construct() { $this->baseUrl = getenv('TEST_SERVER_URL'); } - /** - * @BeforeScenario - * @AfterScenario - */ + #[BeforeScenario] + #[AfterScenario] public function cleanUpBetweenTests() { $this->setCurrentUser('admin'); $this->sendingTo('DELETE', '/apps/files_accesscontrol_testing'); @@ -76,9 +77,7 @@ public function cleanUpBetweenTests() { } } - /** - * @AfterScenario - */ + #[AfterScenario] public function tearDown() { foreach ($this->createdUsers as $user) { $this->deleteUser($user); @@ -92,9 +91,7 @@ public function tearDown() { } } - /** - * @Given /^Ensure tag exists$/ - */ + #[Given(pattern: 'Ensure tag exists')] public function createTag() { $this->setCurrentUser('admin'); $this->sendingTo('POST', '/apps/files_accesscontrol_testing'); @@ -105,9 +102,7 @@ public function createTag() { $this->tagId = $data['tagId']; } - /** - * @Given /^user "([^"]*)" tags file "([^"]*)"$/ - */ + #[Given(pattern: '/^user "([^"]*)" tags file "([^"]*)"$/')] public function tagFile(string $user, string $path) { // TODO: Remove all created tags? $this->setCurrentUser($user); @@ -117,9 +112,7 @@ public function tagFile(string $user, string $path) { $this->assertStatusCode($this->response, 200); } - /** - * @Given /^user "([^"]*)" creates (global|user) flow with (\d+)$/ - */ + #[Given(pattern: '/^user "([^"]*)" creates (global|user) flow with (\d+)$/')] public function createFlow(string $user, string $scope, int $statusCode, TableNode $tableNode) { $this->setCurrentUser($user); @@ -140,9 +133,7 @@ public function createFlow(string $user, string $scope, int $statusCode, TableNo Assert::assertSame($statusCode, $this->response->getStatusCode(), 'HTTP status code mismatch:' . "\n" . $this->response->getBody()->getContents()); } - /** - * @Given /^user "([^"]*)" shares file "([^"]*)" with user "([^"]*)"$/ - */ + #[Given(pattern: '/^user "([^"]*)" shares file "([^"]*)" with user "([^"]*)"$/')] public function userSharesFile(string $sharer, string $file, string $sharee): void { $this->setCurrentUser($sharer); $this->sendingToWith('POST', '/apps/files_sharing/api/v1/shares', [ @@ -153,9 +144,7 @@ public function userSharesFile(string $sharer, string $file, string $sharee): vo ]); } - /** - * @Given /^user "([^"]*)" shares file "([^"]*)" publicly$/ - */ + #[Given(pattern: '/^user "([^"]*)" shares file "([^"]*)" publicly$/')] public function userSharesFilePublicly(string $sharer, string $file): void { $this->setCurrentUser($sharer); $this->sendingToWith('POST', '/apps/files_sharing/api/v1/shares', [ @@ -178,12 +167,8 @@ public function createGroupFolderForGroup(string $folder, string $group): void { } // ChecksumsContext - /** - * @Then The webdav response should have a status code :statusCode - * @param string $statusCode - * @throws \Exception - */ - public function theWebdavResponseShouldHaveAStatusCode($statusCode) { + #[Then(pattern: 'The webdav response should have a status code :statusCode')] + public function theWebdavResponseShouldHaveAStatusCode(string $statusCode) { if (str_contains($statusCode, '|')) { $statusCodes = array_map('intval', explode('|', $statusCode)); } else { @@ -195,7 +180,7 @@ public function theWebdavResponseShouldHaveAStatusCode($statusCode) { } - #[\Behat\Step\Given('the following :appId app config is set')] + #[Given('the following :appId app config is set')] public function setAppConfig(string $appId, TableNode $formData): void { $this->setCurrentUser('admin'); foreach ($formData->getRows() as $row) { @@ -209,21 +194,14 @@ public function setAppConfig(string $appId, TableNode $formData): void { /** * User management */ - - /** - * @Given /^as user "([^"]*)"$/ - * @param string $user - */ + #[Given('/^as user "([^"]*)"$/')] public function setCurrentUser(string $user): string { $before = $this->currentUser; $this->currentUser = $user; return $before ?? 'admin'; } - /** - * @Given /^user "([^"]*)" exists$/ - * @param string $user - */ + #[Given('/^user "([^"]*)" exists$/')] public function assureUserExists(string $user): void { try { $this->userExists($user); @@ -343,22 +321,12 @@ public function addingUserToGroup(string $user, string $group): void { * Requests */ - /** - * @When /^sending "([^"]*)" to "([^"]*)"$/ - * @param string $verb - * @param string $url - */ + #[When(pattern: '/^sending "([^"]*)" to "([^"]*)"$/')] public function sendingTo(string $verb, string $url): void { - $this->sendingToWith($verb, $url, null); + $this->sendingToWith($verb, $url); } - /** - * @When /^sending "([^"]*)" to "([^"]*)" with$/ - * @param string $verb - * @param string $url - * @param array|null $body - * @param array $headers - */ + #[When(pattern: '/^sending "([^"]*)" to "([^"]*)" with$/')] public function sendingToWith(string $verb, string $url, ?array $body = null, array $headers = []): void { $fullUrl = $this->baseUrl . 'ocs/v2.php' . $url; $client = new Client(); @@ -388,10 +356,6 @@ public function sendingToWith(string $verb, string $url, ?array $body = null, ar } } - /** - * @param ResponseInterface $response - * @param int $statusCode - */ protected function assertStatusCode(ResponseInterface $response, int $statusCode): void { Assert::assertEquals($statusCode, $response->getStatusCode()); }