diff options
Diffstat (limited to 'build/integration/features')
44 files changed, 1946 insertions, 1089 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/Auth.php b/build/integration/features/bootstrap/Auth.php index e620af4d530..aeaade85383 100644 --- a/build/integration/features/bootstrap/Auth.php +++ b/build/integration/features/bootstrap/Auth.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -203,7 +204,8 @@ trait Auth { * @param bool $remember */ public function aNewBrowserSessionIsStarted($remember = false) { - $loginUrl = substr($this->baseUrl, 0, -5) . '/login'; + $baseUrl = substr($this->baseUrl, 0, -5); + $loginUrl = $baseUrl . '/login'; // Request a new session and extract CSRF token $client = new Client(); $response = $client->get($loginUrl, [ @@ -222,6 +224,9 @@ trait Auth { 'requesttoken' => $this->requestToken, ], 'cookies' => $this->cookieJar, + 'headers' => [ + 'Origin' => $baseUrl, + ], ] ); $this->extracRequestTokenFromResponse($response); diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php index f05d0372687..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 @@ -9,7 +10,7 @@ use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; trait Avatar { - /** @var string **/ + /** @var string * */ private $lastAvatar; /** @AfterScenario **/ @@ -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 6a1cc7e1ada..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(); @@ -279,7 +288,8 @@ trait BasicStructure { * @param string $user */ public function loggingInUsingWebAs($user) { - $loginUrl = substr($this->baseUrl, 0, -5) . '/index.php/login'; + $baseUrl = substr($this->baseUrl, 0, -5); + $loginUrl = $baseUrl . '/index.php/login'; // Request a new session and extract CSRF token $client = new Client(); $response = $client->get( @@ -302,6 +312,9 @@ trait BasicStructure { 'requesttoken' => $this->requestToken, ], 'cookies' => $this->cookieJar, + 'headers' => [ + 'Origin' => $baseUrl, + ], ] ); $this->extracRequestTokenFromResponse($response); @@ -327,7 +340,7 @@ trait BasicStructure { $fd = $body->getRowsHash(); $options['form_params'] = $fd; } elseif ($body) { - $options = array_merge($options, $body); + $options = array_merge_recursive($options, $body); } $client = new Client(); @@ -415,14 +428,14 @@ trait BasicStructure { } public function createFileSpecificSize($name, $size) { - $file = fopen("work/" . "$name", 'w'); + $file = fopen('work/' . "$name", 'w'); fseek($file, $size - 1, SEEK_CUR); fwrite($file, 'a'); // write a dummy char at SIZE position fclose($file); } public function createFileWithText($name, $text) { - $file = fopen("work/" . "$name", 'w'); + $file = fopen('work/' . "$name", 'w'); fwrite($file, $text); fclose($file); } @@ -458,19 +471,19 @@ trait BasicStructure { */ public static function addFilesToSkeleton() { for ($i = 0; $i < 5; $i++) { - file_put_contents("../../core/skeleton/" . "textfile" . "$i" . ".txt", "Nextcloud test text file\n"); + file_put_contents('../../core/skeleton/' . 'textfile' . "$i" . '.txt', "Nextcloud test text file\n"); } - if (!file_exists("../../core/skeleton/FOLDER")) { - mkdir("../../core/skeleton/FOLDER", 0777, true); + if (!file_exists('../../core/skeleton/FOLDER')) { + mkdir('../../core/skeleton/FOLDER', 0777, true); } - if (!file_exists("../../core/skeleton/PARENT")) { - mkdir("../../core/skeleton/PARENT", 0777, true); + if (!file_exists('../../core/skeleton/PARENT')) { + mkdir('../../core/skeleton/PARENT', 0777, true); } - file_put_contents("../../core/skeleton/PARENT/" . "parent.txt", "Nextcloud test text file\n"); - if (!file_exists("../../core/skeleton/PARENT/CHILD")) { - mkdir("../../core/skeleton/PARENT/CHILD", 0777, true); + file_put_contents('../../core/skeleton/PARENT/' . 'parent.txt', "Nextcloud test text file\n"); + if (!file_exists('../../core/skeleton/PARENT/CHILD')) { + mkdir('../../core/skeleton/PARENT/CHILD', 0777, true); } - file_put_contents("../../core/skeleton/PARENT/CHILD/" . "child.txt", "Nextcloud test text file\n"); + file_put_contents('../../core/skeleton/PARENT/CHILD/' . 'child.txt', "Nextcloud test text file\n"); } /** @@ -478,18 +491,18 @@ trait BasicStructure { */ public static function removeFilesFromSkeleton() { for ($i = 0; $i < 5; $i++) { - self::removeFile("../../core/skeleton/", "textfile" . "$i" . ".txt"); + self::removeFile('../../core/skeleton/', 'textfile' . "$i" . '.txt'); } - if (is_dir("../../core/skeleton/FOLDER")) { - rmdir("../../core/skeleton/FOLDER"); + if (is_dir('../../core/skeleton/FOLDER')) { + rmdir('../../core/skeleton/FOLDER'); } - self::removeFile("../../core/skeleton/PARENT/CHILD/", "child.txt"); - if (is_dir("../../core/skeleton/PARENT/CHILD")) { - rmdir("../../core/skeleton/PARENT/CHILD"); + self::removeFile('../../core/skeleton/PARENT/CHILD/', 'child.txt'); + if (is_dir('../../core/skeleton/PARENT/CHILD')) { + rmdir('../../core/skeleton/PARENT/CHILD'); } - self::removeFile("../../core/skeleton/PARENT/", "parent.txt"); - if (is_dir("../../core/skeleton/PARENT")) { - rmdir("../../core/skeleton/PARENT"); + self::removeFile('../../core/skeleton/PARENT/', 'parent.txt'); + if (is_dir('../../core/skeleton/PARENT')) { + rmdir('../../core/skeleton/PARENT'); } } @@ -497,7 +510,7 @@ trait BasicStructure { * @BeforeScenario @local_storage */ public static function removeFilesFromLocalStorageBefore() { - $dir = "./work/local_storage/"; + $dir = './work/local_storage/'; $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); foreach ($ri as $file) { @@ -509,7 +522,7 @@ trait BasicStructure { * @AfterScenario @local_storage */ public static function removeFilesFromLocalStorageAfter() { - $dir = "./work/local_storage/"; + $dir = './work/local_storage/'; $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); foreach ($ri as $file) { diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php index a9db5c89d4d..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. @@ -11,7 +12,7 @@ use GuzzleHttp\Exception\GuzzleException; use Psr\Http\Message\ResponseInterface; class CalDavContext implements \Behat\Behat\Context\Context { - /** @var string */ + /** @var string */ private $baseUrl; /** @var Client */ private $client; @@ -41,7 +42,7 @@ class CalDavContext implements \Behat\Behat\Context\Context { /** @AfterScenario */ public function afterScenario() { - $davUrl = $this->baseUrl. '/remote.php/dav/calendars/admin/MyCalendar'; + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/admin/MyCalendar'; try { $this->client->delete( $davUrl, @@ -266,7 +267,7 @@ class CalDavContext implements \Behat\Behat\Context\Context { * @param string $name */ public function createsACalendarNamed($user, $name) { - $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$user.'/'.$name; + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( @@ -289,7 +290,7 @@ class CalDavContext implements \Behat\Behat\Context\Context { * @param string $name */ public function publiclySharesTheCalendarNamed($user, $name) { - $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$user.'/'.$name; + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php index 79ede6ac8ba..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. @@ -22,7 +23,9 @@ class CapabilitiesContext implements Context, SnippetAcceptingContext { * @param \Behat\Gherkin\Node\TableNode|null $formData */ public function checkCapabilitiesResponse(\Behat\Gherkin\Node\TableNode $formData) { - $capabilitiesXML = simplexml_load_string($this->response->getBody())->data->capabilities; + $capabilitiesXML = simplexml_load_string($this->response->getBody()); + Assert::assertNotFalse($capabilitiesXML, 'Failed to fetch capabilities'); + $capabilitiesXML = $capabilitiesXML->data->capabilities; foreach ($formData->getHash() as $row) { $path_to_element = explode('@@@', $row['path_to_element']); @@ -32,9 +35,9 @@ class CapabilitiesContext implements Context, SnippetAcceptingContext { } $answeredValue = (string)$answeredValue; Assert::assertEquals( - $row['value'] === "EMPTY" ? '' : $row['value'], + $row['value'] === 'EMPTY' ? '' : $row['value'], $answeredValue, - "Failed field " . $row['capability'] . " " . $row['path_to_element'] + 'Failed field ' . $row['capability'] . ' ' . $row['path_to_element'] ); } } diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php index d5694ec619b..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. @@ -11,7 +12,7 @@ use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Message\ResponseInterface; class CardDavContext implements \Behat\Behat\Context\Context { - /** @var string */ + /** @var string */ private $baseUrl; /** @var Client */ private $client; @@ -110,7 +111,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @throws \Exception */ public function createsAnAddressbookNamedWithStatuscode($user, $addressBook, $statusCode) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( @@ -123,7 +124,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { <d:prop> <d:resourcetype> <d:collection />,<card:addressbook /> - </d:resourcetype>,<d:displayname>'.$addressBook.'</d:displayname> + </d:resourcetype>,<d:displayname>' . $addressBook . '</d:displayname> </d:prop> </d:set> </d:mkcol>', @@ -190,7 +191,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @Given :user uploads the contact :fileName to the addressbook :addressbook */ public function uploadsTheContactToTheAddressbook($user, $fileName, $addressBook) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook . '/' . $fileName; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook . '/' . $fileName; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( @@ -223,7 +224,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @When Exporting the picture of contact :fileName from addressbook :addressBook as user :user */ public function whenExportingThePictureOfContactFromAddressbookAsUser($fileName, $addressBook, $user) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook . '/' . $fileName . '?photo=true'; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook . '/' . $fileName . '?photo=true'; $password = ($user === 'admin') ? 'admin' : '123456'; try { @@ -249,7 +250,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @When Downloading the contact :fileName from addressbook :addressBook as user :user */ public function whenDownloadingTheContactFromAddressbookAsUser($fileName, $addressBook, $user) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook . '/' . $fileName; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook . '/' . $fileName; $password = ($user === 'admin') ? 'admin' : '123456'; try { diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php index 5661015e714..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. @@ -10,7 +11,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Message\ResponseInterface; class ChecksumsContext implements \Behat\Behat\Context\Context { - /** @var string */ + /** @var string */ private $baseUrl; /** @var Client */ private $client; @@ -87,7 +88,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { */ public function theWebdavResponseShouldHaveAStatusCode($statusCode) { if ((int)$statusCode !== $this->response->getStatusCode()) { - throw new \Exception("Expected $statusCode, got ".$this->response->getStatusCode()); + throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode()); } } @@ -131,7 +132,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { $checksums = $parsed[0]['value'][1]['value'][0]['value'][0]; if ($checksums['value'][0]['value'] !== $checksum) { - throw new \Exception("Expected $checksum, got ".$checksums['value'][0]['value']); + throw new \Exception("Expected $checksum, got " . $checksums['value'][0]['value']); } } @@ -159,7 +160,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { */ public function theHeaderChecksumShouldMatch($checksum) { if ($this->response->getHeader('OC-Checksum')[0] !== $checksum) { - throw new \Exception("Expected $checksum, got ".$this->response->getHeader('OC-Checksum')[0]); + throw new \Exception("Expected $checksum, got " . $this->response->getHeader('OC-Checksum')[0]); } } @@ -199,7 +200,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { $status = $parsed[0]['value'][1]['value'][1]['value']; if ($status !== 'HTTP/1.1 404 Not Found') { - throw new \Exception("Expected 'HTTP/1.1 404 Not Found', got ".$status); + throw new \Exception("Expected 'HTTP/1.1 404 Not Found', got " . $status); } } @@ -208,34 +209,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { */ public function theOcChecksumHeaderShouldNotBeThere() { if ($this->response->hasHeader('OC-Checksum')) { - throw new \Exception("Expected no checksum header but got ".$this->response->getHeader('OC-Checksum')[0]); + throw new \Exception('Expected no checksum header but got ' . $this->response->getHeader('OC-Checksum')[0]); } } - - /** - * @Given user :user uploads chunk file :num of :total with :data to :destination with checksum :checksum - * @param string $user - * @param int $num - * @param int $total - * @param string $data - * @param string $destination - * @param string $checksum - */ - public function userUploadsChunkFileOfWithToWithChecksum($user, $num, $total, $data, $destination, $checksum) { - $num -= 1; - $this->response = $this->client->put( - $this->baseUrl . '/remote.php/webdav' . $destination . '-chunking-42-'.$total.'-'.$num, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user) - ], - 'body' => $data, - 'headers' => [ - 'OC-Checksum' => $checksum, - 'OC-Chunked' => '1', - ] - ] - ); - } } diff --git a/build/integration/features/bootstrap/CollaborationContext.php b/build/integration/features/bootstrap/CollaborationContext.php index 854abe98b2f..27fa1795c5d 100644 --- a/build/integration/features/bootstrap/CollaborationContext.php +++ b/build/integration/features/bootstrap/CollaborationContext.php @@ -71,7 +71,7 @@ class CollaborationContext implements Context { try { $destination = '/users/admin/myaddressbook'; $data = '<x0:mkcol xmlns:x0="DAV:"><x0:set><x0:prop><x0:resourcetype><x0:collection/><x4:addressbook xmlns:x4="urn:ietf:params:xml:ns:carddav"/></x0:resourcetype><x0:displayname>myaddressbook</x0:displayname></x0:prop></x0:set></x0:mkcol>'; - $this->response = $this->makeDavRequest($this->currentUser, "MKCOL", $destination, ['Content-Type' => 'application/xml'], $data, "addressbooks"); + $this->response = $this->makeDavRequest($this->currentUser, 'MKCOL', $destination, ['Content-Type' => 'application/xml'], $data, 'addressbooks'); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -93,7 +93,7 @@ EMAIL;TYPE=HOME:user@example.com REV;VALUE=DATE-AND-OR-TIME:20211130T140111Z END:VCARD EOF; - $this->response = $this->makeDavRequest($this->currentUser, "PUT", $destination, [], $data, "addressbooks"); + $this->response = $this->makeDavRequest($this->currentUser, 'PUT', $destination, [], $data, 'addressbooks'); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); 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 47a85885ce4..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. @@ -6,6 +7,7 @@ */ require __DIR__ . '/../../vendor/autoload.php'; +use Behat\Behat\Context\Exception\ContextNotFoundException; use Behat\Behat\Hook\Scope\BeforeScenarioScope; use PHPUnit\Framework\Assert; @@ -41,8 +43,12 @@ class CommandLineContext implements \Behat\Behat\Context\Context { /** @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); - // this should really be "WebDavContext" ... - $this->featureContext = $environment->getContext('FeatureContext'); + // this should really be "WebDavContext" + try { + $this->featureContext = $environment->getContext('FeatureContext'); + } catch (ContextNotFoundException) { + $this->featureContext = $environment->getContext('DavFeatureContext'); + } } private function findLastTransferFolderForUser($sourceUser, $targetUser) { @@ -104,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 8d7b0fe0c2f..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. @@ -29,8 +30,6 @@ class CommentsContext implements \Behat\Behat\Context\Context { } } - - /** * get a named entry from response instead of picking a random entry from values * @@ -138,7 +137,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } } @@ -180,13 +179,13 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } if ($res->getStatusCode() === 207) { $service = new Sabre\Xml\Service(); $this->response = $service->parse($res->getBody()->getContents()); - $this->commentId = (int) ($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0); + $this->commentId = (int)($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0); } } @@ -238,7 +237,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } } @@ -275,7 +274,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { $count = count($this->response); } if ($count !== (int)$number) { - throw new \Exception("Found more comments than $number (" . $count . ")"); + throw new \Exception("Found more comments than $number (" . $count . ')'); } } @@ -305,7 +304,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } } } 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 new file mode 100644 index 00000000000..ec6085cff98 --- /dev/null +++ b/build/integration/features/bootstrap/DavFeatureContext.php @@ -0,0 +1,24 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 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 DavFeatureContext implements Context, SnippetAcceptingContext { + use AppConfiguration; + use ContactsMenu; + use ExternalStorage; + use Search; + use WebDav; + use Trashbin; + + protected function resetAppConfigs() { + $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); + } +} diff --git a/build/integration/features/bootstrap/Download.php b/build/integration/features/bootstrap/Download.php index aa10830427a..549a033346e 100644 --- a/build/integration/features/bootstrap/Download.php +++ b/build/integration/features/bootstrap/Download.php @@ -1,14 +1,16 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ use PHPUnit\Framework\Assert; +use Psr\Http\Message\StreamInterface; require __DIR__ . '/../../vendor/autoload.php'; trait Download { - /** @var string **/ + /** @var string * */ private $downloadedFile; /** @AfterScenario **/ @@ -20,16 +22,16 @@ trait Download { * @When user :user downloads zip file for entries :entries in folder :folder */ public function userDownloadsZipFileForEntriesInFolder($user, $entries, $folder) { + $folder = trim($folder, '/'); $this->asAn($user); - $this->sendingToDirectUrl('GET', "/index.php/apps/files/ajax/download.php?dir=" . $folder . "&files=[" . $entries . "]"); + $this->sendingToDirectUrl('GET', "/remote.php/dav/files/$user/$folder?accept=zip&files=[" . $entries . ']'); $this->theHTTPStatusCodeShouldBe('200'); - - $this->getDownloadedFile(); } private function getDownloadedFile() { $this->downloadedFile = ''; + /** @var StreamInterface */ $body = $this->response->getBody(); while (!$body->eof()) { $this->downloadedFile .= $body->read(8192); @@ -38,14 +40,28 @@ trait Download { } /** + * @Then the downloaded file is a zip file + */ + public function theDownloadedFileIsAZipFile() { + $this->getDownloadedFile(); + + Assert::assertTrue( + strpos($this->downloadedFile, "\x50\x4B\x01\x02") !== false, + 'File does not contain the central directory file header' + ); + } + + /** * @Then the downloaded zip file is a zip32 file */ public function theDownloadedZipFileIsAZip32File() { + $this->theDownloadedFileIsAZipFile(); + // assertNotContains is not used to prevent the whole file from being // printed in case of error. Assert::assertTrue( strpos($this->downloadedFile, "\x50\x4B\x06\x06") === false, - "File contains the zip64 end of central dir signature" + 'File contains the zip64 end of central dir signature' ); } @@ -53,11 +69,13 @@ trait Download { * @Then the downloaded zip file is a zip64 file */ public function theDownloadedZipFileIsAZip64File() { + $this->theDownloadedFileIsAZipFile(); + // assertNotContains is not used to prevent the whole file from being // printed in case of error. Assert::assertTrue( strpos($this->downloadedFile, "\x50\x4B\x06\x06") !== false, - "File does not contain the zip64 end of central dir signature" + 'File does not contain the zip64 end of central dir signature' ); } @@ -77,7 +95,7 @@ trait Download { // in case of error and to be able to get the extra field length. Assert::assertEquals( 1, preg_match($fileHeaderRegExp, $this->downloadedFile, $matches), - "Local header for file did not appear once in zip file" + 'Local header for file did not appear once in zip file' ); $extraFieldLength = unpack('vextraFieldLength', $matches[1])['extraFieldLength']; @@ -97,7 +115,7 @@ trait Download { // in case of error. Assert::assertEquals( 1, preg_match($fileHeaderAndContentRegExp, $this->downloadedFile), - "Local header and contents for file did not appear once in zip file" + 'Local header and contents for file did not appear once in zip file' ); } @@ -117,7 +135,21 @@ trait Download { // in case of error. Assert::assertEquals( 1, preg_match($folderHeaderRegExp, $this->downloadedFile), - "Local header for folder did not appear once in zip file" + 'Local header for folder did not appear once in zip file' + ); + } + + /** + * @Then the downloaded file has the content of :sourceFilename from :user data + */ + public function theDownloadedFileHasContentOfUserFile($sourceFilename, $user) { + $this->getDownloadedFile(); + $expectedFileContents = file_get_contents($this->getDataDirectory() . "/$user/files" . $sourceFilename); + + // prevent the whole file from being printed in case of error. + Assert::assertEquals( + 0, strcmp($expectedFileContents, $this->downloadedFile), + 'Downloaded file content does not match local file content' ); } } diff --git a/build/integration/features/bootstrap/ExternalStorage.php b/build/integration/features/bootstrap/ExternalStorage.php new file mode 100644 index 00000000000..8fe2653a026 --- /dev/null +++ b/build/integration/features/bootstrap/ExternalStorage.php @@ -0,0 +1,123 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +require __DIR__ . '/../../vendor/autoload.php'; + +trait ExternalStorage { + private array $storageIds = []; + + private array $lastExternalStorageData; + + /** + * @AfterScenario + **/ + public function deleteCreatedStorages(): void { + foreach ($this->storageIds as $storageId) { + $this->deleteStorage($storageId); + } + $this->storageIds = []; + } + + private function deleteStorage(string $storageId): void { + // Based on "runOcc" from CommandLine trait + $args = ['files_external:delete', '--yes', $storageId]; + $args = array_map(function ($arg) { + return escapeshellarg($arg); + }, $args); + $args[] = '--no-ansi --no-warnings'; + $args = implode(' ', $args); + + $descriptor = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open('php console.php ' . $args, $descriptor, $pipes, $ocPath = '../..'); + $lastStdOut = stream_get_contents($pipes[1]); + proc_close($process); + } + + /** + * @When logged in user creates external global storage + * + * @param TableNode $fields + */ + public function loggedInUserCreatesExternalGlobalStorage(TableNode $fields): void { + $this->sendJsonWithRequestTokenAndBasicAuth('POST', '/index.php/apps/files_external/globalstorages', $fields); + $this->theHTTPStatusCodeShouldBe('201'); + + $this->lastExternalStorageData = json_decode($this->response->getBody(), $asAssociativeArray = true); + + $this->storageIds[] = $this->lastExternalStorageData['id']; + } + + /** + * @When logged in user updates last external userglobal storage + * + * @param TableNode $fields + */ + public function loggedInUserUpdatesLastExternalUserglobalStorage(TableNode $fields): void { + $this->sendJsonWithRequestTokenAndBasicAuth('PUT', '/index.php/apps/files_external/userglobalstorages/' . $this->lastExternalStorageData['id'], $fields); + $this->theHTTPStatusCodeShouldBe('200'); + + $this->lastExternalStorageData = json_decode($this->response->getBody(), $asAssociativeArray = true); + } + + /** + * @Then fields of last external storage match with + * + * @param TableNode $fields + */ + public function fieldsOfLastExternalStorageMatchWith(TableNode $fields): void { + foreach ($fields->getRowsHash() as $expectedField => $expectedValue) { + if (!array_key_exists($expectedField, $this->lastExternalStorageData)) { + Assert::fail("$expectedField was not found in response"); + } + + Assert::assertEquals($expectedValue, $this->lastExternalStorageData[$expectedField], "Field '$expectedField' does not match ({$this->lastExternalStorageData[$expectedField]})"); + } + } + + private function sendJsonWithRequestToken(string $method, string $url, TableNode $fields): void { + $isFirstField = true; + $fieldsAsJsonString = '{'; + foreach ($fields->getRowsHash() as $key => $value) { + $fieldsAsJsonString .= ($isFirstField ? '' : ',') . '"' . $key . '":' . $value; + $isFirstField = false; + } + $fieldsAsJsonString .= '}'; + + $body = [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => $fieldsAsJsonString, + ]; + $this->sendingAToWithRequesttoken($method, $url, $body); + } + + private function sendJsonWithRequestTokenAndBasicAuth(string $method, string $url, TableNode $fields): void { + $isFirstField = true; + $fieldsAsJsonString = '{'; + foreach ($fields->getRowsHash() as $key => $value) { + $fieldsAsJsonString .= ($isFirstField ? '' : ',') . '"' . $key . '":' . $value; + $isFirstField = false; + } + $fieldsAsJsonString .= '}'; + + $body = [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Basic ' . base64_encode('admin:admin'), + ], + 'body' => $fieldsAsJsonString, + ]; + $this->sendingAToWithRequesttoken($method, $url, $body); + } +} diff --git a/build/integration/features/bootstrap/FakeSMTPHelper.php b/build/integration/features/bootstrap/FakeSMTPHelper.php index 2d90494b82c..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 @@ -34,7 +35,7 @@ class fakeSMTP { $hasValidTo = false; $receivingData = false; $header = true; - $this->reply('220 '.$this->serverHello); + $this->reply('220 ' . $this->serverHello); $this->mail['ipaddress'] = $this->detectIP(); while ($data = fgets($this->fd)) { $data = preg_replace('@\r\n@', "\n", $data); @@ -60,7 +61,7 @@ class fakeSMTP { $this->reply('250 2.1.5 Ok'); $hasValidTo = true; } else { - $this->reply('501 5.1.3 Bad recipient address syntax '.$match[1]); + $this->reply('501 5.1.3 Bad recipient address syntax ' . $match[1]); } } } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) { @@ -70,7 +71,7 @@ class fakeSMTP { } elseif (!$receivingData && preg_match('/^NOOP$/i', trim($data))) { $this->reply('250 2.0.0 Ok'); } elseif (!$receivingData && preg_match('/^VRFY (.*)/i', trim($data), $match)) { - $this->reply('250 2.0.0 '.$match[1]); + $this->reply('250 2.0.0 ' . $match[1]); } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) { if (!$hasValidTo) { $this->reply('503 5.5.1 Error: need RCPT command'); @@ -79,7 +80,7 @@ class fakeSMTP { $receivingData = true; } } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) { - $this->reply('250 HELO '.$this->mail['ipaddress']); + $this->reply('250 HELO ' . $this->mail['ipaddress']); } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) { break; } elseif (!$receivingData) { @@ -88,7 +89,7 @@ class fakeSMTP { } elseif ($receivingData && $data == ".\n") { /* Email Received, now let's look at it */ $receivingData = false; - $this->reply('250 2.0.0 Ok: queued as '.$this->generateRandom(10)); + $this->reply('250 2.0.0 Ok: queued as ' . $this->generateRandom(10)); $splitmail = explode("\n\n", $this->mail['rawEmail'], 2); if (count($splitmail) == 2) { $this->mail['emailHeaders'] = $splitmail[0]; @@ -109,14 +110,14 @@ class fakeSMTP { } } /* Say good bye */ - $this->reply('221 2.0.0 Bye '.$this->mail['ipaddress']); + $this->reply('221 2.0.0 Bye ' . $this->mail['ipaddress']); fclose($this->fd); } public function log($s) { if ($this->logFile) { - file_put_contents($this->logFile, trim($s)."\n", FILE_APPEND); + file_put_contents($this->logFile, trim($s) . "\n", FILE_APPEND); } } diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php index 638772da0b7..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. @@ -9,13 +10,20 @@ use Behat\Behat\Context\SnippetAcceptingContext; 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 03a03ef2fd6..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'; @@ -38,7 +40,7 @@ class FederationContext implements Context, SnippetAcceptingContext { $port = getenv('PORT_FED'); - self::$phpFederatedServerPid = exec('php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); + self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); } /** @@ -64,7 +66,7 @@ class FederationContext implements Context, SnippetAcceptingContext { * @param string $shareeServer "LOCAL" or "REMOTE" */ public function federateSharing($sharerUser, $sharerServer, $sharerPath, $shareeUser, $shareeServer) { - if ($shareeServer == "REMOTE") { + if ($shareeServer == 'REMOTE') { $shareWith = "$shareeUser@" . substr($this->remoteBaseUrl, 0, -4); } else { $shareWith = "$shareeUser@" . substr($this->localBaseUrl, 0, -4); @@ -85,7 +87,7 @@ class FederationContext implements Context, SnippetAcceptingContext { * @param string $shareeServer "LOCAL" or "REMOTE" */ public function federateGroupSharing($sharerUser, $sharerServer, $sharerPath, $shareeGroup, $shareeServer) { - if ($shareeServer == "REMOTE") { + if ($shareeServer == 'REMOTE') { $shareWith = "$shareeGroup@" . substr($this->remoteBaseUrl, 0, -4); } else { $shareWith = "$shareeGroup@" . substr($this->localBaseUrl, 0, -4); @@ -134,7 +136,7 @@ class FederationContext implements Context, SnippetAcceptingContext { public function acceptLastPendingShare($user, $server) { $previous = $this->usingServer($server); $this->asAn($user); - $this->sendingToWith('GET', "/apps/files_sharing/api/v1/remote_shares/pending", null); + $this->sendingToWith('GET', '/apps/files_sharing/api/v1/remote_shares/pending', null); $this->theHTTPStatusCodeShouldBe('200'); $this->theOCSStatusCodeShouldBe('100'); $share_id = simplexml_load_string($this->response->getBody())->data[0]->element[0]->id; @@ -152,7 +154,7 @@ class FederationContext implements Context, SnippetAcceptingContext { */ public function deleteLastAcceptedRemoteShare($user) { $this->asAn($user); - $this->sendingToWith('DELETE', "/apps/files_sharing/api/v1/remote_shares/" . $this->lastAcceptedRemoteShareId, null); + $this->sendingToWith('DELETE', '/apps/files_sharing/api/v1/remote_shares/' . $this->lastAcceptedRemoteShareId, null); } /** @@ -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 b611e55ea0f..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) { + public function droppingFileWith($path, $content, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -25,11 +26,16 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . "/public.php/dav/files/$token/$path"; + $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; + } + $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); try { @@ -39,10 +45,19 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } } + + /** + * @When Dropping file :path with :content as :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) { @@ -52,16 +67,28 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . "/public.php/dav/files/$token/$folder"; + $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 5e5a75fdc26..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 @@ -20,7 +21,7 @@ trait Mail { return; } - exec("kill " . $this->fakeSmtpServerPid); + exec('kill ' . $this->fakeSmtpServerPid); $this->invokingTheCommand('config:system:delete mail_smtpport'); } @@ -33,6 +34,6 @@ trait Mail { // FakeSMTP uses 2525 instead. $this->invokingTheCommand('config:system:set mail_smtpport --value=2525 --type integer'); - $this->fakeSmtpServerPid = exec("php features/bootstrap/FakeSMTPHelper.php >/dev/null 2>&1 & echo $!"); + $this->fakeSmtpServerPid = exec('php features/bootstrap/FakeSMTPHelper.php >/dev/null 2>&1 & echo $!'); } } 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 2fb1c807cc5..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; @@ -34,7 +37,7 @@ trait Provisioning { $this->userExists($user); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheUser($user); $this->currentUser = $previous_user; } @@ -51,7 +54,7 @@ trait Provisioning { $this->userExists($user); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheUser($user, $displayname); $this->currentUser = $previous_user; } @@ -72,7 +75,7 @@ trait Provisioning { return; } $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheUser($user); $this->currentUser = $previous_user; try { @@ -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 = []; @@ -250,7 +284,7 @@ trait Provisioning { public function createUser($user) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheUser($user); $this->userExists($user); $this->currentUser = $previous_user; @@ -258,7 +292,7 @@ trait Provisioning { public function deleteUser($user) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheUser($user); $this->userDoesNotExist($user); $this->currentUser = $previous_user; @@ -266,7 +300,7 @@ trait Provisioning { public function createGroup($group) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheGroup($group); $this->groupExists($group); $this->currentUser = $previous_user; @@ -274,7 +308,7 @@ trait Provisioning { public function deleteGroup($group) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheGroup($group); $this->groupDoesNotExist($group); $this->currentUser = $previous_user; @@ -343,7 +377,7 @@ trait Provisioning { */ public function assureUserBelongsToGroup($user, $group) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; if (!$this->userBelongsToGroup($user, $group)) { $this->addingUserToGroup($user, $group); @@ -522,7 +556,7 @@ trait Provisioning { $this->groupExists($group); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheGroup($group); $this->currentUser = $previous_user; } @@ -543,7 +577,7 @@ trait Provisioning { return; } $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheGroup($group); $this->currentUser = $previous_user; try { @@ -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); @@ -775,7 +809,7 @@ trait Provisioning { * @param string $app */ public function appIsDisabled($app) { - $fullUrl = $this->baseUrl . "v2.php/cloud/apps?filter=disabled"; + $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=disabled'; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { @@ -796,7 +830,7 @@ trait Provisioning { * @param string $app */ public function appIsEnabled($app) { - $fullUrl = $this->baseUrl . "v2.php/cloud/apps?filter=enabled"; + $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=enabled'; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { @@ -820,7 +854,7 @@ trait Provisioning { * @param string $app */ public function appIsNotEnabled($app) { - $fullUrl = $this->baseUrl . "v2.php/cloud/apps?filter=enabled"; + $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=enabled'; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { @@ -873,7 +907,7 @@ trait Provisioning { $this->response = $client->get($fullUrl, $options); // boolean to string is integer - Assert::assertEquals("1", simplexml_load_string($this->response->getBody())->data[0]->enabled); + Assert::assertEquals('1', simplexml_load_string($this->response->getBody())->data[0]->enabled); } /** @@ -882,13 +916,13 @@ 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], ]); // method used from BasicStructure trait - $this->sendingToWith("PUT", "/cloud/users/" . $user, $body); + $this->sendingToWith('PUT', '/cloud/users/' . $user, $body); } /** @@ -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 b1605e4da25..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 @@ -41,7 +42,7 @@ class RemoteContext implements Context { * @param string $remoteServer "NON_EXISTING" or "REMOTE" */ public function selectRemoteInstance($remoteServer) { - if ($remoteServer == "REMOTE") { + if ($remoteServer == 'REMOTE') { $baseUri = $this->remoteUrl; } else { $baseUri = 'nonexistingnextcloudserver.local'; 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 c0affcb9cda..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,15 +55,19 @@ 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; } try { - $this->response = $client->request("POST", $fullUrl, $options); + $this->response = $client->request('POST', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); } @@ -98,7 +103,7 @@ trait Sharing { public function acceptingLastShare() { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/pending/$share_id"; - $this->sendingToWith("POST", $url, null); + $this->sendingToWith('POST', $url, null); $this->theHTTPStatusCodeShouldBe('200'); } @@ -118,7 +123,7 @@ trait Sharing { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/pending/$share_id"; - $this->sendingToWith("POST", $url, null); + $this->sendingToWith('POST', $url, null); $this->currentUser = $previousUser; @@ -134,7 +139,7 @@ trait Sharing { } else { $url = $this->lastShareData->data->url; } - $fullUrl = $url . "/download"; + $fullUrl = $url . '/download'; $this->checkDownload($fullUrl, null, 'text/plain'); } @@ -148,7 +153,7 @@ trait Sharing { $token = $this->lastShareData->data->token; } - $fullUrl = substr($this->baseUrl, 0, -4) . "index.php/s/" . $token . "/download"; + $fullUrl = substr($this->baseUrl, 0, -4) . 'index.php/s/' . $token . '/download'; $this->checkDownload($fullUrl, null, 'text/plain'); } @@ -194,7 +199,7 @@ trait Sharing { * @When /^Adding expiration date to last share$/ */ public function addingExpirationDate() { - $share_id = (string) $this->lastShareData->data[0]->id; + $share_id = (string)$this->lastShareData->data[0]->id; $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; $client = new Client(); $options = []; @@ -203,9 +208,9 @@ trait Sharing { } else { $options['auth'] = [$this->currentUser, $this->regularUser]; } - $date = date('Y-m-d', strtotime("+3 days")); + $date = date('Y-m-d', strtotime('+3 days')); $options['form_params'] = ['expireDate' => $date]; - $this->response = $this->response = $client->request("PUT", $fullUrl, $options); + $this->response = $this->response = $client->request('PUT', $fullUrl, $options); Assert::assertEquals(200, $this->response->getStatusCode()); } @@ -214,7 +219,7 @@ trait Sharing { * @param TableNode|null $body */ public function updatingLastShare($body) { - $share_id = (string) $this->lastShareData->data[0]->id; + $share_id = (string)$this->lastShareData->data[0]->id; $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; $client = new Client(); $options = [ @@ -238,7 +243,7 @@ trait Sharing { } try { - $this->response = $client->request("PUT", $fullUrl, $options); + $this->response = $client->request('PUT', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); } @@ -286,13 +291,13 @@ trait Sharing { } if ($viewOnly === true) { - $body['attributes'] = json_encode([['scope' => 'permissions', 'key' => 'download', 'enabled' => false]]); + $body['attributes'] = json_encode([['scope' => 'permissions', 'key' => 'download', 'value' => false]]); } $options['form_params'] = $body; try { - $this->response = $client->request("POST", $fullUrl, $options); + $this->response = $client->request('POST', $fullUrl, $options); $this->lastShareData = simplexml_load_string($this->response->getBody()); } catch (\GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); @@ -303,18 +308,18 @@ trait Sharing { public function isFieldInResponse($field, $contentExpected) { $data = simplexml_load_string($this->response->getBody())->data[0]; if ((string)$field == 'expiration') { - if(!empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . " 00:00:00"; + if (!empty($contentExpected)) { + $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00'; } } if (count($data->element) > 0) { foreach ($data as $element) { - if ($contentExpected == "A_TOKEN") { + if ($contentExpected == 'A_TOKEN') { return (strlen((string)$element->$field) == 15); - } elseif ($contentExpected == "A_NUMBER") { + } elseif ($contentExpected == 'A_NUMBER') { return is_numeric((string)$element->$field); - } elseif ($contentExpected == "AN_URL") { - return $this->isExpectedUrl((string)$element->$field, "index.php/s/"); + } elseif ($contentExpected == 'AN_URL') { + return $this->isExpectedUrl((string)$element->$field, 'index.php/s/'); } elseif ((string)$element->$field == $contentExpected) { return true; } else { @@ -324,14 +329,16 @@ trait Sharing { return false; } else { - if ($contentExpected == "A_TOKEN") { + if ($contentExpected == 'A_TOKEN') { return (strlen((string)$data->$field) == 15); - } elseif ($contentExpected == "A_NUMBER") { + } elseif ($contentExpected == 'A_NUMBER') { return is_numeric((string)$data->$field); - } elseif ($contentExpected == "AN_URL") { - return $this->isExpectedUrl((string)$data->$field, "index.php/s/"); + } elseif ($contentExpected == 'AN_URL') { + return $this->isExpectedUrl((string)$data->$field, 'index.php/s/'); } elseif ($contentExpected == $data->$field) { return true; + } else { + print($data->$field); } return false; } @@ -455,7 +462,7 @@ trait Sharing { public function deletingLastShare() { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; - $this->sendingToWith("DELETE", $url, null); + $this->sendingToWith('DELETE', $url, null); } /** @@ -464,7 +471,7 @@ trait Sharing { public function gettingInfoOfLastShare() { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; - $this->sendingToWith("GET", $url, null); + $this->sendingToWith('GET', $url, null); } /** @@ -496,13 +503,13 @@ trait Sharing { $fd = $body->getRowsHash(); foreach ($fd as $field => $value) { - if (substr($field, 0, 10) === "share_with") { - $value = str_replace("REMOTE", substr($this->remoteBaseUrl, 0, -5), $value); - $value = str_replace("LOCAL", substr($this->localBaseUrl, 0, -5), $value); + if (substr($field, 0, 10) === 'share_with') { + $value = str_replace('REMOTE', substr($this->remoteBaseUrl, 0, -5), $value); + $value = str_replace('LOCAL', substr($this->localBaseUrl, 0, -5), $value); } - if (substr($field, 0, 6) === "remote") { - $value = str_replace("REMOTE", substr($this->remoteBaseUrl, 0, -4), $value); - $value = str_replace("LOCAL", substr($this->localBaseUrl, 0, -4), $value); + if (substr($field, 0, 6) === 'remote') { + $value = str_replace('REMOTE', substr($this->remoteBaseUrl, 0, -4), $value); + $value = str_replace('LOCAL', substr($this->localBaseUrl, 0, -4), $value); } if (!$this->isFieldInResponse($field, $value)) { Assert::fail("$field" . " doesn't have value " . "$value"); @@ -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 { @@ -603,7 +610,7 @@ trait Sharing { } if ($field === 'expiration' && !empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . " 00:00:00"; + $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00'; } if ($contentExpected === 'A_NUMBER') { 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 3a2c0e41744..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. @@ -245,7 +246,7 @@ class TagsContext implements \Behat\Behat\Context\Context { if (count($table->getRows()) !== count($tags)) { throw new \Exception( sprintf( - "Expected %s tags, got %s.", + 'Expected %s tags, got %s.', count($table->getRows()), count($tags) ) @@ -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 1b1be71bace..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 @@ -97,7 +98,7 @@ trait Trashbin { foreach ($elementsSimplified as $expectedElement) { $expectedElement = ltrim($expectedElement, '/'); if (array_search($expectedElement, $trashContent) === false) { - Assert::fail("$expectedElement" . " is not in trash listing"); + Assert::fail("$expectedElement" . ' is not in trash listing'); } } } diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index c2de207284d..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. @@ -17,7 +18,7 @@ require __DIR__ . '/../../vendor/autoload.php'; trait WebDav { use Sharing; - private string $davPath = "remote.php/webdav"; + private string $davPath = 'remote.php/webdav'; private bool $usingOldDavPath = true; private ?array $storedETAG = null; // map with user as key and another map as value, which has path as key and etag as value private ?int $storedFileID = null; @@ -40,7 +41,7 @@ trait WebDav { * @Given /^using old dav path$/ */ public function usingOldDavPath() { - $this->davPath = "remote.php/webdav"; + $this->davPath = 'remote.php/webdav'; $this->usingOldDavPath = true; } @@ -48,7 +49,15 @@ trait WebDav { * @Given /^using new dav path$/ */ public function usingNewDavPath() { - $this->davPath = "remote.php/dav"; + $this->davPath = 'remote.php/dav'; + $this->usingOldDavPath = false; + } + + /** + * @Given /^using new public dav path$/ + */ + public function usingNewPublicDavPath() { + $this->davPath = 'public.php/dav'; $this->usingOldDavPath = false; } @@ -60,13 +69,13 @@ trait WebDav { } } - public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = "files") { - if ($type === "files") { + public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = 'files') { + if ($type === 'files') { $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . "$path"; - } elseif ($type === "uploads") { + } elseif ($type === 'uploads') { $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . "$path"; } else { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path"; + $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path"; } $client = new GClient(); $options = [ @@ -75,7 +84,7 @@ trait WebDav { ]; if ($user === 'admin') { $options['auth'] = $this->adminUser; - } else { + } elseif ($user !== '') { $options['auth'] = [$user, $this->regularUser]; } return $client->request($method, $fullUrl, $options); @@ -90,7 +99,7 @@ trait WebDav { public function userMovedFile($user, $entry, $fileSource, $fileDestination) { $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); $headers['Destination'] = $fullUrl . $fileDestination; - $this->response = $this->makeDavRequest($user, "MOVE", $fileSource, $headers); + $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); Assert::assertEquals(201, $this->response->getStatusCode()); } @@ -104,7 +113,7 @@ trait WebDav { $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); $headers['Destination'] = $fullUrl . $fileDestination; try { - $this->response = $this->makeDavRequest($user, "MOVE", $fileSource, $headers); + $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); } catch (\GuzzleHttp\Exception\ClientException $e) { $this->response = $e->getResponse(); } @@ -134,7 +143,7 @@ trait WebDav { */ public function downloadFileWithRange($fileSource, $range) { $headers['Range'] = $range; - $this->response = $this->makeDavRequest($this->currentUser, "GET", $fileSource, $headers); + $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileSource, $headers); } /** @@ -151,7 +160,7 @@ trait WebDav { 'Range' => $range ]; - $this->response = $client->request("GET", $fullUrl, $options); + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -169,7 +178,7 @@ trait WebDav { ] ]; - $this->response = $client->request("GET", $fullUrl, $options); + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -189,7 +198,7 @@ trait WebDav { */ public function checkPropForFile($file, $prefix, $prop, $value) { $elementList = $this->propfindFile($this->currentUser, $file, "<$prefix:$prop/>"); - $property = $elementList['/'.$this->getDavFilesPath($this->currentUser).$file][200]["{DAV:}$prop"]; + $property = $elementList['/' . $this->getDavFilesPath($this->currentUser) . $file][200]["{DAV:}$prop"]; Assert::assertEquals($property, $value); } @@ -231,6 +240,37 @@ trait WebDav { } /** + * @When Downloading folder :folderName + */ + public function downloadingFolder(string $folderName) { + try { + $this->response = $this->makeDavRequest($this->currentUser, 'GET', $folderName, ['Accept' => 'application/zip']); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @When Downloading public folder :folderName + */ + public function downloadPublicFolder(string $folderName) { + $token = $this->lastShareData->data->token; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$folderName"; + + $client = new GClient(); + $options = []; + $options['headers'] = [ + 'Accept' => 'application/zip' + ]; + + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** * @When Downloading file :fileName * @param string $fileName */ @@ -243,6 +283,42 @@ trait WebDav { } /** + * @When Downloading public file :filename + */ + public function downloadingPublicFile(string $filename) { + $token = $this->lastShareData->data->token; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$filename"; + + $client = new GClient(); + $options = [ + 'headers' => [ + 'X-Requested-With' => 'XMLHttpRequest', + ] + ]; + + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @When Downloading public file :filename without ajax header + */ + public function downloadingPublicFileWithoutHeader(string $filename) { + $token = $this->lastShareData->data->token; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$filename"; + + $client = new GClient(); + try { + $this->response = $client->request('GET', $fullUrl); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** * @Then Downloaded content should start with :start * @param int $start * @throws \Exception @@ -348,7 +424,7 @@ trait WebDav { public function theResponseShouldContainAShareTypesPropertyWith($table) { $keys = $this->response; if (!array_key_exists('{http://owncloud.org/ns}share-types', $keys)) { - throw new \Exception("Cannot find property \"{http://owncloud.org/ns}share-types\""); + throw new \Exception('Cannot find property "{http://owncloud.org/ns}share-types"'); } $foundTypes = []; @@ -513,7 +589,7 @@ trait WebDav { </d:searchrequest>'; try { - $this->response = $this->makeDavRequest($user, "SEARCH", '', [ + $this->response = $this->makeDavRequest($user, 'SEARCH', '', [ 'Content-Type' => 'text/xml' ], $body, ''); @@ -555,7 +631,7 @@ trait WebDav { if ($type === 'files') { return $this->encodePath($this->getDavFilesPath($user) . $path); } else { - return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path); + return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path); } } @@ -588,9 +664,9 @@ trait WebDav { $elementRows = $expectedElements->getRows(); $elementsSimplified = $this->simplifyArray($elementRows); foreach ($elementsSimplified as $expectedElement) { - $webdavPath = "/" . $this->getDavFilesPath($user) . $expectedElement; + $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement; if (!array_key_exists($webdavPath, $elementList)) { - Assert::fail("$webdavPath" . " is not in propfind answer"); + Assert::fail("$webdavPath" . ' is not in propfind answer'); } } } @@ -605,7 +681,7 @@ trait WebDav { public function userUploadsAFileTo($user, $source, $destination) { $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); try { - $this->response = $this->makeDavRequest($user, "PUT", $destination, [], $file); + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -622,11 +698,11 @@ trait WebDav { * @param string $destination */ public function userAddsAFileTo($user, $bytes, $destination) { - $filename = "filespecificSize.txt"; + $filename = 'filespecificSize.txt'; $this->createFileSpecificSize($filename, $bytes); Assert::assertEquals(1, file_exists("work/$filename")); $this->userUploadsAFileTo($user, "work/$filename", $destination); - $this->removeFile("work/", $filename); + $this->removeFile('work/', $filename); $expectedElements = new \Behat\Gherkin\Node\TableNode([["$destination"]]); $this->checkElementList($user, $expectedElements); } @@ -637,7 +713,7 @@ trait WebDav { public function userUploadsAFileWithContentTo($user, $content, $destination) { $file = \GuzzleHttp\Psr7\Utils::streamFor($content); try { - $this->response = $this->makeDavRequest($user, "PUT", $destination, [], $file); + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -673,7 +749,7 @@ trait WebDav { public function userCreatedAFolder($user, $destination) { try { $destination = '/' . ltrim($destination, '/'); - $this->response = $this->makeDavRequest($user, "MKCOL", $destination, []); + $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, []); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -684,21 +760,6 @@ trait WebDav { } /** - * @Given user :user uploads chunk file :num of :total with :data to :destination - * @param string $user - * @param int $num - * @param int $total - * @param string $data - * @param string $destination - */ - public function userUploadsChunkFileOfWithToWithChecksum($user, $num, $total, $data, $destination) { - $num -= 1; - $data = \GuzzleHttp\Psr7\Utils::streamFor($data); - $file = $destination . '-chunking-42-' . $total . '-' . $num; - $this->makeDavRequest($user, 'PUT', $file, ['OC-Chunked' => '1'], $data, "uploads"); - } - - /** * @Given user :user uploads bulked files :name1 with :content1 and :name2 with :content2 and :name3 with :content3 * @param string $user * @param string $name1 @@ -709,31 +770,31 @@ trait WebDav { * @param string $content3 */ public function userUploadsBulkedFiles($user, $name1, $content1, $name2, $content2, $name3, $content3) { - $boundary = "boundary_azertyuiop"; + $boundary = 'boundary_azertyuiop'; - $body = ""; - $body .= '--'.$boundary."\r\n"; - $body .= "X-File-Path: ".$name1."\r\n"; + $body = ''; + $body .= '--' . $boundary . "\r\n"; + $body .= 'X-File-Path: ' . $name1 . "\r\n"; $body .= "X-File-MD5: f6a6263167c92de8644ac998b3c4e4d1\r\n"; $body .= "X-OC-Mtime: 1111111111\r\n"; - $body .= "Content-Length: ".strlen($content1)."\r\n"; + $body .= 'Content-Length: ' . strlen($content1) . "\r\n"; $body .= "\r\n"; - $body .= $content1."\r\n"; - $body .= '--'.$boundary."\r\n"; - $body .= "X-File-Path: ".$name2."\r\n"; + $body .= $content1 . "\r\n"; + $body .= '--' . $boundary . "\r\n"; + $body .= 'X-File-Path: ' . $name2 . "\r\n"; $body .= "X-File-MD5: 87c7d4068be07d390a1fffd21bf1e944\r\n"; $body .= "X-OC-Mtime: 2222222222\r\n"; - $body .= "Content-Length: ".strlen($content2)."\r\n"; + $body .= 'Content-Length: ' . strlen($content2) . "\r\n"; $body .= "\r\n"; - $body .= $content2."\r\n"; - $body .= '--'.$boundary."\r\n"; - $body .= "X-File-Path: ".$name3."\r\n"; + $body .= $content2 . "\r\n"; + $body .= '--' . $boundary . "\r\n"; + $body .= 'X-File-Path: ' . $name3 . "\r\n"; $body .= "X-File-MD5: e86a1cf0678099986a901c79086f5617\r\n"; $body .= "X-File-Mtime: 3333333333\r\n"; - $body .= "Content-Length: ".strlen($content3)."\r\n"; + $body .= 'Content-Length: ' . strlen($content3) . "\r\n"; $body .= "\r\n"; - $body .= $content3."\r\n"; - $body .= '--'.$boundary."--\r\n"; + $body .= $content3 . "\r\n"; + $body .= '--' . $boundary . "--\r\n"; $stream = fopen('php://temp', 'r+'); fwrite($stream, $body); @@ -743,13 +804,13 @@ trait WebDav { $options = [ 'auth' => [$user, $this->regularUser], 'headers' => [ - 'Content-Type' => 'multipart/related; boundary='.$boundary, + 'Content-Type' => 'multipart/related; boundary=' . $boundary, 'Content-Length' => (string)strlen($body), ], 'body' => $body ]; - return $client->request("POST", substr($this->baseUrl, 0, -4) . "remote.php/dav/bulk", $options); + return $client->request('POST', substr($this->baseUrl, 0, -4) . 'remote.php/dav/bulk', $options); } /** @@ -758,7 +819,7 @@ trait WebDav { public function userCreatesANewChunkingUploadWithId($user, $id) { $this->parts = []; $destination = '/uploads/' . $user . '/' . $id; - $this->makeDavRequest($user, 'MKCOL', $destination, [], null, "uploads"); + $this->makeDavRequest($user, 'MKCOL', $destination, [], null, 'uploads'); } /** @@ -767,7 +828,7 @@ trait WebDav { public function userUploadsNewChunkFileOfWithToId($user, $num, $data, $id) { $data = \GuzzleHttp\Psr7\Utils::streamFor($data); $destination = '/uploads/' . $user . '/' . $id . '/' . $num; - $this->makeDavRequest($user, 'PUT', $destination, [], $data, "uploads"); + $this->makeDavRequest($user, 'PUT', $destination, [], $data, 'uploads'); } /** @@ -778,7 +839,7 @@ trait WebDav { $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest; $this->makeDavRequest($user, 'MOVE', $source, [ 'Destination' => $destination - ], null, "uploads"); + ], null, 'uploads'); } /** @@ -792,7 +853,7 @@ trait WebDav { $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ 'Destination' => $destination, 'OC-Total-Length' => $size - ], null, "uploads"); + ], null, 'uploads'); } catch (\GuzzleHttp\Exception\BadResponseException $ex) { $this->response = $ex->getResponse(); } @@ -808,7 +869,7 @@ trait WebDav { $destination = '/uploads/' . $user . '/' . $this->getUploadId($id); $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [ 'Destination' => $this->s3MultipartDestination, - ], null, "uploads"); + ], null, 'uploads'); } /** @@ -819,7 +880,7 @@ trait WebDav { $destination = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/' . $num; $this->response = $this->makeDavRequest($user, 'PUT', $destination, [ 'Destination' => $this->s3MultipartDestination - ], $data, "uploads"); + ], $data, 'uploads'); } /** @@ -830,7 +891,7 @@ trait WebDav { try { $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ 'Destination' => $this->s3MultipartDestination, - ], null, "uploads"); + ], null, 'uploads'); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -957,6 +1018,23 @@ trait WebDav { } /** + * @When Requesting share note on dav endpoint + */ + public function requestingShareNote() { + $propfind = '<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns"><d:prop><nc:note /></d:prop></d:propfind>'; + if (count($this->lastShareData->data->element) > 0) { + $token = $this->lastShareData->data[0]->token; + } else { + $token = $this->lastShareData->data->token; + } + try { + $this->response = $this->makeDavRequest('', 'PROPFIND', $token, [], $propfind); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** * @Then there are no duplicate headers */ public function thereAreNoDuplicateHeaders() { @@ -984,9 +1062,9 @@ trait WebDav { $elementRows = $expectedElements->getRows(); $elementsSimplified = $this->simplifyArray($elementRows); foreach ($elementsSimplified as $expectedElement) { - $webdavPath = "/" . $this->getDavFilesPath($user) . $expectedElement; + $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement; if (!array_key_exists($webdavPath, $elementList)) { - Assert::fail("$webdavPath" . " is not in report answer"); + Assert::fail("$webdavPath" . ' is not in report answer'); } } } @@ -1001,12 +1079,12 @@ trait WebDav { $elementList = $this->listFolder($user, $folder, 1); $elementListKeys = array_keys($elementList); array_shift($elementListKeys); - $davPrefix = "/" . $this->getDavFilesPath($user); + $davPrefix = '/' . $this->getDavFilesPath($user); foreach ($elementListKeys as $element) { if (substr($element, 0, strlen($davPrefix)) == $davPrefix) { $element = substr($element, strlen($davPrefix)); } - $this->userDeletesFile($user, "element", $element); + $this->userDeletesFile($user, 'element', $element); } } @@ -1017,7 +1095,7 @@ trait WebDav { * @return int */ private function getFileIdForPath($user, $path) { - $propertiesTable = new \Behat\Gherkin\Node\TableNode([["{http://owncloud.org/ns}fileid"]]); + $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{http://owncloud.org/ns}fileid']]); $this->asGetsPropertiesOfFolderWith($user, 'file', $path, $propertiesTable); return (int)$this->response['{http://owncloud.org/ns}fileid']; } @@ -1047,7 +1125,7 @@ trait WebDav { public function userCreatesAFileLocallyWithChunks($arg1, $chunks) { $this->parts = []; for ($i = 1;$i <= (int)$chunks;$i++) { - $randomletter = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 1); + $randomletter = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 1); file_put_contents('/tmp/part-upload-' . $i, str_repeat($randomletter, 5 * 1024 * 1024)); $this->parts[] = '/tmp/part-upload-' . $i; } @@ -1057,7 +1135,7 @@ trait WebDav { * @Given user :user creates the chunk :id with a size of :size MB */ public function userCreatesAChunk($user, $id, $size) { - $randomletter = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 1); + $randomletter = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 1); file_put_contents('/tmp/part-upload-' . $id, str_repeat($randomletter, (int)$size * 1024 * 1024)); $this->parts[] = '/tmp/part-upload-' . $id; } 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/log-condition.feature b/build/integration/features/log-condition.feature new file mode 100644 index 00000000000..4059db1ebf3 --- /dev/null +++ b/build/integration/features/log-condition.feature @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: log-condition + + Background: + Given invoking occ with "config:system:set log.condition matches 0 users 0 --value admin" + Then the command was successful + + Scenario: Accessing /status.php with log.condition + When requesting "/status.php" with "GET" + Then the HTTP status code should be "200" + + Scenario: Accessing /index.php with log.condition + When requesting "/index.php" with "GET" + Then the HTTP status code should be "200" + + Scenario: Accessing /remote.php/webdav with log.condition + When requesting "/remote.php/webdav" with "GET" + Then the HTTP status code should be "401" + + Scenario: Accessing /remote.php/dav with log.condition + When requesting "/remote.php/dav" with "GET" + Then the HTTP status code should be "401" + + Scenario: Accessing /ocs/v1.php with log.condition + When requesting "/ocs/v1.php" with "GET" + Then the HTTP status code should be "200" + + Scenario: Accessing /ocs/v2.php with log.condition + When requesting "/ocs/v2.php" with "GET" + Then the HTTP status code should be "404" + + Scenario: Accessing /public.php/webdav with log.condition + When requesting "/public.php/webdav" with "GET" + Then the HTTP status code should be "401" + + Scenario: Accessing /public.php/dav with log.condition + When requesting "/public.php/dav" with "GET" + Then the HTTP status code should be "503" diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 2238c820939..8fcfb076497 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -2,62 +2,65 @@ # SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc. # SPDX-License-Identifier: AGPL-3.0-only Feature: provisioning - Background: - Given using api version "1" - - Scenario: Getting an not existing user - Given As an "admin" - When sending "GET" to "/cloud/users/test" - Then the OCS status code should be "404" - And the HTTP status code should be "200" - - Scenario: Listing all users - Given As an "admin" - When sending "GET" to "/cloud/users" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Create a user - Given As an "admin" - And user "brand-new-user" does not exist - When sending "POST" to "/cloud/users" with - | userid | brand-new-user | - | password | 123456 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And user "brand-new-user" exists - - Scenario: Create an existing user - Given As an "admin" - And user "brand-new-user" exists - When sending "POST" to "/cloud/users" with - | userid | brand-new-user | - | password | 123456 | - Then the OCS status code should be "102" - And the HTTP status code should be "200" - And user "brand-new-user" has - | id | brand-new-user | - | displayname | brand-new-user | - | email | | - | phone | | - | address | | - | website | | - | twitter | | - - Scenario: Get an existing user - Given As an "admin" - When sending "GET" to "/cloud/users/brand-new-user" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Getting all users - Given As an "admin" - And user "brand-new-user" exists - And user "admin" exists - When sending "GET" to "/cloud/users" - Then users returned are - | brand-new-user | - | admin | + 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" + When sending "GET" to "/cloud/users/test" + Then the OCS status code should be "404" + And the HTTP status code should be "200" + + Scenario: Listing all users + Given As an "admin" + When sending "GET" to "/cloud/users" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: Create a user + Given As an "admin" + And user "brand-new-user" does not exist + When sending "POST" to "/cloud/users" with + | userid | brand-new-user | + | password | 123456 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And user "brand-new-user" exists + + Scenario: Create an existing user + Given As an "admin" + And user "brand-new-user" exists + When sending "POST" to "/cloud/users" with + | userid | brand-new-user | + | password | 123456 | + Then the OCS status code should be "102" + And the HTTP status code should be "200" + And user "brand-new-user" has + | id | brand-new-user | + | displayname | brand-new-user | + | email | | + | phone | | + | address | | + | website | | + | twitter | | + + Scenario: Get an existing user + Given As an "admin" + When sending "GET" to "/cloud/users/brand-new-user" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: Getting all users + Given As an "admin" + And user "brand-new-user" exists + And user "admin" exists + When sending "GET" to "/cloud/users" + Then users returned are + | brand-new-user | + | admin | Scenario: Get editable fields Given As an "admin" @@ -69,13 +72,15 @@ Feature: provisioning | phone | | address | | website | - | twitter | + | twitter | + | bluesky | | fediverse | | organisation | | role | | headline | | biography | | profile_enabled | + | pronouns | Given As an "brand-new-user" Then user "brand-new-user" has editable fields | displayname | @@ -85,12 +90,14 @@ Feature: provisioning | address | | website | | twitter | + | bluesky | | fediverse | | organisation | | role | | headline | | biography | | profile_enabled | + | pronouns | Then user "self" has editable fields | displayname | | email | @@ -99,31 +106,33 @@ Feature: provisioning | address | | website | | twitter | + | bluesky | | fediverse | | organisation | | role | | headline | | biography | | profile_enabled | + | pronouns | - Scenario: Edit a user - Given As an "admin" - And user "brand-new-user" exists - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | displayname | - | value | Brand New User | - And 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 | quota | - | value | 12MB | - And 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 | - And the OCS status code should be "100" - And the HTTP status code should be "200" + Scenario: Edit a user + Given As an "admin" + And user "brand-new-user" exists + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | displayname | + | value | Brand New User | + And 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 | quota | + | value | 12MB | + And 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 | + And 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 | additional_mail | | value | no.reply@nextcloud.com | @@ -134,105 +143,126 @@ Feature: provisioning | value | noreply@nextcloud.com | And 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 | phone | - | value | +49 711 / 25 24 28-90 | - And 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 | address | - | value | Foo Bar Town | - And 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 | website | - | value | https://nextcloud.com | - And 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 | twitter | - | value | Nextcloud | - 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 | - | displayname | Brand New User | - | email | no-reply@nextcloud.com | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | phone | + | value | +49 711 / 25 24 28-90 | + And 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 | address | + | value | Foo Bar Town | + And 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 | website | + | value | https://nextcloud.com | + And 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 | 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 + | id | brand-new-user | + | displayname | Brand New User | + | email | no-reply@nextcloud.com | | additional_mail | no.reply@nextcloud.com;noreply@nextcloud.com | - | phone | +4971125242890 | - | address | Foo Bar Town | - | website | https://nextcloud.com | - | twitter | Nextcloud | + | phone | +4971125242890 | + | address | Foo Bar Town | + | website | https://nextcloud.com | + | 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 + Scenario: Edit a user account properties scopes + Given user "brand-new-user" exists And As an "brand-new-user" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | phoneScope | - | value | v2-private | - 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 | twitterScope | - | 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 | - 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 | emailScope | - | 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 | - And the OCS status code should be "100" - And the HTTP status code should be "200" - # Duplicating primary address - And sending "PUT" to "/cloud/users/brand-new-user" with - | key | additional_mail | - | value | no-reply@nextcloud.com | - And the OCS status code should be "102" - And the HTTP status code should be "200" - And sending "PUT" to "/cloud/users/brand-new-user" with - | key | additional_mail | - | value | no.reply2@nextcloud.com | - And the OCS status code should be "100" - And the HTTP status code should be "200" - # Duplicating another additional address - And sending "PUT" to "/cloud/users/brand-new-user" with - | key | additional_mail | - | value | no.reply2@nextcloud.com | - And the OCS status code should be "102" - And the HTTP status code should be "200" - Then user "brand-new-user" has - | id | brand-new-user | - | phoneScope | v2-private | - | twitterScope | v2-local | - | addressScope | v2-federated | - | emailScope | v2-published | - | websiteScope | v2-published | - | displaynameScope | v2-federated | - | avatarScope | v2-local | + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | phoneScope | + | value | v2-private | + 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 | twitterScope | + | 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 | + 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 | emailScope | + | value | v2-published | + 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 | + And the OCS status code should be "100" + And the HTTP status code should be "200" + # Duplicating primary address + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_mail | + | value | no-reply@nextcloud.com | + And the OCS status code should be "101" + And the HTTP status code should be "200" + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_mail | + | value | no.reply2@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + # Duplicating another additional address + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_mail | + | value | no.reply2@nextcloud.com | + And the OCS status code should be "101" + And the HTTP status code should be "200" + Then user "brand-new-user" has + | id | brand-new-user | + | phoneScope | v2-private | + | twitterScope | v2-local | + | blueskyScope | v2-local | + | addressScope | v2-federated | + | emailScope | v2-published | Scenario: Edit a user account multivalue property scopes Given user "brand-new-user" exists @@ -261,24 +291,24 @@ Feature: provisioning | id | brand-new-user | | additional_mailScope | v2-federated;v2-published | - Scenario: Edit a user account properties scopes with invalid or unsupported value - Given user "brand-new-user" exists + Scenario: Edit a user account properties scopes with invalid or unsupported value + Given user "brand-new-user" exists And As an "brand-new-user" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | phoneScope | - | value | invalid | - Then the OCS status code should be "102" - And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | displaynameScope | - | value | v2-private | - Then the OCS status code should be "102" - And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | emailScope | - | value | v2-private | - Then the OCS status code should be "102" - And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | phoneScope | + | value | invalid | + Then the OCS status code should be "101" + And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | displaynameScope | + | value | v2-private | + Then the OCS status code should be "101" + And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | emailScope | + | value | v2-private | + Then the OCS status code should be "101" + And the HTTP status code should be "200" Scenario: Edit a user account multi-value property scopes with invalid or unsupported value Given user "brand-new-user" exists @@ -317,539 +347,546 @@ Feature: provisioning Then user "brand-new-user" has not | additional_mail | no.reply6@nextcloud.com | - Scenario: An admin cannot edit user account property scopes - Given As an "admin" - And user "brand-new-user" exists - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | phoneScope | - | value | v2-private | - Then the OCS status code should be "103" - And the HTTP status code should be "200" - - Scenario: Search by phone number - Given As an "admin" - And user "phone-user" exists - And sending "PUT" to "/cloud/users/phone-user" with - | key | phone | - | value | +49 711 / 25 24 28-90 | - And the OCS status code should be "100" - And the HTTP status code should be "200" - Then search users by phone for region "DE" with - | random-string1 | 0711 / 123 456 78 | - | random-string1 | 0711 / 252 428-90 | - | random-string2 | 0711 / 90-824 252 | - And the OCS status code should be "100" - And the HTTP status code should be "200" - Then phone matches returned are - | random-string1 | phone-user@localhost:8080 | - - Scenario: Create a group - Given As an "admin" - And group "new-group" does not exist - When sending "POST" to "/cloud/groups" with - | groupid | new-group | - | password | 123456 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And group "new-group" exists - And group "new-group" has - | displayname | new-group | - - Scenario: Create a group with custom display name - Given As an "admin" - And group "new-group" does not exist - When sending "POST" to "/cloud/groups" with - | groupid | new-group | - | password | 123456 | - | displayname | new-group-displayname | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And group "new-group" exists - And group "new-group" has - | displayname | new-group-displayname | - - Scenario: Create a group with special characters - Given As an "admin" - And group "España" does not exist - When sending "POST" to "/cloud/groups" with - | groupid | España | - | password | 123456 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And group "España" exists - And group "España" has - | displayname | España | - - Scenario: adding user to a group without sending the group - Given As an "admin" - And user "brand-new-user" exists - When sending "POST" to "/cloud/users/brand-new-user/groups" with - | groupid | | - Then the OCS status code should be "101" - And the HTTP status code should be "200" - - Scenario: adding user to a group which doesn't exist - Given As an "admin" - And user "brand-new-user" exists - And group "not-group" does not exist - When sending "POST" to "/cloud/users/brand-new-user/groups" with - | groupid | not-group | - Then the OCS status code should be "102" - And the HTTP status code should be "200" - - Scenario: adding user to a group without privileges - Given user "brand-new-user" exists - And As an "brand-new-user" - When sending "POST" to "/cloud/users/brand-new-user/groups" with - | groupid | new-group | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: adding user to a group - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - When sending "POST" to "/cloud/users/brand-new-user/groups" with - | groupid | new-group | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: getting groups of an user - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - When sending "GET" to "/cloud/users/brand-new-user/groups" - Then groups returned are - | new-group | - And the OCS status code should be "100" - - Scenario: adding a user which doesn't exist to a group - Given As an "admin" - And user "not-user" does not exist - And group "new-group" exists - When sending "POST" to "/cloud/users/not-user/groups" with - | groupid | new-group | - Then the OCS status code should be "103" - And the HTTP status code should be "200" - - Scenario: getting a group - Given As an "admin" - And group "new-group" exists - When sending "GET" to "/cloud/groups/new-group" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Getting all groups - Given As an "admin" - And group "new-group" exists - And group "admin" exists - When sending "GET" to "/cloud/groups" - Then groups returned are - | España | - | admin | - | new-group | - - Scenario: create a subadmin - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - When sending "POST" to "/cloud/users/brand-new-user/subadmins" with - | groupid | new-group | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: get users using a subadmin - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - And user "brand-new-user" belongs to group "new-group" - And user "brand-new-user" is subadmin of group "new-group" - And As an "brand-new-user" - When sending "GET" to "/cloud/users" - Then users returned are - | brand-new-user | - And the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: removing a user from a group which doesn't exists - Given As an "admin" - And user "brand-new-user" exists - And group "not-group" does not exist - When sending "DELETE" to "/cloud/users/brand-new-user/groups" with - | groupid | not-group | - Then the OCS status code should be "102" - - Scenario: removing a user from a group - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - And user "brand-new-user" belongs to group "new-group" - When sending "DELETE" to "/cloud/users/brand-new-user/groups" with - | groupid | new-group | - Then the OCS status code should be "100" - And user "brand-new-user" does not belong to group "new-group" - - Scenario: create a subadmin using a user which not exist - Given As an "admin" - And user "not-user" does not exist - And group "new-group" exists - When sending "POST" to "/cloud/users/not-user/subadmins" with - | groupid | new-group | - Then the OCS status code should be "101" - And the HTTP status code should be "200" - - Scenario: create a subadmin using a group which not exist - Given As an "admin" - And user "brand-new-user" exists - And group "not-group" does not exist - When sending "POST" to "/cloud/users/brand-new-user/subadmins" with - | groupid | not-group | - Then the OCS status code should be "102" - And the HTTP status code should be "200" - - Scenario: Getting subadmin groups - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - When sending "GET" to "/cloud/users/brand-new-user/subadmins" - Then subadmin groups returned are - | new-group | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Getting subadmin groups of a user which not exist - Given As an "admin" - And user "not-user" does not exist - And group "new-group" exists - When sending "GET" to "/cloud/users/not-user/subadmins" - Then the OCS status code should be "404" - And the HTTP status code should be "200" - - Scenario: Getting subadmin users of a group - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - When sending "GET" to "/cloud/groups/new-group/subadmins" - Then subadmin users returned are - | brand-new-user | - And the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Getting subadmin users of a group which doesn't exist - Given As an "admin" - And user "brand-new-user" exists - And group "not-group" does not exist - When sending "GET" to "/cloud/groups/not-group/subadmins" - Then the OCS status code should be "101" - And the HTTP status code should be "200" - - Scenario: Removing subadmin from a group - Given As an "admin" - And user "brand-new-user" exists - And group "new-group" exists - And user "brand-new-user" is subadmin of group "new-group" - When sending "DELETE" to "/cloud/users/brand-new-user/subadmins" with - | groupid | new-group | - And the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Delete a user - Given As an "admin" - And user "brand-new-user" exists - When sending "DELETE" to "/cloud/users/brand-new-user" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And user "brand-new-user" does not exist - - Scenario: Delete a group - Given As an "admin" - And group "new-group" exists - When sending "DELETE" to "/cloud/groups/new-group" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And group "new-group" does not exist - - Scenario: Delete a group with special characters - Given As an "admin" - And group "España" exists - When sending "DELETE" to "/cloud/groups/España" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And group "España" does not exist - - Scenario: get enabled apps - Given As an "admin" - When sending "GET" to "/cloud/apps?filter=enabled" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And apps returned are - | cloud_federation_api | - | comments | - | contactsinteraction | - | dashboard | - | dav | - | federatedfilesharing | - | federation | - | files | - | files_reminders | - | files_sharing | - | files_trashbin | - | files_versions | - | lookup_server_connector | - | provisioning_api | - | settings | - | sharebymail | - | systemtags | - | theming | - | twofactor_backupcodes | - | updatenotification | - | user_ldap | - | user_status | - | viewer | - | workflowengine | - | weather_status | - | files_external | - | oauth2 | - - Scenario: get app info - Given As an "admin" - When sending "GET" to "/cloud/apps/files" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: get app info from app that does not exist - Given As an "admin" - When sending "GET" to "/cloud/apps/this_app_should_never_exist" - Then the OCS status code should be "998" - And the HTTP status code should be "200" - - Scenario: enable an app - Given As an "admin" - And app "testing" is disabled - When sending "POST" to "/cloud/apps/testing" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And app "testing" is enabled - - Scenario: enable an app that does not exist - Given As an "admin" - When sending "POST" to "/cloud/apps/this_app_should_never_exist" - Then the OCS status code should be "998" - And the HTTP status code should be "200" - - Scenario: disable an app - 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 - - Scenario: disable an user - Given As an "admin" - And user "user1" exists - When sending "PUT" to "/cloud/users/user1/disable" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And user "user1" is disabled - - Scenario: enable an user - Given As an "admin" - And user "user1" exists - And assure user "user1" is disabled - When sending "PUT" to "/cloud/users/user1/enable" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And user "user1" is enabled - - Scenario: Subadmin should be able to enable or disable an user in their group - Given As an "admin" - And user "subadmin" exists - And user "user1" exists - And group "new-group" exists - And user "subadmin" belongs to group "new-group" - And user "user1" belongs to group "new-group" - And Assure user "subadmin" is subadmin of group "new-group" - And As an "subadmin" - When sending "PUT" to "/cloud/users/user1/disable" - Then the OCS status code should be "100" - Then the HTTP status code should be "200" - And As an "admin" - And user "user1" is disabled - - Scenario: Subadmin should not be able to enable or disable an user not in their group - Given As an "admin" - And user "subadmin" exists - And user "user1" exists - And group "new-group" exists - And group "another-group" exists - And user "subadmin" belongs to group "new-group" - And user "user1" belongs to group "another-group" - And Assure user "subadmin" is subadmin of group "new-group" - And As an "subadmin" - When sending "PUT" to "/cloud/users/user1/disable" - Then the OCS status code should be "998" - Then the HTTP status code should be "200" - And As an "admin" - And user "user1" is enabled - - Scenario: Subadmins should not be able to disable users that have admin permissions in their group - Given As an "admin" - And user "another-admin" exists - And user "subadmin" exists - And group "new-group" exists - And user "another-admin" belongs to group "admin" - And user "subadmin" belongs to group "new-group" - And user "another-admin" belongs to group "new-group" - And Assure user "subadmin" is subadmin of group "new-group" - And As an "subadmin" - When sending "PUT" to "/cloud/users/another-admin/disable" - Then the OCS status code should be "998" - Then the HTTP status code should be "200" - And As an "admin" - And user "another-admin" is enabled - - Scenario: Admin can disable another admin user - Given As an "admin" - And user "another-admin" exists - And user "another-admin" belongs to group "admin" - When sending "PUT" to "/cloud/users/another-admin/disable" - Then the OCS status code should be "100" - Then the HTTP status code should be "200" - And user "another-admin" is disabled - - Scenario: Admin can enable another admin user - Given As an "admin" - And user "another-admin" exists - And user "another-admin" belongs to group "admin" - And assure user "another-admin" is disabled - When sending "PUT" to "/cloud/users/another-admin/enable" - Then the OCS status code should be "100" - Then the HTTP status code should be "200" - And user "another-admin" is enabled - - Scenario: Admin can disable subadmins in the same group - Given As an "admin" - And user "subadmin" exists - And group "new-group" exists - And user "subadmin" belongs to group "new-group" - And user "admin" belongs to group "new-group" - And Assure user "subadmin" is subadmin of group "new-group" - When sending "PUT" to "/cloud/users/subadmin/disable" - Then the OCS status code should be "100" - Then the HTTP status code should be "200" - And user "subadmin" is disabled - - Scenario: Admin can enable subadmins in the same group - Given As an "admin" - And user "subadmin" exists - And group "new-group" exists - And user "subadmin" belongs to group "new-group" - And user "admin" belongs to group "new-group" - And Assure user "subadmin" is subadmin of group "new-group" - And assure user "another-admin" is disabled - When sending "PUT" to "/cloud/users/subadmin/disable" - Then the OCS status code should be "100" - Then the HTTP status code should be "200" - And user "subadmin" is disabled - - Scenario: Admin user cannot disable himself - Given As an "admin" - And user "another-admin" exists - And user "another-admin" belongs to group "admin" - And As an "another-admin" - When sending "PUT" to "/cloud/users/another-admin/disable" - Then the OCS status code should be "101" - And the HTTP status code should be "200" - And As an "admin" - And user "another-admin" is enabled - - Scenario:Admin user cannot enable himself - Given As an "admin" - And user "another-admin" exists - And user "another-admin" belongs to group "admin" - And assure user "another-admin" is disabled - And As an "another-admin" - When sending "PUT" to "/cloud/users/another-admin/enable" - And As an "admin" - Then user "another-admin" is disabled - - Scenario: disable an user with a regular user - Given As an "admin" - And user "user1" exists - And user "user2" exists - And As an "user1" - When sending "PUT" to "/cloud/users/user2/disable" - Then the OCS status code should be "403" - And the HTTP status code should be "200" - And As an "admin" - And user "user2" is enabled - - Scenario: enable an user with a regular user - Given As an "admin" - And user "user1" exists - And user "user2" exists - And assure user "user2" is disabled - And As an "user1" - When sending "PUT" to "/cloud/users/user2/enable" - Then the OCS status code should be "403" - And the HTTP status code should be "200" - And As an "admin" - And user "user2" is disabled - - Scenario: Subadmin should not be able to disable himself - Given As an "admin" - And user "subadmin" exists - And group "new-group" exists - And user "subadmin" belongs to group "new-group" - And Assure user "subadmin" is subadmin of group "new-group" - And As an "subadmin" - When sending "PUT" to "/cloud/users/subadmin/disable" - Then the OCS status code should be "101" - Then the HTTP status code should be "200" - And As an "admin" - And user "subadmin" is enabled - - Scenario: Subadmin should not be able to enable himself - Given As an "admin" - And user "subadmin" exists - And group "new-group" exists - And user "subadmin" belongs to group "new-group" - And Assure user "subadmin" is subadmin of group "new-group" - And assure user "subadmin" is disabled - And As an "subadmin" - When sending "PUT" to "/cloud/users/subadmin/enabled" - And As an "admin" - And user "subadmin" is disabled - - Scenario: Making a ocs request with an enabled user - Given As an "admin" - And user "user0" exists - And As an "user0" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And the OCS status code should be "100" - - Scenario: Making a web request with an enabled user - Given As an "admin" - And user "user0" exists - And As an "user0" - When sending "GET" with exact url to "/index.php/apps/files" - Then the HTTP status code should be "200" - - Scenario: Making a ocs request with a disabled user - Given As an "admin" - And user "user0" exists - And assure user "user0" is disabled - And As an "user0" - When sending "GET" to "/cloud/capabilities" - Then the OCS status code should be "997" - And the HTTP status code should be "401" - - Scenario: Making a web request with a disabled user - Given As an "admin" - And user "user0" exists - And assure user "user0" is disabled - And As an "user0" - When sending "GET" with exact url to "/index.php/apps/files" - And the HTTP status code should be "401" + Scenario: An admin cannot edit user account property scopes + Given As an "admin" + And user "brand-new-user" exists + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | phoneScope | + | value | v2-private | + Then the OCS status code should be "113" + And the HTTP status code should be "200" + + Scenario: Search by phone number + Given As an "admin" + And user "phone-user" exists + And sending "PUT" to "/cloud/users/phone-user" with + | key | phone | + | value | +49 711 / 25 24 28-90 | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then search users by phone for region "DE" with + | random-string1 | 0711 / 123 456 78 | + | random-string1 | 0711 / 252 428-90 | + | random-string2 | 0711 / 90-824 252 | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then phone matches returned are + | random-string1 | phone-user@localhost:8080 | + + Scenario: Create a group + Given As an "admin" + And group "new-group" does not exist + When sending "POST" to "/cloud/groups" with + | groupid | new-group | + | password | 123456 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And group "new-group" exists + And group "new-group" has + | displayname | new-group | + + Scenario: Create a group with custom display name + Given As an "admin" + And group "new-group" does not exist + When sending "POST" to "/cloud/groups" with + | groupid | new-group | + | password | 123456 | + | displayname | new-group-displayname | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And group "new-group" exists + And group "new-group" has + | displayname | new-group-displayname | + + Scenario: Create a group with special characters + Given As an "admin" + And group "España" does not exist + When sending "POST" to "/cloud/groups" with + | groupid | España | + | password | 123456 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And group "España" exists + And group "España" has + | displayname | España | + + Scenario: adding user to a group without sending the group + Given As an "admin" + And user "brand-new-user" exists + When sending "POST" to "/cloud/users/brand-new-user/groups" with + | groupid | | + Then the OCS status code should be "101" + And the HTTP status code should be "200" + + Scenario: adding user to a group which doesn't exist + Given As an "admin" + And user "brand-new-user" exists + And group "not-group" does not exist + When sending "POST" to "/cloud/users/brand-new-user/groups" with + | groupid | not-group | + Then the OCS status code should be "102" + And the HTTP status code should be "200" + + Scenario: adding user to a group without privileges + Given user "brand-new-user" exists + And As an "brand-new-user" + When sending "POST" to "/cloud/users/brand-new-user/groups" with + | groupid | new-group | + Then the OCS status code should be "403" + And the HTTP status code should be "200" + + Scenario: adding user to a group + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + When sending "POST" to "/cloud/users/brand-new-user/groups" with + | groupid | new-group | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: getting groups of an user + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + When sending "GET" to "/cloud/users/brand-new-user/groups" + Then groups returned are + | new-group | + And the OCS status code should be "100" + + Scenario: adding a user which doesn't exist to a group + Given As an "admin" + And user "not-user" does not exist + And group "new-group" exists + When sending "POST" to "/cloud/users/not-user/groups" with + | groupid | new-group | + Then the OCS status code should be "103" + And the HTTP status code should be "200" + + Scenario: getting a group + Given As an "admin" + And group "new-group" exists + When sending "GET" to "/cloud/groups/new-group" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: Getting all groups + Given As an "admin" + And group "new-group" exists + And group "admin" exists + When sending "GET" to "/cloud/groups" + Then groups returned are + | España | + | admin | + | hidden_group | + | new-group | + + Scenario: create a subadmin + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + When sending "POST" to "/cloud/users/brand-new-user/subadmins" with + | groupid | new-group | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: get users using a subadmin + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + And user "brand-new-user" belongs to group "new-group" + And user "brand-new-user" is subadmin of group "new-group" + And As an "brand-new-user" + When sending "GET" to "/cloud/users" + Then users returned are + | brand-new-user | + And the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: removing a user from a group which doesn't exists + Given As an "admin" + And user "brand-new-user" exists + And group "not-group" does not exist + When sending "DELETE" to "/cloud/users/brand-new-user/groups" with + | groupid | not-group | + Then the OCS status code should be "102" + + Scenario: removing a user from a group + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + And user "brand-new-user" belongs to group "new-group" + When sending "DELETE" to "/cloud/users/brand-new-user/groups" with + | groupid | new-group | + Then the OCS status code should be "100" + And user "brand-new-user" does not belong to group "new-group" + + Scenario: create a subadmin using a user which not exist + Given As an "admin" + And user "not-user" does not exist + And group "new-group" exists + When sending "POST" to "/cloud/users/not-user/subadmins" with + | groupid | new-group | + Then the OCS status code should be "101" + And the HTTP status code should be "200" + + Scenario: create a subadmin using a group which not exist + Given As an "admin" + And user "brand-new-user" exists + And group "not-group" does not exist + When sending "POST" to "/cloud/users/brand-new-user/subadmins" with + | groupid | not-group | + Then the OCS status code should be "102" + And the HTTP status code should be "200" + + Scenario: Getting subadmin groups + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + When sending "GET" to "/cloud/users/brand-new-user/subadmins" + Then subadmin groups returned are + | new-group | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: Getting subadmin groups of a user which not exist + Given As an "admin" + And user "not-user" does not exist + And group "new-group" exists + When sending "GET" to "/cloud/users/not-user/subadmins" + Then the OCS status code should be "404" + And the HTTP status code should be "200" + + Scenario: Getting subadmin users of a group + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + When sending "GET" to "/cloud/groups/new-group/subadmins" + Then subadmin users returned are + | brand-new-user | + And the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: Getting subadmin users of a group which doesn't exist + Given As an "admin" + And user "brand-new-user" exists + And group "not-group" does not exist + When sending "GET" to "/cloud/groups/not-group/subadmins" + Then the OCS status code should be "101" + And the HTTP status code should be "200" + + Scenario: Removing subadmin from a group + Given As an "admin" + And user "brand-new-user" exists + And group "new-group" exists + And user "brand-new-user" is subadmin of group "new-group" + When sending "DELETE" to "/cloud/users/brand-new-user/subadmins" with + | groupid | new-group | + And the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: Delete a user + Given As an "admin" + And user "brand-new-user" exists + When sending "DELETE" to "/cloud/users/brand-new-user" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And user "brand-new-user" does not exist + + Scenario: Delete a group + Given As an "admin" + And group "new-group" exists + When sending "DELETE" to "/cloud/groups/new-group" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And group "new-group" does not exist + + Scenario: Delete a group with special characters + Given As an "admin" + And group "España" exists + When sending "DELETE" to "/cloud/groups/España" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And group "España" does not exist + + Scenario: get enabled apps + Given As an "admin" + When sending "GET" to "/cloud/apps?filter=enabled" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And apps returned are + | cloud_federation_api | + | comments | + | contactsinteraction | + | dashboard | + | dav | + | federatedfilesharing | + | federation | + | files | + | files_reminders | + | files_sharing | + | files_trashbin | + | files_versions | + | lookup_server_connector | + | profile | + | provisioning_api | + | settings | + | sharebymail | + | systemtags | + | testing | + | theming | + | twofactor_backupcodes | + | updatenotification | + | user_ldap | + | user_status | + | viewer | + | workflowengine | + | webhook_listeners | + | weather_status | + | files_external | + | oauth2 | + + Scenario: get app info + Given As an "admin" + When sending "GET" to "/cloud/apps/files" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + + Scenario: get app info from app that does not exist + Given As an "admin" + When sending "GET" to "/cloud/apps/this_app_should_never_exist" + Then the OCS status code should be "998" + 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" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And app "testing" is enabled + + Scenario: enable an app that does not exist + Given As an "admin" + When sending "POST" to "/cloud/apps/this_app_should_never_exist" + Then the OCS status code should be "998" + 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" + And user "user1" exists + When sending "PUT" to "/cloud/users/user1/disable" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And user "user1" is disabled + + Scenario: enable an user + Given As an "admin" + And user "user1" exists + And assure user "user1" is disabled + When sending "PUT" to "/cloud/users/user1/enable" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And user "user1" is enabled + + Scenario: Subadmin should be able to enable or disable an user in their group + Given As an "admin" + And user "subadmin" exists + And user "user1" exists + And group "new-group" exists + And user "subadmin" belongs to group "new-group" + And user "user1" belongs to group "new-group" + And Assure user "subadmin" is subadmin of group "new-group" + And As an "subadmin" + When sending "PUT" to "/cloud/users/user1/disable" + Then the OCS status code should be "100" + Then the HTTP status code should be "200" + And As an "admin" + And user "user1" is disabled + + Scenario: Subadmin should not be able to enable or disable an user not in their group + Given As an "admin" + And user "subadmin" exists + And user "user1" exists + And group "new-group" exists + And group "another-group" exists + And user "subadmin" belongs to group "new-group" + And user "user1" belongs to group "another-group" + And Assure user "subadmin" is subadmin of group "new-group" + And As an "subadmin" + When sending "PUT" to "/cloud/users/user1/disable" + Then the OCS status code should be "998" + Then the HTTP status code should be "200" + And As an "admin" + And user "user1" is enabled + + Scenario: Subadmins should not be able to disable users that have admin permissions in their group + Given As an "admin" + And user "another-admin" exists + And user "subadmin" exists + And group "new-group" exists + And user "another-admin" belongs to group "admin" + And user "subadmin" belongs to group "new-group" + And user "another-admin" belongs to group "new-group" + And Assure user "subadmin" is subadmin of group "new-group" + And As an "subadmin" + When sending "PUT" to "/cloud/users/another-admin/disable" + Then the OCS status code should be "998" + Then the HTTP status code should be "200" + And As an "admin" + And user "another-admin" is enabled + + Scenario: Admin can disable another admin user + Given As an "admin" + And user "another-admin" exists + And user "another-admin" belongs to group "admin" + When sending "PUT" to "/cloud/users/another-admin/disable" + Then the OCS status code should be "100" + Then the HTTP status code should be "200" + And user "another-admin" is disabled + + Scenario: Admin can enable another admin user + Given As an "admin" + And user "another-admin" exists + And user "another-admin" belongs to group "admin" + And assure user "another-admin" is disabled + When sending "PUT" to "/cloud/users/another-admin/enable" + Then the OCS status code should be "100" + Then the HTTP status code should be "200" + And user "another-admin" is enabled + + Scenario: Admin can disable subadmins in the same group + Given As an "admin" + And user "subadmin" exists + And group "new-group" exists + And user "subadmin" belongs to group "new-group" + And user "admin" belongs to group "new-group" + And Assure user "subadmin" is subadmin of group "new-group" + When sending "PUT" to "/cloud/users/subadmin/disable" + Then the OCS status code should be "100" + Then the HTTP status code should be "200" + And user "subadmin" is disabled + + Scenario: Admin can enable subadmins in the same group + Given As an "admin" + And user "subadmin" exists + And group "new-group" exists + And user "subadmin" belongs to group "new-group" + And user "admin" belongs to group "new-group" + And Assure user "subadmin" is subadmin of group "new-group" + And assure user "another-admin" is disabled + When sending "PUT" to "/cloud/users/subadmin/disable" + Then the OCS status code should be "100" + Then the HTTP status code should be "200" + And user "subadmin" is disabled + + Scenario: Admin user cannot disable himself + Given As an "admin" + And user "another-admin" exists + And user "another-admin" belongs to group "admin" + And As an "another-admin" + When sending "PUT" to "/cloud/users/another-admin/disable" + Then the OCS status code should be "101" + And the HTTP status code should be "200" + And As an "admin" + And user "another-admin" is enabled + + Scenario:Admin user cannot enable himself + Given As an "admin" + And user "another-admin" exists + And user "another-admin" belongs to group "admin" + And assure user "another-admin" is disabled + And As an "another-admin" + When sending "PUT" to "/cloud/users/another-admin/enable" + And As an "admin" + Then user "another-admin" is disabled + + Scenario: disable an user with a regular user + Given As an "admin" + And user "user1" exists + And user "user2" exists + And As an "user1" + When sending "PUT" to "/cloud/users/user2/disable" + Then the OCS status code should be "403" + And the HTTP status code should be "200" + And As an "admin" + And user "user2" is enabled + + Scenario: enable an user with a regular user + Given As an "admin" + And user "user1" exists + And user "user2" exists + And assure user "user2" is disabled + And As an "user1" + When sending "PUT" to "/cloud/users/user2/enable" + Then the OCS status code should be "403" + And the HTTP status code should be "200" + And As an "admin" + And user "user2" is disabled + + Scenario: Subadmin should not be able to disable himself + Given As an "admin" + And user "subadmin" exists + And group "new-group" exists + And user "subadmin" belongs to group "new-group" + And Assure user "subadmin" is subadmin of group "new-group" + And As an "subadmin" + When sending "PUT" to "/cloud/users/subadmin/disable" + Then the OCS status code should be "101" + Then the HTTP status code should be "200" + And As an "admin" + And user "subadmin" is enabled + + Scenario: Subadmin should not be able to enable himself + Given As an "admin" + And user "subadmin" exists + And group "new-group" exists + And user "subadmin" belongs to group "new-group" + And Assure user "subadmin" is subadmin of group "new-group" + And assure user "subadmin" is disabled + And As an "subadmin" + When sending "PUT" to "/cloud/users/subadmin/enabled" + And As an "admin" + And user "subadmin" is disabled + + Scenario: Making a ocs request with an enabled user + Given As an "admin" + And user "user0" exists + And As an "user0" + When sending "GET" to "/cloud/capabilities" + Then the HTTP status code should be "200" + And the OCS status code should be "100" + + Scenario: Making a web request with an enabled user + Given As an "admin" + And user "user0" exists + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/files" + Then the HTTP status code should be "200" + + Scenario: Making a ocs request with a disabled user + Given As an "admin" + And user "user0" exists + And assure user "user0" is disabled + And As an "user0" + When sending "GET" to "/cloud/capabilities" + Then the OCS status code should be "997" + And the HTTP status code should be "401" + + Scenario: Making a web request with a disabled user + Given As an "admin" + And user "user0" exists + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/files" + And the HTTP status code should be "401" 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" |