diff options
Diffstat (limited to 'build/integration/features')
41 files changed, 727 insertions, 164 deletions
diff --git a/build/integration/features/bootstrap/Activity.php b/build/integration/features/bootstrap/Activity.php new file mode 100644 index 00000000000..4172776304d --- /dev/null +++ b/build/integration/features/bootstrap/Activity.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +trait Activity { + use BasicStructure; + + /** + * @Then last activity should be + * @param TableNode $activity + */ + public function lastActivityIs(TableNode $activity): void { + $this->sendRequestForJSON('GET', '/apps/activity/api/v2/activity'); + $this->theHTTPStatusCodeShouldBe('200'); + $data = json_decode($this->response->getBody()->getContents(), true); + $activities = $data['ocs']['data']; + /* Sort by id */ + uasort($activities, fn ($a, $b) => $a['activity_id'] <=> $b['activity_id']); + $lastActivity = array_pop($activities); + foreach ($activity->getRowsHash() as $key => $value) { + Assert::assertEquals($value, $lastActivity[$key]); + } + } +} diff --git a/build/integration/features/bootstrap/AppConfiguration.php b/build/integration/features/bootstrap/AppConfiguration.php index 5f39c58ffeb..e8580ed537b 100644 --- a/build/integration/features/bootstrap/AppConfiguration.php +++ b/build/integration/features/bootstrap/AppConfiguration.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php index ffc391c0d45..beebf1c024a 100644 --- a/build/integration/features/bootstrap/Avatar.php +++ b/build/integration/features/bootstrap/Avatar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -240,10 +241,10 @@ trait Avatar { } private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) { - if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) && - $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) && - $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) && - $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { + if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) + && $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) + && $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) + && $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { return true; } @@ -251,8 +252,8 @@ trait Avatar { } private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { - if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) && - $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { + if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) + && $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { return true; } diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index b51c0a6a34c..59a4312913e 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -8,6 +9,7 @@ use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; @@ -18,6 +20,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; @@ -120,7 +123,11 @@ trait BasicStructure { * @return string */ public function getOCSResponse($response) { - return simplexml_load_string($response->getBody())->meta[0]->statuscode; + $body = simplexml_load_string((string)$response->getBody()); + if ($body === false) { + throw new \RuntimeException('Could not parse OCS response, body is not valid XML'); + } + return $body->meta[0]->statuscode; } /** @@ -170,6 +177,8 @@ trait BasicStructure { $this->response = $client->request($verb, $fullUrl, $options); } catch (ClientException $ex) { $this->response = $ex->getResponse(); + } catch (ServerException $ex) { + $this->response = $ex->getResponse(); } } @@ -185,8 +194,8 @@ trait BasicStructure { $options = []; if ($this->currentUser === 'admin') { $options['auth'] = ['admin', 'admin']; - } elseif (strpos($this->currentUser, 'guest') !== 0) { - $options['auth'] = [$this->currentUser, self::TEST_PASSWORD]; + } elseif (strpos($this->currentUser, 'anonymous') !== 0) { + $options['auth'] = [$this->currentUser, $this->regularUser]; } if ($body instanceof TableNode) { $fd = $body->getRowsHash(); diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php index 80f8c53fc4e..459c35089fa 100644 --- a/build/integration/features/bootstrap/CalDavContext.php +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php index ae32056e17f..7d09ab6ddcf 100644 --- a/build/integration/features/bootstrap/CapabilitiesContext.php +++ b/build/integration/features/bootstrap/CapabilitiesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php index a59f0d56f96..733c98dca02 100644 --- a/build/integration/features/bootstrap/CardDavContext.php +++ b/build/integration/features/bootstrap/CardDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php index 3392f8545d9..c8abf91127e 100644 --- a/build/integration/features/bootstrap/ChecksumsContext.php +++ b/build/integration/features/bootstrap/ChecksumsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLine.php b/build/integration/features/bootstrap/CommandLine.php index 84b3dfd447f..924d723daa6 100644 --- a/build/integration/features/bootstrap/CommandLine.php +++ b/build/integration/features/bootstrap/CommandLine.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php index 5ea8d12a970..e7764356270 100644 --- a/build/integration/features/bootstrap/CommandLineContext.php +++ b/build/integration/features/bootstrap/CommandLineContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -109,19 +110,6 @@ class CommandLineContext implements \Behat\Behat\Context\Context { } /** - * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)" with received shares$/ - */ - public function transferringOwnershipPathWithIncomingShares($path, $user1, $user2) { - $path = '--path=' . $path; - if ($this->runOcc(['files:transfer-ownership', $path, $user1, $user2, '--transfer-incoming-shares=1']) === 0) { - $this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2); - } else { - // failure - $this->lastTransferPath = null; - } - } - - /** * @When /^using received transfer folder of "([^"]+)" as dav path$/ */ public function usingTransferFolderAsDavPath($user) { diff --git a/build/integration/features/bootstrap/CommentsContext.php b/build/integration/features/bootstrap/CommentsContext.php index 17795a48fb4..53001b1c204 100644 --- a/build/integration/features/bootstrap/CommentsContext.php +++ b/build/integration/features/bootstrap/CommentsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/ContactsMenu.php b/build/integration/features/bootstrap/ContactsMenu.php index 4fc3c03c5e9..f6bf6b9422b 100644 --- a/build/integration/features/bootstrap/ContactsMenu.php +++ b/build/integration/features/bootstrap/ContactsMenu.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ConversionsContext.php b/build/integration/features/bootstrap/ConversionsContext.php new file mode 100644 index 00000000000..ccd14c460f8 --- /dev/null +++ b/build/integration/features/bootstrap/ConversionsContext.php @@ -0,0 +1,60 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; +use Behat\Gherkin\Node\TableNode; + +class ConversionsContext implements Context, SnippetAcceptingContext { + use AppConfiguration; + use BasicStructure; + use WebDav; + + /** @BeforeScenario */ + public function setUpScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(true); + } + + /** @AfterScenario */ + public function tearDownScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(false); + } + + protected function resetAppConfigs() { + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileId(string $user, string $path, string $mime) { + $this->userConvertsTheSavedFileIdTo($user, $path, $mime, null); + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)" and saves it to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileIdTo(string $user, string $path, string $mime, ?string $destination) { + try { + $fileId = $this->getFileIdForPath($user, $path); + } catch (Exception $e) { + // return a fake value to keep going and be able to test the error + $fileId = 0; + } + + $data = [['fileId', $fileId], ['targetMimeType', $mime]]; + if ($destination !== null) { + $data[] = ['destination', $destination]; + } + + $this->asAn($user); + $this->sendingToWith('post', '/apps/files/api/v1/convert', new TableNode($data)); + } +} diff --git a/build/integration/features/bootstrap/DavFeatureContext.php b/build/integration/features/bootstrap/DavFeatureContext.php index acca52ccafc..ec6085cff98 100644 --- a/build/integration/features/bootstrap/DavFeatureContext.php +++ b/build/integration/features/bootstrap/DavFeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Download.php b/build/integration/features/bootstrap/Download.php index 2a66f7c3d89..549a033346e 100644 --- a/build/integration/features/bootstrap/Download.php +++ b/build/integration/features/bootstrap/Download.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ExternalStorage.php b/build/integration/features/bootstrap/ExternalStorage.php index b1e4c92810b..8fe2653a026 100644 --- a/build/integration/features/bootstrap/ExternalStorage.php +++ b/build/integration/features/bootstrap/ExternalStorage.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/FakeSMTPHelper.php b/build/integration/features/bootstrap/FakeSMTPHelper.php index caf2139faab..32387869edd 100644 --- a/build/integration/features/bootstrap/FakeSMTPHelper.php +++ b/build/integration/features/bootstrap/FakeSMTPHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php index 59f1d0068dd..ab37556f931 100644 --- a/build/integration/features/bootstrap/FeatureContext.php +++ b/build/integration/features/bootstrap/FeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -13,9 +14,16 @@ require __DIR__ . '/../../vendor/autoload.php'; * Features context. */ class FeatureContext implements Context, SnippetAcceptingContext { + use AppConfiguration; use ContactsMenu; use ExternalStorage; use Search; use WebDav; use Trashbin; + + protected function resetAppConfigs(): void { + $this->deleteServerConfig('bruteForce', 'whitelist_0'); + $this->deleteServerConfig('bruteForce', 'whitelist_1'); + $this->deleteServerConfig('bruteforcesettings', 'apply_allowlist_to_ratelimit'); + } } diff --git a/build/integration/features/bootstrap/FederationContext.php b/build/integration/features/bootstrap/FederationContext.php index bbd81396df5..95dc8119ad6 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -7,6 +8,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; @@ -168,8 +170,52 @@ class FederationContext implements Context, SnippetAcceptingContext { self::$phpFederatedServerPid = ''; } + /** + * @BeforeScenario @TrustedFederation + */ + public function theServersAreTrustingEachOther() { + $this->asAn('admin'); + // Trust the remote server on the local server + $this->usingServer('LOCAL'); + $this->sendRequestForJSON('POST', '/apps/federation/trusted-servers', ['url' => 'http://localhost:' . getenv('PORT')]); + Assert::assertTrue(($this->response->getStatusCode() === 200 || $this->response->getStatusCode() === 409)); + + // Trust the local server on the remote server + $this->usingServer('REMOTE'); + $this->sendRequestForJSON('POST', '/apps/federation/trusted-servers', ['url' => 'http://localhost:' . getenv('PORT_FED')]); + // If the server is already trusted, we expect a 409 + Assert::assertTrue(($this->response->getStatusCode() === 200 || $this->response->getStatusCode() === 409)); + } + + /** + * @AfterScenario @TrustedFederation + */ + public function theServersAreNoLongerTrustingEachOther() { + $this->asAn('admin'); + // Untrust the remote servers on the local server + $this->usingServer('LOCAL'); + $this->sendRequestForJSON('GET', '/apps/federation/trusted-servers'); + $this->theHTTPStatusCodeShouldBe('200'); + $trustedServersIDs = array_map(fn ($server) => $server->id, json_decode($this->response->getBody())->ocs->data); + foreach ($trustedServersIDs as $id) { + $this->sendRequestForJSON('DELETE', '/apps/federation/trusted-servers/' . $id); + $this->theHTTPStatusCodeShouldBe('200'); + } + + // Untrust the local server on the remote server + $this->usingServer('REMOTE'); + $this->sendRequestForJSON('GET', '/apps/federation/trusted-servers'); + $this->theHTTPStatusCodeShouldBe('200'); + $trustedServersIDs = array_map(fn ($server) => $server->id, json_decode($this->response->getBody())->ocs->data); + foreach ($trustedServersIDs as $id) { + $this->sendRequestForJSON('DELETE', '/apps/federation/trusted-servers/' . $id); + $this->theHTTPStatusCodeShouldBe('200'); + } + } + protected function resetAppConfigs() { $this->deleteServerConfig('files_sharing', 'incoming_server2server_group_share_enabled'); $this->deleteServerConfig('files_sharing', 'outgoing_server2server_group_share_enabled'); + $this->deleteServerConfig('files_sharing', 'federated_trusted_share_auto_accept'); } } diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 1b9d598645f..0c437f28a72 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -15,7 +16,7 @@ class FilesDropContext implements Context, SnippetAcceptingContext { /** * @When Dropping file :path with :content */ - public function droppingFileWith($path, $content, $nickName = null) { + public function droppingFileWith($path, $content, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -28,11 +29,11 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$path"); $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest' + 'X-REQUESTED-WITH' => 'XMLHttpRequest', ]; - if ($nickName) { - $options['headers']['X-NC-NICKNAME'] = $nickName; + if ($nickname) { + $options['headers']['X-NC-NICKNAME'] = $nickname; } $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); @@ -43,20 +44,20 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $this->response = $e->getResponse(); } } - - + + /** * @When Dropping file :path with :content as :nickName */ - public function droppingFileWithAs($path, $content, $nickName) { - $this->droppingFileWith($path, $content, $nickName); + public function droppingFileWithAs($path, $content, $nickname) { + $this->droppingFileWith($path, $content, $nickname); } /** * @When Creating folder :folder in drop */ - public function creatingFolderInDrop($folder) { + public function creatingFolderInDrop($folder, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -69,13 +70,25 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$folder"); $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest' + 'X-REQUESTED-WITH' => 'XMLHttpRequest', ]; + if ($nickname) { + $options['headers']['X-NC-NICKNAME'] = $nickname; + } + try { $this->response = $client->request('MKCOL', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $e) { $this->response = $e->getResponse(); } } + + + /** + * @When Creating folder :folder in drop as :nickName + */ + public function creatingFolderInDropWithNickname($folder, $nickname) { + return $this->creatingFolderInDrop($folder, $nickname); + } } diff --git a/build/integration/features/bootstrap/LDAPContext.php b/build/integration/features/bootstrap/LDAPContext.php index f0181b36c71..986dced77a1 100644 --- a/build/integration/features/bootstrap/LDAPContext.php +++ b/build/integration/features/bootstrap/LDAPContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php index d28c24730ba..d48ed6399c5 100644 --- a/build/integration/features/bootstrap/Mail.php +++ b/build/integration/features/bootstrap/Mail.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/MetadataContext.php b/build/integration/features/bootstrap/MetadataContext.php new file mode 100644 index 00000000000..32042590c86 --- /dev/null +++ b/build/integration/features/bootstrap/MetadataContext.php @@ -0,0 +1,124 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Behat\Context\Context; +use Behat\Step\Then; +use Behat\Step\When; +use PHPUnit\Framework\Assert; +use Sabre\DAV\Client as SClient; + +require __DIR__ . '/../../vendor/autoload.php'; + +class MetadataContext implements Context { + private string $davPath = '/remote.php/dav'; + + public function __construct( + private string $baseUrl, + private array $admin, + private string $regular_user_password, + ) { + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + #[When('User :user sets the :metadataKey prop with value :metadataValue on :fileName')] + public function userSetsProp(string $user, string $metadataKey, string $metadataValue, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:set> + <d:prop> + <nc:' . $metadataKey . '>' . $metadataValue . '</nc:' . $metadataKey . '> + </d:prop> + </d:set> +</d:propertyupdate>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body); + } + + #[When('User :user deletes the :metadataKey prop on :fileName')] + public function userDeletesProp(string $user, string $metadataKey, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:remove> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> + </d:remove> +</d:propertyupdate>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body); + } + + #[Then('User :user should see the prop :metadataKey equal to :metadataValue for file :fileName')] + public function checkPropForFile(string $user, string $metadataKey, string $metadataValue, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> +</d:propfind>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body); + $parsedResponse = $client->parseMultistatus($response['body']); + + Assert::assertEquals($parsedResponse[$davUrl]['200']['{http://nextcloud.com/ns}' . $metadataKey], $metadataValue); + } + + #[Then('User :user should not see the prop :metadataKey for file :fileName')] + public function checkPropDoesNotExistsForFile(string $user, string $metadataKey, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> +</d:propfind>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body); + $parsedResponse = $client->parseMultistatus($response['body']); + + Assert::assertEquals($parsedResponse[$davUrl]['404']['{http://nextcloud.com/ns}' . $metadataKey], null); + } + + private function getDavUrl(string $user, string $fileName) { + return $this->davPath . '/files/' . $user . $fileName; + } +} diff --git a/build/integration/features/bootstrap/PrincipalPropertySearchContext.php b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php new file mode 100644 index 00000000000..9dfd9379240 --- /dev/null +++ b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php @@ -0,0 +1,141 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use GuzzleHttp\BodySummarizer; +use GuzzleHttp\Client; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Utils; +use Psr\Http\Message\ResponseInterface; + +class PrincipalPropertySearchContext implements Context { + private string $baseUrl; + private Client $client; + private ResponseInterface $response; + + public function __construct(string $baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function setUpScenario(): void { + $this->client = $this->createGuzzleInstance(); + } + + /** + * Create a Guzzle client with a higher truncateAt value to read full error responses. + */ + private function createGuzzleInstance(): Client { + $bodySummarizer = new BodySummarizer(2048); + + $stack = new HandlerStack(Utils::chooseHandler()); + $stack->push(Middleware::httpErrors($bodySummarizer), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return new Client(['handler' => $stack]); + } + + /** + * @When searching for a principal matching :match + * @param string $match + * @throws \Exception + */ + public function principalPropertySearch(string $match) { + $davUrl = $this->baseUrl . '/remote.php/dav/'; + $user = 'admin'; + $password = 'admin'; + + $this->response = $this->client->request( + 'REPORT', + $davUrl, + [ + 'body' => '<x0:principal-property-search xmlns:x0="DAV:" test="anyof"> + <x0:property-search> + <x0:prop> + <x0:displayname/> + <x2:email-address xmlns:x2="http://sabredav.org/ns"/> + </x0:prop> + <x0:match>' . $match . '</x0:match> + </x0:property-search> + <x0:prop> + <x0:displayname/> + <x1:calendar-user-type xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:calendar-user-address-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x0:principal-URL/> + <x0:alternate-URI-set/> + <x2:email-address xmlns:x2="http://sabredav.org/ns"/> + <x3:language xmlns:x3="http://nextcloud.com/ns"/> + <x1:calendar-home-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-inbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-outbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-default-calendar-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x3:resource-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-make xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-model xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-is-electric xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-range xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-seating-capacity xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-contact-person xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-contact-person-vcard xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-seating-capacity xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-address xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-story xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-room-number xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-features xmlns:x3="http://nextcloud.com/ns"/> + </x0:prop> + <x0:apply-to-principal-collection-set/> +</x0:principal-property-search> +', + 'auth' => [ + $user, + $password, + ], + 'headers' => [ + 'Content-Type' => 'application/xml; charset=UTF-8', + 'Depth' => '0', + ], + ] + ); + } + + /** + * @Then The search HTTP status code should be :code + * @param string $code + * @throws \Exception + */ + public function theHttpStatusCodeShouldBe(string $code): void { + if ((int)$code !== $this->response->getStatusCode()) { + throw new \Exception('Expected ' . (int)$code . ' got ' . $this->response->getStatusCode()); + } + } + + /** + * @Then The search response should contain :needle + * @param string $needle + * @throws \Exception + */ + public function theResponseShouldContain(string $needle): void { + $body = $this->response->getBody()->getContents(); + + if (str_contains($body, $needle) === false) { + throw new \Exception('Response does not contain "' . $needle . '"'); + } + } +} diff --git a/build/integration/features/bootstrap/Provisioning.php b/build/integration/features/bootstrap/Provisioning.php index 9e3e8ccfd8b..935ad2a4a1d 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -1,9 +1,12 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ + +use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Message\ResponseInterface; use PHPUnit\Framework\Assert; @@ -124,7 +127,7 @@ trait Provisioning { * @Then /^user "([^"]*)" has$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ public function userHasSetting($user, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; @@ -145,12 +148,43 @@ trait Provisioning { if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) { $expectedValues = explode(';', $setting[1]); foreach ($expectedValues as $expected) { - Assert::assertTrue(in_array($expected, $value['element'], true)); + Assert::assertTrue(in_array($expected, $value['element'], true), 'Data wrong for field: ' . $setting[0]); } } elseif (isset($value[0])) { - Assert::assertEqualsCanonicalizing($setting[1], $value[0]); + Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]); } else { - Assert::assertEquals('', $setting[1]); + Assert::assertEquals('', $setting[1], 'Data wrong for field: ' . $setting[0]); + } + } + } + + /** + * @Then /^user "([^"]*)" has the following profile data$/ + */ + public function userHasProfileData(string $user, ?TableNode $settings): void { + $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/profile/$user"; + $client = new Client(); + $options = []; + if ($this->currentUser === 'admin') { + $options['auth'] = $this->adminUser; + } else { + $options['auth'] = [$this->currentUser, $this->regularUser]; + } + $options['headers'] = [ + 'OCS-APIREQUEST' => 'true', + 'Accept' => 'application/json', + ]; + + $response = $client->get($fullUrl, $options); + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + $data = $data['ocs']['data']; + foreach ($settings->getRows() as $setting) { + Assert::assertArrayHasKey($setting[0], $data, 'Profile data field missing: ' . $setting[0]); + if ($setting[1] === 'NULL') { + Assert::assertNull($data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]); + } else { + Assert::assertEquals($setting[1], $data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]); } } } @@ -159,7 +193,7 @@ trait Provisioning { * @Then /^group "([^"]*)" has$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ public function groupHasSetting($group, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group"; @@ -191,7 +225,7 @@ trait Provisioning { * @Then /^user "([^"]*)" has editable fields$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $fields + * @param TableNode|null $fields */ public function userHasEditableFields($user, $fields) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields"; @@ -221,9 +255,9 @@ trait Provisioning { * @Then /^search users by phone for region "([^"]*)" with$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ - public function searchUserByPhone($region, \Behat\Gherkin\Node\TableNode $searchTable) { + public function searchUserByPhone($region, TableNode $searchTable) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone"; $client = new Client(); $options = []; @@ -624,10 +658,10 @@ trait Provisioning { /** * @Then /^users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function theUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRows(); $usersSimplified = $this->simplifyArray($users); $respondedArray = $this->getArrayOfUsersResponded($this->response); @@ -637,10 +671,10 @@ trait Provisioning { /** * @Then /^phone matches returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function thePhoneUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRowsHash(); $listCheckedElements = simplexml_load_string($this->response->getBody())->data; $respondedArray = json_decode(json_encode($listCheckedElements), true); @@ -650,10 +684,10 @@ trait Provisioning { /** * @Then /^detailed users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function theDetailedUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRows(); $usersSimplified = $this->simplifyArray($users); $respondedArray = $this->getArrayOfDetailedUsersResponded($this->response); @@ -664,10 +698,10 @@ trait Provisioning { /** * @Then /^groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theGroupsShouldBe($groupsList) { - if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { + if ($groupsList instanceof TableNode) { $groups = $groupsList->getRows(); $groupsSimplified = $this->simplifyArray($groups); $respondedArray = $this->getArrayOfGroupsResponded($this->response); @@ -677,10 +711,10 @@ trait Provisioning { /** * @Then /^subadmin groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theSubadminGroupsShouldBe($groupsList) { - if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { + if ($groupsList instanceof TableNode) { $groups = $groupsList->getRows(); $groupsSimplified = $this->simplifyArray($groups); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); @@ -690,10 +724,10 @@ trait Provisioning { /** * @Then /^apps returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $appList + * @param TableNode|null $appList */ public function theAppsShouldBe($appList) { - if ($appList instanceof \Behat\Gherkin\Node\TableNode) { + if ($appList instanceof TableNode) { $apps = $appList->getRows(); $appsSimplified = $this->simplifyArray($apps); $respondedArray = $this->getArrayOfAppsResponded($this->response); @@ -703,7 +737,7 @@ trait Provisioning { /** * @Then /^subadmin users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theSubadminUsersShouldBe($groupsList) { $this->theSubadminGroupsShouldBe($groupsList); @@ -882,7 +916,7 @@ trait Provisioning { * @param string $quota */ public function userHasAQuotaOf($user, $quota) { - $body = new \Behat\Gherkin\Node\TableNode([ + $body = new TableNode([ 0 => ['key', 'quota'], 1 => ['value', $quota], ]); @@ -950,7 +984,7 @@ trait Provisioning { /** * @Then /^user "([^"]*)" has not$/ */ - public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) { + public function userHasNotSetting($user, TableNode $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; $client = new Client(); $options = []; diff --git a/build/integration/features/bootstrap/RateLimitingContext.php b/build/integration/features/bootstrap/RateLimitingContext.php new file mode 100644 index 00000000000..15c8c5c8379 --- /dev/null +++ b/build/integration/features/bootstrap/RateLimitingContext.php @@ -0,0 +1,31 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; + +class RateLimitingContext implements Context { + use BasicStructure; + use CommandLine; + use Provisioning; + + /** + * @BeforeScenario @RateLimiting + */ + public function enableRateLimiting() { + // Enable rate limiting for the tests. + // Ratelimiting is disabled by default, so we need to enable it + $this->runOcc(['config:system:set', 'ratelimit.protection.enabled', '--value', 'true', '--type', 'bool']); + } + + /** + * @AfterScenario @RateLimiting + */ + public function disableRateLimiting() { + // Restore the default rate limiting configuration. + // Ratelimiting is disabled by default, so we need to disable it + $this->runOcc(['config:system:set', 'ratelimit.protection.enabled', '--value', 'false', '--type', 'bool']); + } +} diff --git a/build/integration/features/bootstrap/RemoteContext.php b/build/integration/features/bootstrap/RemoteContext.php index ae9da4b3614..6102f686ea7 100644 --- a/build/integration/features/bootstrap/RemoteContext.php +++ b/build/integration/features/bootstrap/RemoteContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/RoutingContext.php b/build/integration/features/bootstrap/RoutingContext.php new file mode 100644 index 00000000000..762570547e0 --- /dev/null +++ b/build/integration/features/bootstrap/RoutingContext.php @@ -0,0 +1,19 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; + +require __DIR__ . '/../../vendor/autoload.php'; + +class RoutingContext implements Context, SnippetAcceptingContext { + use Provisioning; + use AppConfiguration; + use CommandLine; + + protected function resetAppConfigs(): void { + } +} diff --git a/build/integration/features/bootstrap/Search.php b/build/integration/features/bootstrap/Search.php index 47259be769c..49a4fe92822 100644 --- a/build/integration/features/bootstrap/Search.php +++ b/build/integration/features/bootstrap/Search.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php index 96cb00d8601..aa131cec597 100644 --- a/build/integration/features/bootstrap/SetupContext.php +++ b/build/integration/features/bootstrap/SetupContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php index e152a749bfa..37e0e63e547 100644 --- a/build/integration/features/bootstrap/ShareesContext.php +++ b/build/integration/features/bootstrap/ShareesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index a58a1b4fda3..0cc490ff110 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -54,8 +55,12 @@ trait Sharing { $fd = $body->getRowsHash(); if (array_key_exists('expireDate', $fd)) { $dateModification = $fd['expireDate']; - if (!empty($dateModification)) { + if ($dateModification === 'null') { + $fd['expireDate'] = null; + } elseif (!empty($dateModification)) { $fd['expireDate'] = date('Y-m-d', strtotime($dateModification)); + } else { + $fd['expireDate'] = ''; } } $options['form_params'] = $fd; @@ -332,6 +337,8 @@ trait Sharing { return $this->isExpectedUrl((string)$data->$field, 'index.php/s/'); } elseif ($contentExpected == $data->$field) { return true; + } else { + print($data->$field); } return false; } @@ -557,18 +564,18 @@ trait Sharing { ]; $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash()); - if (!array_key_exists('uid_file_owner', $expectedFields) && - array_key_exists('uid_owner', $expectedFields)) { + if (!array_key_exists('uid_file_owner', $expectedFields) + && array_key_exists('uid_owner', $expectedFields)) { $expectedFields['uid_file_owner'] = $expectedFields['uid_owner']; } - if (!array_key_exists('displayname_file_owner', $expectedFields) && - array_key_exists('displayname_owner', $expectedFields)) { + if (!array_key_exists('displayname_file_owner', $expectedFields) + && array_key_exists('displayname_owner', $expectedFields)) { $expectedFields['displayname_file_owner'] = $expectedFields['displayname_owner']; } - if (array_key_exists('share_type', $expectedFields) && - $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */ && - array_key_exists('share_with', $expectedFields)) { + if (array_key_exists('share_type', $expectedFields) + && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */ + && array_key_exists('share_with', $expectedFields)) { if ($expectedFields['share_with'] === 'private_conversation') { $expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/'; } else { diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php index 97c2a35ad84..a9dd99108a9 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -17,6 +18,7 @@ class SharingContext implements Context, SnippetAcceptingContext { use Trashbin; use AppConfiguration; use CommandLine; + use Activity; protected function resetAppConfigs() { $this->deleteServerConfig('core', 'shareapi_default_permissions'); @@ -27,6 +29,9 @@ class SharingContext implements Context, SnippetAcceptingContext { $this->deleteServerConfig('core', 'shareapi_default_expire_date'); $this->deleteServerConfig('core', 'shareapi_expire_after_n_days'); $this->deleteServerConfig('core', 'link_defaultExpDays'); + $this->deleteServerConfig('core', 'shareapi_allow_federation_on_public_shares'); + $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); + $this->deleteServerConfig('core', 'shareapi_allow_view_without_download'); $this->runOcc(['config:system:delete', 'share_folder']); } diff --git a/build/integration/features/bootstrap/TagsContext.php b/build/integration/features/bootstrap/TagsContext.php index 262271e3710..c64626de68d 100644 --- a/build/integration/features/bootstrap/TagsContext.php +++ b/build/integration/features/bootstrap/TagsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -255,9 +256,9 @@ class TagsContext implements \Behat\Behat\Context\Context { foreach ($table->getRowsHash() as $rowDisplayName => $row) { foreach ($tags as $key => $tag) { if ( - $tag['display-name'] === $rowDisplayName && - $tag['user-visible'] === $row[0] && - $tag['user-assignable'] === $row[1] + $tag['display-name'] === $rowDisplayName + && $tag['user-visible'] === $row[0] + && $tag['user-assignable'] === $row[1] ) { unset($tags[$key]); } diff --git a/build/integration/features/bootstrap/TalkContext.php b/build/integration/features/bootstrap/TalkContext.php index fe248e1af7c..6f351c30ccf 100644 --- a/build/integration/features/bootstrap/TalkContext.php +++ b/build/integration/features/bootstrap/TalkContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Theming.php b/build/integration/features/bootstrap/Theming.php new file mode 100644 index 00000000000..f44a6533a1b --- /dev/null +++ b/build/integration/features/bootstrap/Theming.php @@ -0,0 +1,49 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +trait Theming { + + private bool $undoAllThemingChangesAfterScenario = false; + + /** + * @AfterScenario + */ + public function undoAllThemingChanges() { + if (!$this->undoAllThemingChangesAfterScenario) { + return; + } + + $this->loggingInUsingWebAs('admin'); + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/undoAllChanges'); + + $this->undoAllThemingChangesAfterScenario = false; + } + + /** + * @When logged in admin uploads theming image for :key from file :source + * + * @param string $key + * @param string $source + */ + public function loggedInAdminUploadsThemingImageForFromFile(string $key, string $source) { + $this->undoAllThemingChangesAfterScenario = true; + + $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/uploadImage?key=' . $key, + [ + 'multipart' => [ + [ + 'name' => 'image', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } +} diff --git a/build/integration/features/bootstrap/Trashbin.php b/build/integration/features/bootstrap/Trashbin.php index 6c8fd5e4fb6..dfcc23289a7 100644 --- a/build/integration/features/bootstrap/Trashbin.php +++ b/build/integration/features/bootstrap/Trashbin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index e71502b6b0c..2cb37002ac0 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -262,7 +263,11 @@ trait WebDav { 'Accept' => 'application/zip' ]; - $this->response = $client->request('GET', $fullUrl, $options); + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } } /** diff --git a/build/integration/features/contacts-menu.feature b/build/integration/features/contacts-menu.feature index f01b34aa1ba..772c0e5405c 100644 --- a/build/integration/features/contacts-menu.feature +++ b/build/integration/features/contacts-menu.feature @@ -71,8 +71,6 @@ Feature: contacts-menu And searched contact "1" is named "Test name" And searched contact "2" is named "user2" - - Scenario: users can not be found by display name if visibility is private Given user "user0" exists And user "user1" exists @@ -80,11 +78,11 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | displayname | Test name | - | displaynameScope | private | + | displaynameScope | v2-private | And Logging in using web as "user2" And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken | displayname | Another test name | - | displaynameScope | contacts | + | displaynameScope | v2-federated | When Logging in using web as "user0" And searching for contacts matching with "test" # Disabled because it regularly fails on drone: @@ -98,11 +96,11 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | email | test@example.com | - | emailScope | private | + | emailScope | v2-private | And Logging in using web as "user2" And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken | email | another_test@example.com | - | emailScope | contacts | + | emailScope | v2-federated | # Disabled because it regularly fails on drone: # When Logging in using web as "user0" # And searching for contacts matching with "test" @@ -116,15 +114,15 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | displayname | Test name | - | displaynameScope | contacts | + | displaynameScope | v2-federated | | email | test@example.com | - | emailScope | private | + | emailScope | v2-private | And Logging in using web as "user2" And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken | displayname | Another test name | - | displaynameScope | private | + | displaynameScope | v2-private | | email | another_test@example.com | - | emailScope | contacts | + | emailScope | v2-federated | When Logging in using web as "user0" And searching for contacts matching with "test" Then the list of searched contacts has "2" contacts @@ -140,9 +138,9 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | displayname | Test name | - | displaynameScope | private | + | displaynameScope | v2-private | And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | displaynameScope | contacts | + | displaynameScope | v2-federated | When Logging in using web as "user0" And searching for contacts matching with "test" Then the list of searched contacts has "1" contacts @@ -154,9 +152,9 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | email | test@example.com | - | emailScope | private | + | emailScope | v2-private | And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | emailScope | contacts | + | emailScope | v2-federated | # Disabled because it regularly fails on drone: # When Logging in using web as "user0" # And searching for contacts matching with "test" @@ -170,7 +168,7 @@ Feature: contacts-menu And user "user1" exists And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | displaynameScope | private | + | displaynameScope | v2-private | And As an "admin" And sending "PUT" to "/cloud/users/user1" with | key | displayname | @@ -185,7 +183,7 @@ Feature: contacts-menu And user "user1" exists And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | emailScope | private | + | emailScope | v2-private | And As an "admin" And sending "PUT" to "/cloud/users/user1" with | key | email | diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 12498bedd7f..8fcfb076497 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -4,6 +4,9 @@ Feature: provisioning Background: Given using api version "1" + Given parameter "whitelist_0" of app "bruteForce" is set to "127.0.0.1" + Given parameter "whitelist_1" of app "bruteForce" is set to "::1" + Given parameter "apply_allowlist_to_ratelimit" of app "bruteforcesettings" is set to "true" Scenario: Getting an not existing user Given As an "admin" @@ -69,7 +72,8 @@ Feature: provisioning | phone | | address | | website | - | twitter | + | twitter | + | bluesky | | fediverse | | organisation | | role | @@ -86,6 +90,7 @@ Feature: provisioning | address | | website | | twitter | + | bluesky | | fediverse | | organisation | | role | @@ -101,6 +106,7 @@ Feature: provisioning | address | | website | | twitter | + | bluesky | | fediverse | | organisation | | role | @@ -155,6 +161,9 @@ Feature: provisioning And sending "PUT" to "/cloud/users/brand-new-user" with | key | twitter | | value | Nextcloud | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | bluesky | + | value | nextcloud.bsky.social | And the OCS status code should be "100" And the HTTP status code should be "200" Then user "brand-new-user" has @@ -165,7 +174,37 @@ Feature: provisioning | phone | +4971125242890 | | address | Foo Bar Town | | website | https://nextcloud.com | - | twitter | Nextcloud | + | twitter | Nextcloud | + | bluesky | nextcloud.bsky.social | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | organisation | + | value | Nextcloud GmbH | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | role | + | value | Engineer | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has the following profile data + | userId | brand-new-user | + | displayname | Brand New User | + | organisation | Nextcloud GmbH | + | role | Engineer | + | address | Foo Bar Town | + | timezone | UTC | + | timezoneOffset | 0 | + | pronouns | NULL | + + Scenario: Edit a user with mixed case emails + Given As an "admin" + And user "brand-new-user" exists + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | email | + | value | mixed-CASE@Nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has + | id | brand-new-user | + | email | mixed-case@nextcloud.com | Scenario: Edit a user account properties scopes Given user "brand-new-user" exists @@ -180,6 +219,11 @@ Feature: provisioning | value | v2-local | Then the OCS status code should be "100" And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | blueskyScope | + | value | v2-local | + Then the OCS status code should be "100" + And the HTTP status code should be "200" When sending "PUT" to "/cloud/users/brand-new-user" with | key | addressScope | | value | v2-federated | @@ -190,21 +234,6 @@ Feature: provisioning | value | v2-published | Then the OCS status code should be "100" And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | websiteScope | - | value | public | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | displaynameScope | - | value | contacts | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | avatarScope | - | value | private | - Then the OCS status code should be "100" - And the HTTP status code should be "200" And sending "PUT" to "/cloud/users/brand-new-user" with | key | email | | value | no-reply@nextcloud.com | @@ -230,12 +259,10 @@ Feature: provisioning Then user "brand-new-user" has | id | brand-new-user | | phoneScope | v2-private | - | twitterScope | v2-local | + | twitterScope | v2-local | + | blueskyScope | v2-local | | addressScope | v2-federated | | emailScope | v2-published | - | websiteScope | v2-published | - | displaynameScope | v2-federated | - | avatarScope | v2-local | Scenario: Edit a user account multivalue property scopes Given user "brand-new-user" exists @@ -450,6 +477,7 @@ Feature: provisioning Then groups returned are | EspaƱa | | admin | + | hidden_group | | new-group | Scenario: create a subadmin @@ -604,6 +632,7 @@ Feature: provisioning | settings | | sharebymail | | systemtags | + | testing | | theming | | twofactor_backupcodes | | updatenotification | @@ -629,6 +658,7 @@ Feature: provisioning And the HTTP status code should be "200" Scenario: enable an app + Given invoking occ with "app:disable testing" Given As an "admin" And app "testing" is disabled When sending "POST" to "/cloud/apps/testing" @@ -643,12 +673,14 @@ Feature: provisioning And the HTTP status code should be "200" Scenario: disable an app + Given invoking occ with "app:enable testing" Given As an "admin" And app "testing" is enabled When sending "DELETE" to "/cloud/apps/testing" Then the OCS status code should be "100" And the HTTP status code should be "200" And app "testing" is disabled + Given invoking occ with "app:enable testing" Scenario: disable an user Given As an "admin" diff --git a/build/integration/features/ratelimiting.feature b/build/integration/features/ratelimiting.feature deleted file mode 100644 index a2fca2fc6be..00000000000 --- a/build/integration/features/ratelimiting.feature +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: ratelimiting - - Background: - Given user "user0" exists - Given As an "admin" - Given invoking occ with "app:enable --force testing" - - Scenario: Accessing a page with only an AnonRateThrottle as user - Given user "user0" exists - # First request should work - When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - # Second one should fail - When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth - Then the HTTP status code should be "429" - # After 11 seconds the next request should work - And Sleep for "11" seconds - When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - - Scenario: Accessing a page with only an AnonRateThrottle as guest - Given Sleep for "11" seconds - # First request should work - When requesting "/index.php/apps/testing/anonProtected" with "GET" - Then the HTTP status code should be "200" - # Second one should fail - When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth - Then the HTTP status code should be "429" - # After 11 seconds the next request should work - And Sleep for "11" seconds - When requesting "/index.php/apps/testing/anonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - - Scenario: Accessing a page with UserRateThrottle and AnonRateThrottle - # First request should work as guest - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" - Then the HTTP status code should be "200" - # Second request should fail as guest - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" - Then the HTTP status code should be "429" - # First request should work as user - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - # Second request should work as user - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - # Third request should work as user - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - # Fourth request should work as user - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - # Fifth request should work as user - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" using basic auth - Then the HTTP status code should be "200" - # Sixth request should fail as user - When requesting "/index.php/apps/testing/userAndAnonProtected" with "GET" - Then the HTTP status code should be "429" |