aboutsummaryrefslogtreecommitdiffstats
path: root/build/integration/features/bootstrap
diff options
context:
space:
mode:
Diffstat (limited to 'build/integration/features/bootstrap')
-rw-r--r--build/integration/features/bootstrap/Activity.php32
-rw-r--r--build/integration/features/bootstrap/AppConfiguration.php35
-rw-r--r--build/integration/features/bootstrap/Auth.php256
-rw-r--r--build/integration/features/bootstrap/Avatar.php262
-rw-r--r--build/integration/features/bootstrap/BasicStructure.php404
-rw-r--r--build/integration/features/bootstrap/CalDavContext.php278
-rw-r--r--build/integration/features/bootstrap/CapabilitiesContext.php47
-rw-r--r--build/integration/features/bootstrap/CardDavContext.php226
-rw-r--r--build/integration/features/bootstrap/ChecksumsContext.php86
-rw-r--r--build/integration/features/bootstrap/CollaborationContext.php206
-rw-r--r--build/integration/features/bootstrap/CommandLine.php135
-rw-r--r--build/integration/features/bootstrap/CommandLineContext.php127
-rw-r--r--build/integration/features/bootstrap/CommentsContext.php126
-rw-r--r--build/integration/features/bootstrap/ContactsMenu.php51
-rw-r--r--build/integration/features/bootstrap/ConversionsContext.php60
-rw-r--r--build/integration/features/bootstrap/DavFeatureContext.php24
-rw-r--r--build/integration/features/bootstrap/Download.php155
-rw-r--r--build/integration/features/bootstrap/ExternalStorage.php123
-rw-r--r--build/integration/features/bootstrap/FakeSMTPHelper.php163
-rw-r--r--build/integration/features/bootstrap/FeatureContext.php17
-rw-r--r--build/integration/features/bootstrap/FederationContext.php183
-rw-r--r--build/integration/features/bootstrap/FilesDropContext.php94
-rw-r--r--build/integration/features/bootstrap/LDAPContext.php198
-rw-r--r--build/integration/features/bootstrap/Mail.php39
-rw-r--r--build/integration/features/bootstrap/MetadataContext.php124
-rw-r--r--build/integration/features/bootstrap/PrincipalPropertySearchContext.php141
-rw-r--r--build/integration/features/bootstrap/Provisioning.php612
-rw-r--r--build/integration/features/bootstrap/RateLimitingContext.php31
-rw-r--r--build/integration/features/bootstrap/RemoteContext.php140
-rw-r--r--build/integration/features/bootstrap/RoutingContext.php19
-rw-r--r--build/integration/features/bootstrap/Search.php71
-rw-r--r--build/integration/features/bootstrap/SetupContext.php17
-rw-r--r--build/integration/features/bootstrap/ShareesContext.php65
-rw-r--r--build/integration/features/bootstrap/Sharing.php594
-rw-r--r--build/integration/features/bootstrap/SharingContext.php38
-rw-r--r--build/integration/features/bootstrap/TagsContext.php332
-rw-r--r--build/integration/features/bootstrap/TalkContext.php54
-rw-r--r--build/integration/features/bootstrap/Theming.php49
-rw-r--r--build/integration/features/bootstrap/Trashbin.php153
-rw-r--r--build/integration/features/bootstrap/WebDav.php1060
40 files changed, 6011 insertions, 816 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 af904a30896..e8580ed537b 100644
--- a/build/integration/features/bootstrap/AppConfiguration.php
+++ b/build/integration/features/bootstrap/AppConfiguration.php
@@ -1,8 +1,14 @@
<?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\Behat\Hook\Scope\AfterScenarioScope;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use GuzzleHttp\Message\ResponseInterface;
+use PHPUnit\Framework\Assert;
+use Psr\Http\Message\ResponseInterface;
require __DIR__ . '/../../vendor/autoload.php';
@@ -42,20 +48,37 @@ trait AppConfiguration {
$body = new \Behat\Gherkin\Node\TableNode([['value', $value]]);
$this->sendingToWith('post', "/apps/testing/api/v1/app/{$app}/{$parameter}", $body);
$this->theHTTPStatusCodeShouldBe('200');
- $this->theOCSStatusCodeShouldBe('100');
+ if ($this->apiVersion === 1) {
+ $this->theOCSStatusCodeShouldBe('100');
+ }
+ }
+
+ /**
+ * @param string $app
+ * @param string $parameter
+ * @param string $value
+ */
+ protected function deleteServerConfig($app, $parameter) {
+ $this->sendingTo('DELETE', "/apps/testing/api/v1/app/{$app}/{$parameter}");
+ $this->theHTTPStatusCodeShouldBe('200');
+ if ($this->apiVersion === 1) {
+ $this->theOCSStatusCodeShouldBe('100');
+ }
}
protected function setStatusTestingApp($enabled) {
$this->sendingTo(($enabled ? 'post' : 'delete'), '/cloud/apps/testing');
$this->theHTTPStatusCodeShouldBe('200');
- $this->theOCSStatusCodeShouldBe('100');
+ if ($this->apiVersion === 1) {
+ $this->theOCSStatusCodeShouldBe('100');
+ }
$this->sendingTo('get', '/cloud/apps?filter=enabled');
$this->theHTTPStatusCodeShouldBe('200');
if ($enabled) {
- PHPUnit_Framework_Assert::assertContains('testing', $this->response->getBody()->getContents());
+ Assert::assertStringContainsString('testing', $this->response->getBody()->getContents());
} else {
- PHPUnit_Framework_Assert::assertNotContains('testing', $this->response->getBody()->getContents());
+ Assert::assertStringNotContainsString('testing', $this->response->getBody()->getContents());
}
}
@@ -68,7 +91,7 @@ trait AppConfiguration {
* reset the configs before each scenario
* @param BeforeScenarioScope $event
*/
- public function prepareParameters(BeforeScenarioScope $event){
+ public function prepareParameters(BeforeScenarioScope $event) {
$user = $this->currentUser;
$this->currentUser = 'admin';
diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php
new file mode 100644
index 00000000000..aeaade85383
--- /dev/null
+++ b/build/integration/features/bootstrap/Auth.php
@@ -0,0 +1,256 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use GuzzleHttp\Client;
+use GuzzleHttp\Cookie\CookieJar;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\ServerException;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+trait Auth {
+ /** @var string */
+ private $unrestrictedClientToken;
+ /** @var string */
+ private $restrictedClientToken;
+ /** @var Client */
+ private $client;
+ /** @var string */
+ private $responseXml;
+
+ /** @BeforeScenario */
+ public function setUpScenario() {
+ $this->client = new Client();
+ $this->responseXml = '';
+ $this->cookieJar = new CookieJar();
+ }
+
+ /**
+ * @When requesting :url with :method
+ */
+ public function requestingWith($url, $method) {
+ $this->sendRequest($url, $method);
+ }
+
+ private function sendRequest($url, $method, $authHeader = null, $useCookies = false) {
+ $fullUrl = substr($this->baseUrl, 0, -5) . $url;
+ try {
+ if ($useCookies) {
+ $options = [
+ 'cookies' => $this->cookieJar,
+ ];
+ } else {
+ $options = [];
+ }
+ if ($authHeader) {
+ $options['headers'] = [
+ 'Authorization' => $authHeader
+ ];
+ }
+ $options['headers']['OCS_APIREQUEST'] = 'true';
+ $options['headers']['requesttoken'] = $this->requestToken;
+ $this->response = $this->client->request($method, $fullUrl, $options);
+ } catch (ClientException $ex) {
+ $this->response = $ex->getResponse();
+ } catch (ServerException $ex) {
+ $this->response = $ex->getResponse();
+ }
+ }
+
+ /**
+ * @When the CSRF token is extracted from the previous response
+ */
+ public function theCsrfTokenIsExtractedFromThePreviousResponse() {
+ $this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $this->response->getBody()->getContents()), 0, 89);
+ }
+
+ /**
+ * @param bool $loginViaWeb
+ * @return object
+ */
+ private function createClientToken($loginViaWeb = true) {
+ if ($loginViaWeb) {
+ $this->loggingInUsingWebAs('user0');
+ }
+
+ $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens';
+ $client = new Client();
+ $options = [
+ 'auth' => [
+ 'user0',
+ $loginViaWeb ? '123456' : $this->restrictedClientToken,
+ ],
+ 'form_params' => [
+ 'requesttoken' => $this->requestToken,
+ 'name' => md5(microtime()),
+ ],
+ 'cookies' => $this->cookieJar,
+ ];
+
+ try {
+ $this->response = $client->request('POST', $fullUrl, $options);
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ $this->response = $e->getResponse();
+ }
+ return json_decode($this->response->getBody()->getContents());
+ }
+
+ /**
+ * @Given a new restricted client token is added
+ */
+ public function aNewRestrictedClientTokenIsAdded() {
+ $tokenObj = $this->createClientToken();
+ $newCreatedTokenId = $tokenObj->deviceToken->id;
+ $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens/' . $newCreatedTokenId;
+ $client = new Client();
+ $options = [
+ 'auth' => ['user0', '123456'],
+ 'headers' => [
+ 'requesttoken' => $this->requestToken,
+ ],
+ 'json' => [
+ 'name' => md5(microtime()),
+ 'scope' => [
+ 'filesystem' => false,
+ ],
+ ],
+ 'cookies' => $this->cookieJar,
+ ];
+ $this->response = $client->request('PUT', $fullUrl, $options);
+ $this->restrictedClientToken = $tokenObj->token;
+ }
+
+ /**
+ * @Given a new unrestricted client token is added
+ */
+ public function aNewUnrestrictedClientTokenIsAdded() {
+ $this->unrestrictedClientToken = $this->createClientToken()->token;
+ }
+
+ /**
+ * @When a new unrestricted client token is added using restricted basic token auth
+ */
+ public function aNewUnrestrictedClientTokenIsAddedUsingRestrictedBasicTokenAuth() {
+ $this->createClientToken(false);
+ }
+
+ /**
+ * @When requesting :url with :method using basic auth
+ *
+ * @param string $url
+ * @param string $method
+ */
+ public function requestingWithBasicAuth($url, $method) {
+ $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:123456'));
+ }
+
+ /**
+ * @When requesting :url with :method using unrestricted basic token auth
+ *
+ * @param string $url
+ * @param string $method
+ */
+ public function requestingWithUnrestrictedBasicTokenAuth($url, $method) {
+ $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:' . $this->unrestrictedClientToken), true);
+ }
+
+ /**
+ * @When requesting :url with :method using restricted basic token auth
+ *
+ * @param string $url
+ * @param string $method
+ */
+ public function requestingWithRestrictedBasicTokenAuth($url, $method) {
+ $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:' . $this->restrictedClientToken), true);
+ }
+
+ /**
+ * @When requesting :url with :method using an unrestricted client token
+ *
+ * @param string $url
+ * @param string $method
+ */
+ public function requestingWithUsingAnUnrestrictedClientToken($url, $method) {
+ $this->sendRequest($url, $method, 'Bearer ' . $this->unrestrictedClientToken);
+ }
+
+ /**
+ * @When requesting :url with :method using a restricted client token
+ *
+ * @param string $url
+ * @param string $method
+ */
+ public function requestingWithUsingARestrictedClientToken($url, $method) {
+ $this->sendRequest($url, $method, 'Bearer ' . $this->restrictedClientToken);
+ }
+
+ /**
+ * @When requesting :url with :method using browser session
+ *
+ * @param string $url
+ * @param string $method
+ */
+ public function requestingWithBrowserSession($url, $method) {
+ $this->sendRequest($url, $method, null, true);
+ }
+
+ /**
+ * @Given a new browser session is started
+ *
+ * @param bool $remember
+ */
+ public function aNewBrowserSessionIsStarted($remember = false) {
+ $baseUrl = substr($this->baseUrl, 0, -5);
+ $loginUrl = $baseUrl . '/login';
+ // Request a new session and extract CSRF token
+ $client = new Client();
+ $response = $client->get($loginUrl, [
+ 'cookies' => $this->cookieJar,
+ ]);
+ $this->extracRequestTokenFromResponse($response);
+
+ // Login and extract new token
+ $client = new Client();
+ $response = $client->post(
+ $loginUrl, [
+ 'form_params' => [
+ 'user' => 'user0',
+ 'password' => '123456',
+ 'remember_login' => $remember ? '1' : '0',
+ 'requesttoken' => $this->requestToken,
+ ],
+ 'cookies' => $this->cookieJar,
+ 'headers' => [
+ 'Origin' => $baseUrl,
+ ],
+ ]
+ );
+ $this->extracRequestTokenFromResponse($response);
+ }
+
+ /**
+ * @Given a new remembered browser session is started
+ */
+ public function aNewRememberedBrowserSessionIsStarted() {
+ $this->aNewBrowserSessionIsStarted(true);
+ }
+
+
+ /**
+ * @Given the cookie jar is reset
+ */
+ public function theCookieJarIsReset() {
+ $this->cookieJar = new CookieJar();
+ }
+
+ /**
+ * @When the session cookie expires
+ */
+ public function whenTheSessionCookieExpires() {
+ $this->cookieJar->clearSessionCookies();
+ }
+}
diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php
new file mode 100644
index 00000000000..beebf1c024a
--- /dev/null
+++ b/build/integration/features/bootstrap/Avatar.php
@@ -0,0 +1,262 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 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 Avatar {
+ /** @var string * */
+ private $lastAvatar;
+
+ /** @AfterScenario **/
+ public function cleanupLastAvatar() {
+ $this->lastAvatar = null;
+ }
+
+ private function getLastAvatar() {
+ $this->lastAvatar = '';
+
+ $body = $this->response->getBody();
+ while (!$body->eof()) {
+ $this->lastAvatar .= $body->read(8192);
+ }
+ $body->close();
+ }
+
+ /**
+ * @When user :user gets avatar for user :userAvatar
+ *
+ * @param string $user
+ * @param string $userAvatar
+ */
+ public function userGetsAvatarForUser(string $user, string $userAvatar) {
+ $this->userGetsAvatarForUserWithSize($user, $userAvatar, '128');
+ }
+
+ /**
+ * @When user :user gets avatar for user :userAvatar with size :size
+ *
+ * @param string $user
+ * @param string $userAvatar
+ * @param string $size
+ */
+ public function userGetsAvatarForUserWithSize(string $user, string $userAvatar, string $size) {
+ $this->asAn($user);
+ $this->sendingToDirectUrl('GET', '/index.php/avatar/' . $userAvatar . '/' . $size);
+ $this->theHTTPStatusCodeShouldBe('200');
+
+ $this->getLastAvatar();
+ }
+
+ /**
+ * @When user :user gets avatar for guest :guestAvatar
+ *
+ * @param string $user
+ * @param string $guestAvatar
+ */
+ public function userGetsAvatarForGuest(string $user, string $guestAvatar) {
+ $this->asAn($user);
+ $this->sendingToDirectUrl('GET', '/index.php/avatar/guest/' . $guestAvatar . '/128');
+ $this->theHTTPStatusCodeShouldBe('201');
+
+ $this->getLastAvatar();
+ }
+
+ /**
+ * @When logged in user gets temporary avatar
+ */
+ public function loggedInUserGetsTemporaryAvatar() {
+ $this->loggedInUserGetsTemporaryAvatarWith('200');
+ }
+
+ /**
+ * @When logged in user gets temporary avatar with :statusCode
+ *
+ * @param string $statusCode
+ */
+ public function loggedInUserGetsTemporaryAvatarWith(string $statusCode) {
+ $this->sendingAToWithRequesttoken('GET', '/index.php/avatar/tmp');
+ $this->theHTTPStatusCodeShouldBe($statusCode);
+
+ $this->getLastAvatar();
+ }
+
+ /**
+ * @When logged in user posts temporary avatar from file :source
+ *
+ * @param string $source
+ */
+ public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
+ $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
+
+ $this->sendingAToWithRequesttoken('POST', '/index.php/avatar',
+ [
+ 'multipart' => [
+ [
+ 'name' => 'files[]',
+ 'contents' => $file
+ ]
+ ]
+ ]);
+ $this->theHTTPStatusCodeShouldBe('200');
+ }
+
+ /**
+ * @When logged in user posts temporary avatar from internal path :path
+ *
+ * @param string $path
+ */
+ public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) {
+ $this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path);
+ $this->theHTTPStatusCodeShouldBe('200');
+ }
+
+ /**
+ * @When logged in user crops temporary avatar
+ *
+ * @param TableNode $crop
+ */
+ public function loggedInUserCropsTemporaryAvatar(TableNode $crop) {
+ $this->loggedInUserCropsTemporaryAvatarWith('200', $crop);
+ }
+
+ /**
+ * @When logged in user crops temporary avatar with :statusCode
+ *
+ * @param string $statusCode
+ * @param TableNode $crop
+ */
+ public function loggedInUserCropsTemporaryAvatarWith(string $statusCode, TableNode $crop) {
+ $parameters = [];
+ foreach ($crop->getRowsHash() as $key => $value) {
+ $parameters[] = 'crop[' . $key . ']=' . $value;
+ }
+
+ $this->sendingAToWithRequesttoken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters));
+ $this->theHTTPStatusCodeShouldBe($statusCode);
+ }
+
+ /**
+ * @When logged in user deletes the user avatar
+ */
+ public function loggedInUserDeletesTheUserAvatar() {
+ $this->sendingAToWithRequesttoken('DELETE', '/index.php/avatar');
+ $this->theHTTPStatusCodeShouldBe('200');
+ }
+
+ /**
+ * @Then last avatar is a square of size :size
+ *
+ * @param string size
+ */
+ public function lastAvatarIsASquareOfSize(string $size) {
+ [$width, $height] = getimagesizefromstring($this->lastAvatar);
+
+ Assert::assertEquals($width, $height, 'Expected avatar to be a square');
+ Assert::assertEquals($size, $width);
+ }
+
+ /**
+ * @Then last avatar is not a square
+ */
+ public function lastAvatarIsNotASquare() {
+ [$width, $height] = getimagesizefromstring($this->lastAvatar);
+
+ Assert::assertNotEquals($width, $height, 'Expected avatar to not be a square');
+ }
+
+ /**
+ * @Then last avatar is not a single color
+ */
+ public function lastAvatarIsNotASingleColor() {
+ Assert::assertEquals(null, $this->getColorFromLastAvatar());
+ }
+
+ /**
+ * @Then last avatar is a single :color color
+ *
+ * @param string $color
+ * @param string $size
+ */
+ public function lastAvatarIsASingleColor(string $color) {
+ $expectedColor = $this->hexStringToRgbColor($color);
+ $colorFromLastAvatar = $this->getColorFromLastAvatar();
+
+ Assert::assertTrue($this->isSameColor($expectedColor, $colorFromLastAvatar),
+ $this->rgbColorToHexString($colorFromLastAvatar) . ' does not match expected ' . $color);
+ }
+
+ private function hexStringToRgbColor($hexString) {
+ // Strip initial "#"
+ $hexString = substr($hexString, 1);
+
+ $rgbColorInt = hexdec($hexString);
+
+ // RGBA hex strings are not supported; the given string is assumed to be
+ // an RGB hex string.
+ return [
+ 'red' => ($rgbColorInt >> 16) & 0xFF,
+ 'green' => ($rgbColorInt >> 8) & 0xFF,
+ 'blue' => $rgbColorInt & 0xFF,
+ 'alpha' => 0
+ ];
+ }
+
+ private function rgbColorToHexString($rgbColor) {
+ $rgbColorInt = ($rgbColor['red'] << 16) + ($rgbColor['green'] << 8) + ($rgbColor['blue']);
+
+ return '#' . str_pad(strtoupper(dechex($rgbColorInt)), 6, '0', STR_PAD_LEFT);
+ }
+
+ private function getColorFromLastAvatar() {
+ $image = imagecreatefromstring($this->lastAvatar);
+
+ $firstPixelColorIndex = imagecolorat($image, 0, 0);
+ $firstPixelColor = imagecolorsforindex($image, $firstPixelColorIndex);
+
+ for ($i = 0; $i < imagesx($image); $i++) {
+ for ($j = 0; $j < imagesx($image); $j++) {
+ $currentPixelColorIndex = imagecolorat($image, $i, $j);
+ $currentPixelColor = imagecolorsforindex($image, $currentPixelColorIndex);
+
+ // The colors are compared with a small allowed delta, as even
+ // on solid color images the resizing can cause some small
+ // artifacts that slightly modify the color of certain pixels.
+ if (!$this->isSameColor($firstPixelColor, $currentPixelColor)) {
+ imagedestroy($image);
+
+ return null;
+ }
+ }
+ }
+
+ imagedestroy($image);
+
+ return $firstPixelColor;
+ }
+
+ 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)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) {
+ if ($firstColorComponent >= ($secondColorComponent - $allowedDelta)
+ && $firstColorComponent <= ($secondColorComponent + $allowedDelta)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php
index d2aed82055a..59a4312913e 100644
--- a/build/integration/features/bootstrap/BasicStructure.php
+++ b/build/integration/features/bootstrap/BasicStructure.php
@@ -1,11 +1,27 @@
<?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 GuzzleHttp\Cookie\CookieJar;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\ServerException;
+use PHPUnit\Framework\Assert;
+use Psr\Http\Message\ResponseInterface;
require __DIR__ . '/../../vendor/autoload.php';
trait BasicStructure {
+ use Auth;
+ use Avatar;
+ use Download;
+ use Mail;
+ use Theming;
+
/** @var string */
private $currentUser = '';
@@ -21,22 +37,26 @@ trait BasicStructure {
/** @var ResponseInterface */
private $response = null;
- /** @var \GuzzleHttp\Cookie\CookieJar */
+ /** @var CookieJar */
private $cookieJar;
/** @var string */
- private $requesttoken;
+ private $requestToken;
- public function __construct($baseUrl, $admin, $regular_user_password) {
+ protected $adminUser;
+ protected $regularUser;
+ protected $localBaseUrl;
+ protected $remoteBaseUrl;
+ public function __construct($baseUrl, $admin, $regular_user_password) {
// Initialize your context here
$this->baseUrl = $baseUrl;
$this->adminUser = $admin;
$this->regularUser = $regular_user_password;
- $this->localBaseUrl = substr($this->baseUrl, 0, -4);
- $this->remoteBaseUrl = substr($this->baseUrl, 0, -4);
+ $this->localBaseUrl = $this->baseUrl;
+ $this->remoteBaseUrl = $this->baseUrl;
$this->currentServer = 'LOCAL';
- $this->cookieJar = new \GuzzleHttp\Cookie\CookieJar();
+ $this->cookieJar = new CookieJar();
// in case of ci deployment we take the server url from the environment
$testServerUrl = getenv('TEST_SERVER_URL');
@@ -53,11 +73,11 @@ trait BasicStructure {
}
/**
- * @Given /^using api version "([^"]*)"$/
+ * @Given /^using api version "(\d+)"$/
* @param string $version
*/
public function usingApiVersion($version) {
- $this->apiVersion = $version;
+ $this->apiVersion = (int)$version;
}
/**
@@ -75,7 +95,7 @@ trait BasicStructure {
*/
public function usingServer($server) {
$previousServer = $this->currentServer;
- if ($server === 'LOCAL'){
+ if ($server === 'LOCAL') {
$this->baseUrl = $this->localBaseUrl;
$this->currentServer = 'LOCAL';
return $previousServer;
@@ -98,20 +118,28 @@ trait BasicStructure {
/**
* Parses the xml answer to get ocs response which doesn't match with
* http one in v1 of the api.
+ *
* @param ResponseInterface $response
* @return string
*/
public function getOCSResponse($response) {
- return $response->xml()->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;
}
/**
* This function is needed to use a vertical fashion in the gherkin tables.
+ *
* @param array $arrayOfArrays
* @return array
*/
- public function simplifyArray($arrayOfArrays){
- $a = array_map(function($subArray) { return $subArray[0]; }, $arrayOfArrays);
+ public function simplifyArray($arrayOfArrays) {
+ $a = array_map(function ($subArray) {
+ return $subArray[0];
+ }, $arrayOfArrays);
return $a;
}
@@ -119,7 +147,7 @@ trait BasicStructure {
* @When /^sending "([^"]*)" to "([^"]*)" with$/
* @param string $verb
* @param string $url
- * @param \Behat\Gherkin\Node\TableNode $body
+ * @param TableNode $body
*/
public function sendingToWith($verb, $url, $body) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url;
@@ -127,25 +155,101 @@ trait BasicStructure {
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
- } else {
+ } elseif (strpos($this->currentUser, 'anonymous') !== 0) {
$options['auth'] = [$this->currentUser, $this->regularUser];
}
- if ($body instanceof \Behat\Gherkin\Node\TableNode) {
+ $options['headers'] = [
+ 'OCS-APIRequest' => 'true'
+ ];
+ if ($body instanceof TableNode) {
$fd = $body->getRowsHash();
- $options['body'] = $fd;
+ $options['form_params'] = $fd;
+ }
+
+ // TODO: Fix this hack!
+ if ($verb === 'PUT' && $body === null) {
+ $options['form_params'] = [
+ 'foo' => 'bar',
+ ];
}
try {
- $this->response = $client->send($client->createRequest($verb, $fullUrl, $options));
- } catch (\GuzzleHttp\Exception\ClientException $ex) {
+ $this->response = $client->request($verb, $fullUrl, $options);
+ } catch (ClientException $ex) {
+ $this->response = $ex->getResponse();
+ } catch (ServerException $ex) {
$this->response = $ex->getResponse();
}
}
- public function isExpectedUrl($possibleUrl, $finalPart){
+ /**
+ * @param string $verb
+ * @param string $url
+ * @param TableNode|array|null $body
+ * @param array $headers
+ */
+ protected function sendRequestForJSON(string $verb, string $url, $body = null, array $headers = []): void {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url;
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = ['admin', 'admin'];
+ } elseif (strpos($this->currentUser, 'anonymous') !== 0) {
+ $options['auth'] = [$this->currentUser, $this->regularUser];
+ }
+ if ($body instanceof TableNode) {
+ $fd = $body->getRowsHash();
+ $options['form_params'] = $fd;
+ } elseif (is_array($body)) {
+ $options['form_params'] = $body;
+ }
+
+ $options['headers'] = array_merge($headers, [
+ 'OCS-ApiRequest' => 'true',
+ 'Accept' => 'application/json',
+ ]);
+
+ try {
+ $this->response = $client->{$verb}($fullUrl, $options);
+ } catch (ClientException $ex) {
+ $this->response = $ex->getResponse();
+ }
+ }
+
+ /**
+ * @When /^sending "([^"]*)" with exact url to "([^"]*)"$/
+ * @param string $verb
+ * @param string $url
+ */
+ public function sendingToDirectUrl($verb, $url) {
+ $this->sendingToWithDirectUrl($verb, $url, null);
+ }
+
+ public function sendingToWithDirectUrl($verb, $url, $body) {
+ $fullUrl = substr($this->baseUrl, 0, -5) . $url;
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } elseif (strpos($this->currentUser, 'anonymous') !== 0) {
+ $options['auth'] = [$this->currentUser, $this->regularUser];
+ }
+ if ($body instanceof TableNode) {
+ $fd = $body->getRowsHash();
+ $options['form_params'] = $fd;
+ }
+
+ try {
+ $this->response = $client->request($verb, $fullUrl, $options);
+ } catch (ClientException $ex) {
+ $this->response = $ex->getResponse();
+ }
+ }
+
+ public function isExpectedUrl($possibleUrl, $finalPart) {
$baseUrlChopped = substr($this->baseUrl, 0, -4);
$endCharacter = strlen($baseUrlChopped) + strlen($finalPart);
- return (substr($possibleUrl,0,$endCharacter) == "$baseUrlChopped" . "$finalPart");
+ return (substr($possibleUrl, 0, $endCharacter) == "$baseUrlChopped" . "$finalPart");
}
/**
@@ -153,7 +257,7 @@ trait BasicStructure {
* @param int $statusCode
*/
public function theOCSStatusCodeShouldBe($statusCode) {
- PHPUnit_Framework_Assert::assertEquals($statusCode, $this->getOCSResponse($this->response));
+ Assert::assertEquals($statusCode, $this->getOCSResponse($this->response));
}
/**
@@ -161,14 +265,22 @@ trait BasicStructure {
* @param int $statusCode
*/
public function theHTTPStatusCodeShouldBe($statusCode) {
- PHPUnit_Framework_Assert::assertEquals($statusCode, $this->response->getStatusCode());
+ Assert::assertEquals($statusCode, $this->response->getStatusCode());
+ }
+
+ /**
+ * @Then /^the Content-Type should be "([^"]*)"$/
+ * @param string $contentType
+ */
+ public function theContentTypeShouldbe($contentType) {
+ Assert::assertEquals($contentType, $this->response->getHeader('Content-Type')[0]);
}
/**
* @param ResponseInterface $response
*/
private function extracRequestTokenFromResponse(ResponseInterface $response) {
- $this->requesttoken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89);
+ $this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89);
}
/**
@@ -176,7 +288,8 @@ trait BasicStructure {
* @param string $user
*/
public function loggingInUsingWebAs($user) {
- $loginUrl = substr($this->baseUrl, 0, -5);
+ $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(
@@ -193,12 +306,15 @@ trait BasicStructure {
$response = $client->post(
$loginUrl,
[
- 'body' => [
+ 'form_params' => [
'user' => $user,
'password' => $password,
- 'requesttoken' => $this->requesttoken,
+ 'requesttoken' => $this->requestToken,
],
'cookies' => $this->cookieJar,
+ 'headers' => [
+ 'Origin' => $baseUrl,
+ ],
]
);
$this->extracRequestTokenFromResponse($response);
@@ -208,22 +324,33 @@ trait BasicStructure {
* @When Sending a :method to :url with requesttoken
* @param string $method
* @param string $url
+ * @param TableNode|array|null $body
*/
- public function sendingAToWithRequesttoken($method, $url) {
+ public function sendingAToWithRequesttoken($method, $url, $body = null) {
$baseUrl = substr($this->baseUrl, 0, -5);
+ $options = [
+ 'cookies' => $this->cookieJar,
+ 'headers' => [
+ 'requesttoken' => $this->requestToken
+ ],
+ ];
+
+ if ($body instanceof TableNode) {
+ $fd = $body->getRowsHash();
+ $options['form_params'] = $fd;
+ } elseif ($body) {
+ $options = array_merge_recursive($options, $body);
+ }
+
$client = new Client();
- $request = $client->createRequest(
- $method,
- $baseUrl . $url,
- [
- 'cookies' => $this->cookieJar,
- ]
- );
- $request->addHeader('requesttoken', $this->requesttoken);
try {
- $this->response = $client->send($request);
- } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $client->request(
+ $method,
+ $baseUrl . $url,
+ $options
+ );
+ } catch (ClientException $e) {
$this->response = $e->getResponse();
}
}
@@ -237,64 +364,199 @@ trait BasicStructure {
$baseUrl = substr($this->baseUrl, 0, -5);
$client = new Client();
- $request = $client->createRequest(
- $method,
- $baseUrl . $url,
- [
- 'cookies' => $this->cookieJar,
- ]
- );
try {
- $this->response = $client->send($request);
- } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $client->request(
+ $method,
+ $baseUrl . $url,
+ [
+ 'cookies' => $this->cookieJar
+ ]
+ );
+ } catch (ClientException $e) {
$this->response = $e->getResponse();
}
}
- public static function removeFile($path, $filename){
+ public static function removeFile($path, $filename) {
if (file_exists("$path" . "$filename")) {
unlink("$path" . "$filename");
}
}
/**
+ * @Given User :user modifies text of :filename with text :text
+ * @param string $user
+ * @param string $filename
+ * @param string $text
+ */
+ public function modifyTextOfFile($user, $filename, $text) {
+ self::removeFile($this->getDataDirectory() . "/$user/files", "$filename");
+ file_put_contents($this->getDataDirectory() . "/$user/files" . "$filename", "$text");
+ }
+
+ private function getDataDirectory() {
+ // Based on "runOcc" from CommandLine trait
+ $args = ['config:system:get', 'datadirectory'];
+ $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);
+
+ return trim($lastStdOut);
+ }
+
+ /**
+ * @Given file :filename is created :times times in :user user data
+ * @param string $filename
+ * @param string $times
+ * @param string $user
+ */
+ public function fileIsCreatedTimesInUserData($filename, $times, $user) {
+ for ($i = 0; $i < $times; $i++) {
+ file_put_contents($this->getDataDirectory() . "/$user/files" . "$filename-$i", "content-$i");
+ }
+ }
+
+ public function createFileSpecificSize($name, $size) {
+ $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');
+ fwrite($file, $text);
+ fclose($file);
+ }
+
+ /**
+ * @Given file :filename of size :size is created in local storage
+ * @param string $filename
+ * @param string $size
+ */
+ public function fileIsCreatedInLocalStorageWithSize($filename, $size) {
+ $this->createFileSpecificSize("local_storage/$filename", $size);
+ }
+
+ /**
+ * @Given file :filename with text :text is created in local storage
+ * @param string $filename
+ * @param string $text
+ */
+ public function fileIsCreatedInLocalStorageWithText($filename, $text) {
+ $this->createFileWithText("local_storage/$filename", $text);
+ }
+
+ /**
+ * @When Sleep for :seconds seconds
+ * @param int $seconds
+ */
+ public function sleepForSeconds($seconds) {
+ sleep((int)$seconds);
+ }
+
+ /**
* @BeforeSuite
*/
- public static function addFilesToSkeleton(){
- for ($i=0; $i<5; $i++){
- file_put_contents("../../core/skeleton/" . "textfile" . "$i" . ".txt", "ownCloud test text file\n");
+ public static function addFilesToSkeleton() {
+ for ($i = 0; $i < 5; $i++) {
+ 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", "ownCloud 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", "ownCloud test text file\n");
+ file_put_contents('../../core/skeleton/PARENT/CHILD/' . 'child.txt', "Nextcloud test text file\n");
}
/**
* @AfterSuite
*/
- public static function removeFilesFromSkeleton(){
- for ($i=0; $i<5; $i++){
- self::removeFile("../../core/skeleton/", "textfile" . "$i" . ".txt");
+ public static function removeFilesFromSkeleton() {
+ for ($i = 0; $i < 5; $i++) {
+ 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');
+ }
+ }
+
+ /**
+ * @BeforeScenario @local_storage
+ */
+ public static function removeFilesFromLocalStorageBefore() {
+ $dir = './work/local_storage/';
+ $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
+ $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
+ foreach ($ri as $file) {
+ $file->isDir() ? rmdir($file) : unlink($file);
}
}
-}
+ /**
+ * @AfterScenario @local_storage
+ */
+ public static function removeFilesFromLocalStorageAfter() {
+ $dir = './work/local_storage/';
+ $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
+ $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
+ foreach ($ri as $file) {
+ $file->isDir() ? rmdir($file) : unlink($file);
+ }
+ }
+
+ /**
+ * @Given /^cookies are reset$/
+ */
+ public function cookiesAreReset() {
+ $this->cookieJar = new CookieJar();
+ }
+
+ /**
+ * @Then The following headers should be set
+ * @param TableNode $table
+ * @throws \Exception
+ */
+ public function theFollowingHeadersShouldBeSet(TableNode $table) {
+ foreach ($table->getTable() as $header) {
+ $headerName = $header[0];
+ $expectedHeaderValue = $header[1];
+ $returnedHeader = $this->response->getHeader($headerName)[0];
+ if ($returnedHeader !== $expectedHeaderValue) {
+ throw new \Exception(
+ sprintf(
+ "Expected value '%s' for header '%s', got '%s'",
+ $expectedHeaderValue,
+ $headerName,
+ $returnedHeader
+ )
+ );
+ }
+ }
+ }
+}
diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php
index 30c50630b3e..459c35089fa 100644
--- a/build/integration/features/bootstrap/CalDavContext.php
+++ b/build/integration/features/bootstrap/CalDavContext.php
@@ -1,31 +1,18 @@
<?php
+
/**
- * @author Lukas Reschke <lukas@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
require __DIR__ . '/../../vendor/autoload.php';
use GuzzleHttp\Client;
-use GuzzleHttp\Message\ResponseInterface;
+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;
@@ -48,14 +35,14 @@ class CalDavContext implements \Behat\Behat\Context\Context {
}
/** @BeforeScenario */
- public function tearUpScenario() {
+ public function setUpScenario() {
$this->client = new Client();
$this->responseXml = '';
}
/** @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,
@@ -64,28 +51,34 @@ class CalDavContext implements \Behat\Behat\Context\Context {
'admin',
'admin',
],
+ 'headers' => [
+ 'X-NC-CalDAV-No-Trashbin' => '1',
+ ]
]
);
- } catch (\GuzzleHttp\Exception\ClientException $e) {}
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ }
}
/**
- * @When :user requests calendar :calendar
+ * @When :user requests calendar :calendar on the endpoint :endpoint
* @param string $user
* @param string $calendar
+ * @param string $endpoint
*/
- public function requestsCalendar($user, $calendar) {
- $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$calendar;
+ public function requestsCalendar($user, $calendar, $endpoint) {
+ $davUrl = $this->baseUrl . $endpoint . $calendar;
$password = ($user === 'admin') ? 'admin' : '123456';
try {
- $this->response = $this->client->get(
+ $this->response = $this->client->request(
+ 'PROPFIND',
$davUrl,
[
'auth' => [
$user,
$password,
- ]
+ ],
]
);
} catch (\GuzzleHttp\Exception\ClientException $e) {
@@ -94,12 +87,125 @@ class CalDavContext implements \Behat\Behat\Context\Context {
}
/**
+ * @When :user requests principal :principal on the endpoint :endpoint
+ */
+ public function requestsPrincipal(string $user, string $principal, string $endpoint): void {
+ $davUrl = $this->baseUrl . $endpoint . $principal;
+
+ $password = ($user === 'admin') ? 'admin' : '123456';
+ try {
+ $this->response = $this->client->request(
+ 'PROPFIND',
+ $davUrl,
+ [
+ 'headers' => [
+ 'Content-Type' => 'application/xml; charset=UTF-8',
+ 'Depth' => 0,
+ ],
+ 'body' => '<x0:propfind xmlns:x0="DAV:"><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:principal-collection-set/><x0:supported-report-set/></x0:prop></x0:propfind>',
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ ]
+ );
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * @Then The CalDAV response should contain a property :key
+ * @throws \Exception
+ */
+ public function theCaldavResponseShouldContainAProperty(string $key): void {
+ /** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */
+ $multiStatus = $this->responseXml['value'];
+ $responses = $multiStatus->getResponses()[0]->getResponseProperties();
+ if (!isset($responses[200])) {
+ throw new \Exception(
+ sprintf(
+ 'Expected code 200 got [%s]',
+ implode(',', array_keys($responses)),
+ )
+ );
+ }
+
+ $props = $responses[200];
+ if (!array_key_exists($key, $props)) {
+ throw new \Exception(
+ sprintf(
+ 'Expected property %s in %s',
+ $key,
+ json_encode($props, JSON_PRETTY_PRINT),
+ )
+ );
+ }
+ }
+
+ /**
+ * @Then The CalDAV response should contain a property :key with a href value :value
+ * @throws \Exception
+ */
+ public function theCaldavResponseShouldContainAPropertyWithHrefValue(
+ string $key,
+ string $value,
+ ): void {
+ /** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */
+ $multiStatus = $this->responseXml['value'];
+ $responses = $multiStatus->getResponses()[0]->getResponseProperties();
+ if (!isset($responses[200])) {
+ throw new \Exception(
+ sprintf(
+ 'Expected code 200 got [%s]',
+ implode(',', array_keys($responses)),
+ )
+ );
+ }
+
+ $props = $responses[200];
+ if (!array_key_exists($key, $props)) {
+ throw new \Exception("Cannot find property \"$key\"");
+ }
+
+ $actualValue = $props[$key]->getHref();
+ if ($actualValue !== $value) {
+ throw new \Exception("Property \"$key\" found with value \"$actualValue\", expected \"$value\"");
+ }
+ }
+
+ /**
+ * @Then The CalDAV response should be multi status
+ * @throws \Exception
+ */
+ public function theCaldavResponseShouldBeMultiStatus(): void {
+ if ($this->response->getStatusCode() !== 207) {
+ throw new \Exception(
+ sprintf(
+ 'Expected code 207 got %s',
+ $this->response->getStatusCode()
+ )
+ );
+ }
+
+ $body = $this->response->getBody()->getContents();
+ if ($body && substr($body, 0, 1) === '<') {
+ $reader = new Sabre\Xml\Reader();
+ $reader->xml($body);
+ $reader->elementMap['{DAV:}multistatus'] = \Sabre\DAV\Xml\Response\MultiStatus::class;
+ $reader->elementMap['{DAV:}response'] = \Sabre\DAV\Xml\Element\Response::class;
+ $reader->elementMap['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'] = \Sabre\DAV\Xml\Property\Href::class;
+ $this->responseXml = $reader->parse();
+ }
+ }
+
+ /**
* @Then The CalDAV HTTP status code should be :code
* @param int $code
* @throws \Exception
*/
public function theCaldavHttpStatusCodeShouldBe($code) {
- if((int)$code !== $this->response->getStatusCode()) {
+ if ((int)$code !== $this->response->getStatusCode()) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -110,7 +216,7 @@ class CalDavContext implements \Behat\Behat\Context\Context {
}
$body = $this->response->getBody()->getContents();
- if($body && substr($body, 0, 1) === '<') {
+ if ($body && substr($body, 0, 1) === '<') {
$reader = new Sabre\Xml\Reader();
$reader->xml($body);
$this->responseXml = $reader->parse();
@@ -125,7 +231,7 @@ class CalDavContext implements \Behat\Behat\Context\Context {
public function theExceptionIs($message) {
$result = $this->responseXml['value'][0]['value'];
- if($message !== $result) {
+ if ($message !== $result) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -144,7 +250,7 @@ class CalDavContext implements \Behat\Behat\Context\Context {
public function theErrorMessageIs($message) {
$result = $this->responseXml['value'][1]['value'];
- if($message !== $result) {
+ if ($message !== $result) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -161,10 +267,10 @@ 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';
- $request = $this->client->createRequest(
+ $this->response = $this->client->request(
'MKCALENDAR',
$davUrl,
[
@@ -175,8 +281,110 @@ class CalDavContext implements \Behat\Behat\Context\Context {
],
]
);
+ }
+
+ /**
+ * @Then :user publicly shares the calendar named :name
+ *
+ * @param string $user
+ * @param string $name
+ */
+ public function publiclySharesTheCalendarNamed($user, $name) {
+ $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name;
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ $this->response = $this->client->request(
+ 'POST',
+ $davUrl,
+ [
+ 'body' => '<cs:publish-calendar xmlns:cs="http://calendarserver.org/ns/"/>',
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/xml; charset=UTF-8',
+ ],
+ ]
+ );
+ }
+
+ /**
+ * @Then There should be :amount calendars in the response body
+ *
+ * @param string $amount
+ */
+ public function t($amount) {
+ $jsonEncoded = json_encode($this->responseXml);
+ $arrayElement = json_decode($jsonEncoded, true);
+ $actual = count($arrayElement['value']) - 1;
+ if ($actual !== (int)$amount) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'Expected %s got %s',
+ $amount,
+ $actual
+ )
+ );
+ }
+ }
- $this->response = $this->client->send($request);
+ /**
+ * @When :user sends a create calendar request to :calendar on the endpoint :endpoint
+ */
+ public function sendsCreateCalendarRequest(string $user, string $calendar, string $endpoint) {
+ $davUrl = $this->baseUrl . $endpoint . $calendar;
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ try {
+ $this->response = $this->client->request(
+ 'MKCALENDAR',
+ $davUrl,
+ [
+ 'body' => '<c:mkcalendar xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:" xmlns:a="http://apple.com/ns/ical/" xmlns:o="http://owncloud.org/ns"><d:set><d:prop><d:displayname>test</d:displayname><o:calendar-enabled>1</o:calendar-enabled><a:calendar-color>#21213D</a:calendar-color><c:supported-calendar-component-set><c:comp name="VEVENT"/></c:supported-calendar-component-set></d:prop></d:set></c:mkcalendar>',
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ ]
+ );
+ } catch (GuzzleException $e) {
+ $this->response = $e->getResponse();
+ }
}
+ /**
+ * @Given :user updates property :key to href :value of principal :principal on the endpoint :endpoint
+ */
+ public function updatesHrefPropertyOfPrincipal(
+ string $user,
+ string $key,
+ string $value,
+ string $principal,
+ string $endpoint,
+ ): void {
+ $davUrl = $this->baseUrl . $endpoint . $principal;
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ $propPatch = new \Sabre\DAV\Xml\Request\PropPatch();
+ $propPatch->properties = [$key => new \Sabre\DAV\Xml\Property\Href($value)];
+
+ $xml = new \Sabre\Xml\Service();
+ $body = $xml->write('{DAV:}propertyupdate', $propPatch, '/');
+
+ $this->response = $this->client->request(
+ 'PROPPATCH',
+ $davUrl,
+ [
+ 'headers' => [
+ 'Content-Type' => 'application/xml; charset=UTF-8',
+ ],
+ 'body' => $body,
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ ]
+ );
+ }
}
diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php
index 91a4265504c..7d09ab6ddcf 100644
--- a/build/integration/features/bootstrap/CapabilitiesContext.php
+++ b/build/integration/features/bootstrap/CapabilitiesContext.php
@@ -1,11 +1,13 @@
<?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\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
-use Behat\Behat\Hook\Scope\AfterScenarioScope;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use GuzzleHttp\Client;
-use GuzzleHttp\Message\ResponseInterface;
+use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php';
@@ -13,7 +15,6 @@ require __DIR__ . '/../../vendor/autoload.php';
* Capabilities context.
*/
class CapabilitiesContext implements Context, SnippetAcceptingContext {
-
use BasicStructure;
use AppConfiguration;
@@ -21,35 +22,37 @@ class CapabilitiesContext implements Context, SnippetAcceptingContext {
* @Then /^fields of capabilities match with$/
* @param \Behat\Gherkin\Node\TableNode|null $formData
*/
- public function checkCapabilitiesResponse(\Behat\Gherkin\Node\TableNode $formData){
- $capabilitiesXML = $this->response->xml()->data->capabilities;
+ public function checkCapabilitiesResponse(\Behat\Gherkin\Node\TableNode $formData) {
+ $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']);
$answeredValue = $capabilitiesXML->{$row['capability']};
- for ($i = 0; $i < count($path_to_element); $i++){
+ for ($i = 0; $i < count($path_to_element); $i++) {
$answeredValue = $answeredValue->{$path_to_element[$i]};
}
$answeredValue = (string)$answeredValue;
- PHPUnit_Framework_Assert::assertEquals(
- $row['value']==="EMPTY" ? '' : $row['value'],
+ Assert::assertEquals(
+ $row['value'] === 'EMPTY' ? '' : $row['value'],
$answeredValue,
- "Failed field " . $row['capability'] . " " . $row['path_to_element']
+ 'Failed field ' . $row['capability'] . ' ' . $row['path_to_element']
);
-
}
}
protected function resetAppConfigs() {
- $this->modifyServerConfig('core', 'shareapi_enabled', 'yes');
- $this->modifyServerConfig('core', 'shareapi_allow_links', 'yes');
- $this->modifyServerConfig('core', 'shareapi_allow_public_upload', 'yes');
- $this->modifyServerConfig('core', 'shareapi_allow_resharing', 'yes');
- $this->modifyServerConfig('files_sharing', 'outgoing_server2server_share_enabled', 'yes');
- $this->modifyServerConfig('files_sharing', 'incoming_server2server_share_enabled', 'yes');
- $this->modifyServerConfig('core', 'shareapi_enforce_links_password', 'no');
- $this->modifyServerConfig('core', 'shareapi_allow_public_notification', 'no');
- $this->modifyServerConfig('core', 'shareapi_default_expire_date', 'no');
- $this->modifyServerConfig('core', 'shareapi_enforce_expire_date', 'no');
+ $this->deleteServerConfig('core', 'shareapi_enabled');
+ $this->deleteServerConfig('core', 'shareapi_allow_links');
+ $this->deleteServerConfig('core', 'shareapi_allow_public_upload');
+ $this->deleteServerConfig('core', 'shareapi_allow_resharing');
+ $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled');
+ $this->deleteServerConfig('files_sharing', 'incoming_server2server_share_enabled');
+ $this->deleteServerConfig('core', 'shareapi_enforce_links_password');
+ $this->deleteServerConfig('core', 'shareapi_allow_public_notification');
+ $this->deleteServerConfig('core', 'shareapi_default_expire_date');
+ $this->deleteServerConfig('core', 'shareapi_enforce_expire_date');
+ $this->deleteServerConfig('core', 'shareapi_allow_group_sharing');
}
}
diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php
index d317ba5193f..733c98dca02 100644
--- a/build/integration/features/bootstrap/CardDavContext.php
+++ b/build/integration/features/bootstrap/CardDavContext.php
@@ -1,31 +1,18 @@
<?php
+
/**
- * @author Lukas Reschke <lukas@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
require __DIR__ . '/../../vendor/autoload.php';
use GuzzleHttp\Client;
+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;
@@ -48,7 +35,7 @@ class CardDavContext implements \Behat\Behat\Context\Context {
}
/** @BeforeScenario */
- public function tearUpScenario() {
+ public function setUpScenario() {
$this->client = new Client();
$this->responseXml = '';
}
@@ -67,23 +54,25 @@ class CardDavContext implements \Behat\Behat\Context\Context {
],
]
);
- } catch (\GuzzleHttp\Exception\ClientException $e) {}
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ }
}
-
/**
- * @When :user requests addressbook :addressBook with statuscode :statusCode
+ * @When :user requests addressbook :addressBook with statuscode :statusCode on the endpoint :endpoint
* @param string $user
* @param string $addressBook
* @param int $statusCode
+ * @param string $endpoint
* @throws \Exception
*/
- public function requestsAddressbookWithStatuscode($user, $addressBook, $statusCode) {
- $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$addressBook;
+ public function requestsAddressbookWithStatuscodeOnTheEndpoint($user, $addressBook, $statusCode, $endpoint) {
+ $davUrl = $this->baseUrl . $endpoint . $addressBook;
$password = ($user === 'admin') ? 'admin' : '123456';
try {
- $this->response = $this->client->get(
+ $this->response = $this->client->request(
+ 'PROPFIND',
$davUrl,
[
'auth' => [
@@ -96,7 +85,7 @@ class CardDavContext implements \Behat\Behat\Context\Context {
$this->response = $e->getResponse();
}
- if((int)$statusCode !== $this->response->getStatusCode()) {
+ if ((int)$statusCode !== $this->response->getStatusCode()) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -107,7 +96,7 @@ class CardDavContext implements \Behat\Behat\Context\Context {
}
$body = $this->response->getBody()->getContents();
- if(substr($body, 0, 1) === '<') {
+ if (substr($body, 0, 1) === '<') {
$reader = new Sabre\Xml\Reader();
$reader->xml($body);
$this->responseXml = $reader->parse();
@@ -122,10 +111,10 @@ 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';
- $request = $this->client->createRequest(
+ $this->response = $this->client->request(
'MKCOL',
$davUrl,
[
@@ -135,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>',
@@ -149,9 +138,7 @@ class CardDavContext implements \Behat\Behat\Context\Context {
]
);
- $this->response = $this->client->send($request);
-
- if($this->response->getStatusCode() !== (int)$statusCode) {
+ if ($this->response->getStatusCode() !== (int)$statusCode) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -170,7 +157,7 @@ class CardDavContext implements \Behat\Behat\Context\Context {
public function theCarddavExceptionIs($message) {
$result = $this->responseXml['value'][0]['value'];
- if($message !== $result) {
+ if ($message !== $result) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -189,7 +176,7 @@ class CardDavContext implements \Behat\Behat\Context\Context {
public function theCarddavErrorMessageIs($message) {
$result = $this->responseXml['value'][1]['value'];
- if($message !== $result) {
+ if ($message !== $result) {
throw new \Exception(
sprintf(
'Expected %s got %s',
@@ -200,4 +187,171 @@ 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;
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ $this->response = $this->client->request(
+ 'PUT',
+ $davUrl,
+ [
+ 'body' => file_get_contents(__DIR__ . '/../../data/' . $fileName),
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/xml;charset=UTF-8',
+ ],
+ ]
+ );
+
+ if ($this->response->getStatusCode() !== 201) {
+ throw new \Exception(
+ sprintf(
+ 'Expected %s got %s',
+ 201,
+ $this->response->getStatusCode()
+ )
+ );
+ }
+ }
+
+ /**
+ * @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';
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ try {
+ $this->response = $this->client->request(
+ 'GET',
+ $davUrl,
+ [
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/xml;charset=UTF-8',
+ ],
+ ]
+ );
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * @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;
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ try {
+ $this->response = $this->client->request(
+ 'GET',
+ $davUrl,
+ [
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/xml;charset=UTF-8',
+ ],
+ ]
+ );
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * @Then The following HTTP headers should be set
+ * @param \Behat\Gherkin\Node\TableNode $table
+ * @throws \Exception
+ */
+ public function theFollowingHttpHeadersShouldBeSet(\Behat\Gherkin\Node\TableNode $table) {
+ foreach ($table->getTable() as $header) {
+ $headerName = $header[0];
+ $expectedHeaderValue = $header[1];
+ $returnedHeader = $this->response->getHeader($headerName)[0];
+ if ($returnedHeader !== $expectedHeaderValue) {
+ throw new \Exception(
+ sprintf(
+ "Expected value '%s' for header '%s', got '%s'",
+ $expectedHeaderValue,
+ $headerName,
+ $returnedHeader
+ )
+ );
+ }
+ }
+ }
+
+ /**
+ * @When :user sends a create addressbook request to :addressbook on the endpoint :endpoint
+ */
+ public function sendsCreateAddressbookRequest(string $user, string $addressbook, string $endpoint) {
+ $davUrl = $this->baseUrl . $endpoint . $addressbook;
+ $password = ($user === 'admin') ? 'admin' : '123456';
+
+ try {
+ $this->response = $this->client->request(
+ 'MKCOL',
+ $davUrl,
+ [
+ 'body' => '<d:mkcol xmlns:card="urn:ietf:params:xml:ns:carddav"
+ xmlns:d="DAV:">
+ <d:set>
+ <d:prop>
+ <d:resourcetype>
+ <d:collection />,<card:addressbook />
+ </d:resourcetype>,<d:displayname>' . $addressbook . '</d:displayname>
+ </d:prop>
+ </d:set>
+ </d:mkcol>',
+ 'auth' => [
+ $user,
+ $password,
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/xml;charset=UTF-8',
+ ],
+ ]
+ );
+ } catch (GuzzleException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * @Then The CardDAV HTTP status code should be :code
+ * @param int $code
+ * @throws \Exception
+ */
+ public function theCarddavHttpStatusCodeShouldBe($code) {
+ if ((int)$code !== $this->response->getStatusCode()) {
+ throw new \Exception(
+ sprintf(
+ 'Expected %s got %s',
+ (int)$code,
+ $this->response->getStatusCode()
+ )
+ );
+ }
+
+ $body = $this->response->getBody()->getContents();
+ if ($body && substr($body, 0, 1) === '<') {
+ $reader = new Sabre\Xml\Reader();
+ $reader->xml($body);
+ $this->responseXml = $reader->parse();
+ }
+ }
}
diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php
index af8f9e5590d..c8abf91127e 100644
--- a/build/integration/features/bootstrap/ChecksumsContext.php
+++ b/build/integration/features/bootstrap/ChecksumsContext.php
@@ -1,12 +1,17 @@
<?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 GuzzleHttp\Client;
use GuzzleHttp\Message\ResponseInterface;
class ChecksumsContext implements \Behat\Behat\Context\Context {
- /** @var string */
+ /** @var string */
private $baseUrl;
/** @var Client */
private $client;
@@ -27,7 +32,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
}
/** @BeforeScenario */
- public function tearUpScenario() {
+ public function setUpScenario() {
$this->client = new Client();
}
@@ -41,7 +46,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @return string
*/
private function getPasswordForUser($userName) {
- if($userName === 'admin') {
+ if ($userName === 'admin') {
return 'admin';
}
return '123456';
@@ -54,9 +59,8 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $destination
* @param string $checksum
*/
- public function userUploadsFileToWithChecksum($user, $source, $destination, $checksum)
- {
- $file = \GuzzleHttp\Stream\Stream::factory(fopen($source, 'r'));
+ public function userUploadsFileToWithChecksum($user, $source, $destination, $checksum) {
+ $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
try {
$this->response = $this->client->put(
$this->baseUrl . '/remote.php/webdav' . $destination,
@@ -83,8 +87,8 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @throws \Exception
*/
public function theWebdavResponseShouldHaveAStatusCode($statusCode) {
- if((int)$statusCode !== $this->response->getStatusCode()) {
- throw new \Exception("Expected $statusCode, got ".$this->response->getStatusCode());
+ if ((int)$statusCode !== $this->response->getStatusCode()) {
+ throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode());
}
}
@@ -93,9 +97,8 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $user
* @param string $path
*/
- public function userRequestTheChecksumOfViaPropfind($user, $path)
- {
- $request = $this->client->createRequest(
+ public function userRequestTheChecksumOfViaPropfind($user, $path) {
+ $this->response = $this->client->request(
'PROPFIND',
$this->baseUrl . '/remote.php/webdav' . $path,
[
@@ -111,7 +114,6 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
]
]
);
- $this->response = $this->client->send($request);
}
/**
@@ -119,8 +121,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $checksum
* @throws \Exception
*/
- public function theWebdavChecksumShouldMatch($checksum)
- {
+ public function theWebdavChecksumShouldMatch($checksum) {
$service = new Sabre\Xml\Service();
$parsed = $service->parse($this->response->getBody()->getContents());
@@ -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']);
}
}
@@ -140,8 +141,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $user
* @param string $path
*/
- public function userDownloadsTheFile($user, $path)
- {
+ public function userDownloadsTheFile($user, $path) {
$this->response = $this->client->get(
$this->baseUrl . '/remote.php/webdav' . $path,
[
@@ -158,10 +158,9 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $checksum
* @throws \Exception
*/
- public function theHeaderChecksumShouldMatch($checksum)
- {
- if ($this->response->getHeader('OC-Checksum') !== $checksum) {
- throw new \Exception("Expected $checksum, got ".$this->response->getHeader('OC-Checksum'));
+ public function theHeaderChecksumShouldMatch($checksum) {
+ if ($this->response->getHeader('OC-Checksum')[0] !== $checksum) {
+ throw new \Exception("Expected $checksum, got " . $this->response->getHeader('OC-Checksum')[0]);
}
}
@@ -171,9 +170,8 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $source
* @param string $destination
*/
- public function userCopiedFileTo($user, $source, $destination)
- {
- $request = $this->client->createRequest(
+ public function userCopiedFileTo($user, $source, $destination) {
+ $this->response = $this->client->request(
'MOVE',
$this->baseUrl . '/remote.php/webdav' . $source,
[
@@ -186,14 +184,12 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
],
]
);
- $this->response = $this->client->send($request);
}
/**
* @Then The webdav checksum should be empty
*/
- public function theWebdavChecksumShouldBeEmpty()
- {
+ public function theWebdavChecksumShouldBeEmpty() {
$service = new Sabre\Xml\Service();
$parsed = $service->parse($this->response->getBody()->getContents());
@@ -204,46 +200,16 @@ 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);
}
}
/**
* @Then The OC-Checksum header should not be there
*/
- public function theOcChecksumHeaderShouldNotBeThere()
- {
+ public function theOcChecksumHeaderShouldNotBeThere() {
if ($this->response->hasHeader('OC-Checksum')) {
- throw new \Exception("Expected no checksum header but got ".$this->response->getHeader('OC-Checksum'));
+ 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
new file mode 100644
index 00000000000..27fa1795c5d
--- /dev/null
+++ b/build/integration/features/bootstrap/CollaborationContext.php
@@ -0,0 +1,206 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Behat\Context\Context;
+use Behat\Gherkin\Node\TableNode;
+use GuzzleHttp\Client;
+use PHPUnit\Framework\Assert;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+class CollaborationContext implements Context {
+ use Provisioning;
+ use AppConfiguration;
+ use WebDav;
+
+ /**
+ * @Then /^get autocomplete for "([^"]*)"$/
+ * @param TableNode|null $formData
+ */
+ public function getAutocompleteForUser(string $search, TableNode $formData): void {
+ $this->getAutocompleteWithType(0, $search, $formData);
+ }
+
+ /**
+ * @Then /^get email autocomplete for "([^"]*)"$/
+ * @param TableNode|null $formData
+ */
+ public function getAutocompleteForEmail(string $search, TableNode $formData): void {
+ $this->getAutocompleteWithType(4, $search, $formData);
+ }
+
+ private function getAutocompleteWithType(int $type, string $search, TableNode $formData): void {
+ $query = $search === 'null' ? null : $search;
+
+ $this->sendRequestForJSON('GET', '/core/autocomplete/get?itemType=files&itemId=123&shareTypes[]=' . $type . '&search=' . $query, [
+ 'itemType' => 'files',
+ 'itemId' => '123',
+ 'search' => $query,
+ ]);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $data = json_decode($this->response->getBody()->getContents(), true);
+ $suggestions = $data['ocs']['data'];
+
+ Assert::assertCount(count($formData->getHash()), $suggestions, 'Suggestion count does not match');
+ Assert::assertEquals($formData->getHash(), array_map(static function ($suggestion, $expected) {
+ $data = [];
+ if (isset($expected['id'])) {
+ $data['id'] = $suggestion['id'];
+ }
+ if (isset($expected['source'])) {
+ $data['source'] = $suggestion['source'];
+ }
+ if (isset($expected['status'])) {
+ $data['status'] = json_encode($suggestion['status']);
+ }
+ return $data;
+ }, $suggestions, $formData->getHash()));
+ }
+
+ /**
+ * @Given /^there is a contact in an addressbook$/
+ */
+ public function thereIsAContactInAnAddressbook() {
+ $this->usingNewDavPath();
+ 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');
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
+ $this->response = $e->getResponse();
+ }
+
+ try {
+ $destination = '/users/admin/myaddressbook/contact1.vcf';
+ $data = <<<EOF
+BEGIN:VCARD
+VERSION:4.0
+PRODID:-//Nextcloud Contacts v4.0.2
+UID:a0f4088a-4dca-4308-9b63-09a1ebcf78f3
+FN:A person
+ADR;TYPE=HOME:;;;;;;
+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');
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
+ $this->response = $e->getResponse();
+ }
+ }
+
+ protected function resetAppConfigs(): void {
+ $this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_group');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_phone');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_userid');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_email');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn');
+ $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members');
+ }
+
+ /**
+ * @Given /^user "([^"]*)" has status "([^"]*)"$/
+ * @param string $user
+ * @param string $status
+ */
+ public function assureUserHasStatus($user, $status) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/user_status/status";
+ $client = new Client();
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
+ if ($user === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$user, $this->regularUser];
+ }
+
+ $options['form_params'] = [
+ 'statusType' => $status
+ ];
+
+ $this->response = $client->put($fullUrl, $options);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/user_status";
+ unset($options['form_params']);
+ $this->response = $client->get($fullUrl, $options);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $returnedStatus = json_decode(json_encode(simplexml_load_string($this->response->getBody()->getContents())->data), true)['status'];
+ Assert::assertEquals($status, $returnedStatus);
+ }
+
+ /**
+ * @param string $user
+ * @return null|array
+ */
+ public function getStatusList(string $user): ?array {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/statuses";
+ $client = new Client();
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
+ if ($user === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$user, $this->regularUser];
+ }
+
+ $this->response = $client->get($fullUrl, $options);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $contents = $this->response->getBody()->getContents();
+ return json_decode(json_encode(simplexml_load_string($contents)->data), true);
+ }
+
+ /**
+ * @Given /^user statuses for "([^"]*)" list "([^"]*)" with status "([^"]*)"$/
+ * @param string $user
+ * @param string $statusUser
+ * @param string $status
+ */
+ public function assertStatusesList(string $user, string $statusUser, string $status): void {
+ $statusList = $this->getStatusList($user);
+ Assert::assertArrayHasKey('element', $statusList, 'Returned status list empty or broken');
+ if (array_key_exists('userId', $statusList['element'])) {
+ // If only one user has a status set, the API returns their status directly
+ Assert::assertArrayHasKey('status', $statusList['element'], 'Returned status list empty or broken');
+ $filteredStatusList = [ $statusList['element']['userId'] => $statusList['element']['status'] ];
+ } else {
+ // If more than one user have their status set, the API returns an array of their statuses
+ $filteredStatusList = array_column($statusList['element'], 'status', 'userId');
+ }
+ Assert::assertArrayHasKey($statusUser, $filteredStatusList, 'User not listed in statuses: ' . $statusUser);
+ Assert::assertEquals($status, $filteredStatusList[$statusUser]);
+ }
+
+ /**
+ * @Given /^user statuses for "([^"]*)" are empty$/
+ * @param string $user
+ */
+ public function assertStatusesEmpty(string $user): void {
+ $statusList = $this->getStatusList($user);
+ Assert::assertEmpty($statusList);
+ }
+}
diff --git a/build/integration/features/bootstrap/CommandLine.php b/build/integration/features/bootstrap/CommandLine.php
new file mode 100644
index 00000000000..924d723daa6
--- /dev/null
+++ b/build/integration/features/bootstrap/CommandLine.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+use PHPUnit\Framework\Assert;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+trait CommandLine {
+ /** @var int return code of last command */
+ private $lastCode;
+ /** @var string stdout of last command */
+ private $lastStdOut;
+ /** @var string stderr of last command */
+ private $lastStdErr;
+
+ /** @var string */
+ protected $ocPath = '../..';
+
+ /**
+ * Invokes an OCC command
+ *
+ * @param []string $args OCC command, the part behind "occ". For example: "files:transfer-ownership"
+ * @return int exit code
+ */
+ public function runOcc($args = []) {
+ $args = array_map(function ($arg) {
+ return escapeshellarg($arg);
+ }, $args);
+ $args[] = '--no-ansi';
+ $args = implode(' ', $args);
+
+ $descriptor = [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ];
+ $process = proc_open('php console.php ' . $args, $descriptor, $pipes, $this->ocPath);
+ $this->lastStdOut = stream_get_contents($pipes[1]);
+ $this->lastStdErr = stream_get_contents($pipes[2]);
+ $this->lastCode = proc_close($process);
+
+ // Clean opcode cache
+ $client = new GuzzleHttp\Client();
+ $client->request('GET', 'http://localhost:8080/apps/testing/clean_opcode_cache.php');
+
+ return $this->lastCode;
+ }
+
+ /**
+ * @Given /^invoking occ with "([^"]*)"$/
+ */
+ public function invokingTheCommand($cmd) {
+ $args = explode(' ', $cmd);
+ $this->runOcc($args);
+ }
+
+ /**
+ * Find exception texts in stderr
+ */
+ public function findExceptions() {
+ $exceptions = [];
+ $captureNext = false;
+ // the exception text usually appears after an "[Exception"] row
+ foreach (explode("\n", $this->lastStdErr) as $line) {
+ if (preg_match('/\[Exception\]/', $line)) {
+ $captureNext = true;
+ continue;
+ }
+ if ($captureNext) {
+ $exceptions[] = trim($line);
+ $captureNext = false;
+ }
+ }
+
+ return $exceptions;
+ }
+
+ /**
+ * @Then /^the command was successful$/
+ */
+ public function theCommandWasSuccessful() {
+ $exceptions = $this->findExceptions();
+ if ($this->lastCode !== 0) {
+ $msg = 'The command was not successful, exit code was ' . $this->lastCode . '.';
+ if (!empty($exceptions)) {
+ $msg .= ' Exceptions: ' . implode(', ', $exceptions);
+ }
+ throw new \Exception($msg);
+ } elseif (!empty($exceptions)) {
+ $msg = 'The command was successful but triggered exceptions: ' . implode(', ', $exceptions);
+ throw new \Exception($msg);
+ }
+ }
+
+ /**
+ * @Then /^the command failed with exit code ([0-9]+)$/
+ */
+ public function theCommandFailedWithExitCode($exitCode) {
+ if ($this->lastCode !== (int)$exitCode) {
+ throw new \Exception('The command was expected to fail with exit code ' . $exitCode . ' but got ' . $this->lastCode);
+ }
+ }
+
+ /**
+ * @Then /^the command failed with exception text "([^"]*)"$/
+ */
+ public function theCommandFailedWithException($exceptionText) {
+ $exceptions = $this->findExceptions();
+ if (empty($exceptions)) {
+ throw new \Exception('The command did not throw any exceptions');
+ }
+
+ if (!in_array($exceptionText, $exceptions)) {
+ throw new \Exception('The command did not throw any exception with the text "' . $exceptionText . '"');
+ }
+ }
+
+ /**
+ * @Then /^the command output contains the text "([^"]*)"$/
+ */
+ public function theCommandOutputContainsTheText($text) {
+ Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
+ }
+
+ /**
+ * @Then /^the command error output contains the text "([^"]*)"$/
+ */
+ public function theCommandErrorOutputContainsTheText($text) {
+ Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stderr');
+ }
+}
diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php
new file mode 100644
index 00000000000..e7764356270
--- /dev/null
+++ b/build/integration/features/bootstrap/CommandLineContext.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+require __DIR__ . '/../../vendor/autoload.php';
+
+use Behat\Behat\Context\Exception\ContextNotFoundException;
+use Behat\Behat\Hook\Scope\BeforeScenarioScope;
+use PHPUnit\Framework\Assert;
+
+class CommandLineContext implements \Behat\Behat\Context\Context {
+ use CommandLine;
+
+ private $lastTransferPath;
+
+ private $featureContext;
+ private $localBaseUrl;
+ private $remoteBaseUrl;
+
+ public function __construct($ocPath, $baseUrl) {
+ $this->ocPath = rtrim($ocPath, '/') . '/';
+ $this->localBaseUrl = $baseUrl;
+ $this->remoteBaseUrl = $baseUrl;
+ }
+
+ /**
+ * @Given Maintenance mode is enabled
+ */
+ public function maintenanceModeIsEnabled() {
+ $this->runOcc(['maintenance:mode', '--on']);
+ }
+
+ /**
+ * @Then Maintenance mode is disabled
+ */
+ public function maintenanceModeIsDisabled() {
+ $this->runOcc(['maintenance:mode', '--off']);
+ }
+
+ /** @BeforeScenario */
+ public function gatherContexts(BeforeScenarioScope $scope) {
+ $environment = $scope->getEnvironment();
+ // this should really be "WebDavContext"
+ try {
+ $this->featureContext = $environment->getContext('FeatureContext');
+ } catch (ContextNotFoundException) {
+ $this->featureContext = $environment->getContext('DavFeatureContext');
+ }
+ }
+
+ private function findLastTransferFolderForUser($sourceUser, $targetUser) {
+ $foundPaths = [];
+ $results = $this->featureContext->listFolder($targetUser, '', 1);
+ foreach ($results as $path => $data) {
+ $path = rawurldecode($path);
+ $parts = explode(' ', $path);
+ if (basename($parts[0]) !== 'Transferred') {
+ continue;
+ }
+ if (isset($parts[2]) && $parts[2] === $sourceUser) {
+ // store timestamp as key
+ $foundPaths[] = [
+ 'date' => strtotime(trim($parts[4], '/')),
+ 'path' => $path,
+ ];
+ }
+ }
+
+ if (empty($foundPaths)) {
+ return null;
+ }
+
+ usort($foundPaths, function ($a, $b) {
+ return $a['date'] - $b['date'];
+ });
+
+ $davPath = rtrim($this->featureContext->getDavFilesPath($targetUser), '/');
+
+ $foundPath = end($foundPaths)['path'];
+ // strip dav path
+ return substr($foundPath, strlen($davPath) + 1);
+ }
+
+ /**
+ * @When /^transferring ownership from "([^"]+)" to "([^"]+)"$/
+ */
+ public function transferringOwnership($user1, $user2) {
+ if ($this->runOcc(['files:transfer-ownership', $user1, $user2]) === 0) {
+ $this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2);
+ } else {
+ // failure
+ $this->lastTransferPath = null;
+ }
+ }
+
+ /**
+ * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"$/
+ */
+ public function transferringOwnershipPath($path, $user1, $user2) {
+ $path = '--path=' . $path;
+ if ($this->runOcc(['files:transfer-ownership', $path, $user1, $user2]) === 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) {
+ $davPath = $this->featureContext->getDavFilesPath($user);
+ $davPath = rtrim($davPath, '/') . $this->lastTransferPath;
+ $this->featureContext->usingDavPath($davPath);
+ }
+
+ /**
+ * @Then /^transfer folder name contains "([^"]+)"$/
+ */
+ public function transferFolderNameContains($text) {
+ Assert::assertStringContainsString($text, $this->lastTransferPath);
+ }
+}
diff --git a/build/integration/features/bootstrap/CommentsContext.php b/build/integration/features/bootstrap/CommentsContext.php
index e74e9580dcc..53001b1c204 100644
--- a/build/integration/features/bootstrap/CommentsContext.php
+++ b/build/integration/features/bootstrap/CommentsContext.php
@@ -1,24 +1,10 @@
<?php
+
/**
- * @author Lukas Reschke <lukas@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
require __DIR__ . '/../../vendor/autoload.php';
class CommentsContext implements \Behat\Behat\Context\Context {
@@ -44,12 +30,41 @@ class CommentsContext implements \Behat\Behat\Context\Context {
}
}
+ /**
+ * get a named entry from response instead of picking a random entry from values
+ *
+ * @param string $path
+ *
+ * @return array|string
+ * @throws Exception
+ */
+ private function getValueFromNamedEntries(string $path, array $response): mixed {
+ $next = '';
+ if (str_contains($path, ' ')) {
+ [$key, $next] = explode(' ', $path, 2);
+ } else {
+ $key = $path;
+ }
+
+ foreach ($response as $entry) {
+ if ($entry['name'] === $key) {
+ if ($next !== '') {
+ return $this->getValueFromNamedEntries($next, $entry['value']);
+ } else {
+ return $entry['value'];
+ }
+ }
+ }
+
+ return null;
+ }
+
/** @AfterScenario */
public function teardownScenario() {
$client = new \GuzzleHttp\Client();
try {
$client->delete(
- $this->baseUrl.'/remote.php/webdav/myFileToComment.txt',
+ $this->baseUrl . '/remote.php/webdav/myFileToComment.txt',
[
'auth' => [
'user0',
@@ -70,9 +85,9 @@ class CommentsContext implements \Behat\Behat\Context\Context {
* @return int
*/
private function getFileIdForPath($path) {
- $url = $this->baseUrl.'/remote.php/webdav/'.$path;
- $context = stream_context_create(array(
- 'http' => array(
+ $url = $this->baseUrl . '/remote.php/webdav/' . $path;
+ $context = stream_context_create([
+ 'http' => [
'method' => 'PROPFIND',
'header' => "Authorization: Basic dXNlcjA6MTIzNDU2\r\nContent-Type: application/x-www-form-urlencoded",
'content' => '<?xml version="1.0"?>
@@ -81,8 +96,8 @@ class CommentsContext implements \Behat\Behat\Context\Context {
<oc:fileid />
</d:prop>
</d:propfind>'
- )
- ));
+ ]
+ ]);
$response = file_get_contents($url, false, $context);
preg_match_all('/\<oc:fileid\>(.*)\<\/oc:fileid\>/', $response, $matches);
@@ -97,10 +112,10 @@ class CommentsContext implements \Behat\Behat\Context\Context {
* @param int $statusCode
* @throws \Exception
*/
- public function postsACommentWithContentOnTheFileNamedItShouldReturn($user, $content, $fileName, $statusCode) {
+ public function postsACommentWithContentOnTheFileNamedItShouldReturn($user, $content, $fileName, $statusCode) {
$fileId = $this->getFileIdForPath($fileName);
$this->fileId = (int)$fileId;
- $url = $this->baseUrl.'/remote.php/dav/comments/files/'.$fileId.'/';
+ $url = $this->baseUrl . '/remote.php/dav/comments/files/' . $fileId . '/';
$client = new \GuzzleHttp\Client();
try {
@@ -121,8 +136,8 @@ class CommentsContext implements \Behat\Behat\Context\Context {
$res = $e->getResponse();
}
- if($res->getStatusCode() !== (int)$statusCode) {
- throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")");
+ if ($res->getStatusCode() !== (int)$statusCode) {
+ throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')');
}
}
@@ -136,11 +151,11 @@ class CommentsContext implements \Behat\Behat\Context\Context {
*/
public function asLoadloadAllTheCommentsOfTheFileNamedItShouldReturn($user, $fileName, $statusCode) {
$fileId = $this->getFileIdForPath($fileName);
- $url = $this->baseUrl.'/remote.php/dav/comments/files/'.$fileId.'/';
+ $url = $this->baseUrl . '/remote.php/dav/comments/files/' . $fileId . '/';
try {
$client = new \GuzzleHttp\Client();
- $res = $client->createRequest(
+ $res = $client->request(
'REPORT',
$url,
[
@@ -159,19 +174,18 @@ class CommentsContext implements \Behat\Behat\Context\Context {
],
]
);
- $res = $client->send($res);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$res = $e->getResponse();
}
- if($res->getStatusCode() !== (int)$statusCode) {
- throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")");
+ if ($res->getStatusCode() !== (int)$statusCode) {
+ throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')');
}
- if($res->getStatusCode() === 207) {
+ if ($res->getStatusCode() === 207) {
$service = new Sabre\Xml\Service();
$this->response = $service->parse($res->getBody()->getContents());
- $this->commentId = (int)$this->response[0]['value'][2]['value'][0]['value'][0]['value'];
+ $this->commentId = (int)($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0);
}
}
@@ -188,8 +202,11 @@ class CommentsContext implements \Behat\Behat\Context\Context {
$options = [];
$options['auth'] = [$user, '123456'];
$fd = $body->getRowsHash();
- $options['body'] = $fd;
- $client->send($client->createRequest($verb, $this->baseUrl.'/ocs/v1.php/'.$url, $options));
+ $options['form_params'] = $fd;
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+ $client->request($verb, $this->baseUrl . '/ocs/v1.php/' . $url, $options);
}
/**
@@ -199,7 +216,7 @@ class CommentsContext implements \Behat\Behat\Context\Context {
* @throws \Exception
*/
public function asDeleteTheCreatedCommentItShouldReturn($user, $statusCode) {
- $url = $this->baseUrl.'/remote.php/dav/comments/files/'.$this->fileId.'/'.$this->commentId;
+ $url = $this->baseUrl . '/remote.php/dav/comments/files/' . $this->fileId . '/' . $this->commentId;
$client = new \GuzzleHttp\Client();
try {
@@ -219,8 +236,8 @@ class CommentsContext implements \Behat\Behat\Context\Context {
$res = $e->getResponse();
}
- if($res->getStatusCode() !== (int)$statusCode) {
- throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")");
+ if ($res->getStatusCode() !== (int)$statusCode) {
+ throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')');
}
}
@@ -231,16 +248,17 @@ class CommentsContext implements \Behat\Behat\Context\Context {
* @throws \Exception
*/
public function theResponseShouldContainAPropertyWithValue($key, $value) {
- $keys = $this->response[0]['value'][2]['value'][0]['value'];
+ // $keys = $this->response[0]['value'][1]['value'][0]['value'];
+ $keys = $this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop', $this->response);
$found = false;
- foreach($keys as $singleKey) {
- if($singleKey['name'] === '{http://owncloud.org/ns}'.substr($key, 3)) {
- if($singleKey['value'] === $value) {
+ foreach ($keys as $singleKey) {
+ if ($singleKey['name'] === '{http://owncloud.org/ns}' . substr($key, 3)) {
+ if ($singleKey['value'] === $value) {
$found = true;
}
}
}
- if($found === false) {
+ if ($found === false) {
throw new \Exception("Cannot find property $key with $value");
}
}
@@ -251,8 +269,12 @@ class CommentsContext implements \Behat\Behat\Context\Context {
* @throws \Exception
*/
public function theResponseShouldContainOnlyComments($number) {
- if(count($this->response) !== (int)$number) {
- throw new \Exception("Found more comments than $number (".count($this->response).")");
+ $count = 0;
+ if ($this->response !== null) {
+ $count = count($this->response);
+ }
+ if ($count !== (int)$number) {
+ throw new \Exception("Found more comments than $number (" . $count . ')');
}
}
@@ -271,20 +293,18 @@ class CommentsContext implements \Behat\Behat\Context\Context {
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:set>
<d:prop>
- <oc:message>'.$text.'</oc:message>
+ <oc:message>' . $text . '</oc:message>
</d:prop>
</d:set>
</d:propertyupdate>';
try {
- $res = $client->send($client->createRequest('PROPPATCH', $this->baseUrl.'/remote.php/dav/comments/files/' . $this->fileId . '/' . $this->commentId, $options));
+ $res = $client->request('PROPPATCH', $this->baseUrl . '/remote.php/dav/comments/files/' . $this->fileId . '/' . $this->commentId, $options);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$res = $e->getResponse();
}
- if($res->getStatusCode() !== (int)$statusCode) {
- throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")");
+ if ($res->getStatusCode() !== (int)$statusCode) {
+ 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
new file mode 100644
index 00000000000..f6bf6b9422b
--- /dev/null
+++ b/build/integration/features/bootstrap/ContactsMenu.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use PHPUnit\Framework\Assert;
+
+trait ContactsMenu {
+ // BasicStructure trait is expected to be used in the class that uses this
+ // trait.
+
+ /**
+ * @When /^searching for contacts matching with "([^"]*)"$/
+ *
+ * @param string $filter
+ */
+ public function searchingForContactsMatchingWith(string $filter) {
+ $url = '/index.php/contactsmenu/contacts';
+
+ $parameters[] = 'filter=' . $filter;
+
+ $url .= '?' . implode('&', $parameters);
+
+ $this->sendingAToWithRequesttoken('POST', $url);
+ }
+
+ /**
+ * @Then /^the list of searched contacts has "(\d+)" contacts$/
+ */
+ public function theListOfSearchedContactsHasContacts(int $count) {
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $searchedContacts = json_decode($this->response->getBody(), $asAssociativeArray = true)['contacts'];
+
+ Assert::assertEquals($count, count($searchedContacts));
+ }
+
+ /**
+ * @Then /^searched contact "(\d+)" is named "([^"]*)"$/
+ *
+ * @param int $index
+ * @param string $expectedName
+ */
+ public function searchedContactXIsNamed(int $index, string $expectedName) {
+ $searchedContacts = json_decode($this->response->getBody(), $asAssociativeArray = true)['contacts'];
+ $searchedContact = $searchedContacts[$index];
+
+ Assert::assertEquals($expectedName, $searchedContact['fullName']);
+ }
+}
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
new file mode 100644
index 00000000000..549a033346e
--- /dev/null
+++ b/build/integration/features/bootstrap/Download.php
@@ -0,0 +1,155 @@
+<?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 * */
+ private $downloadedFile;
+
+ /** @AfterScenario **/
+ public function cleanupDownloadedFile() {
+ $this->downloadedFile = null;
+ }
+
+ /**
+ * @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', "/remote.php/dav/files/$user/$folder?accept=zip&files=[" . $entries . ']');
+ $this->theHTTPStatusCodeShouldBe('200');
+ }
+
+ private function getDownloadedFile() {
+ $this->downloadedFile = '';
+
+ /** @var StreamInterface */
+ $body = $this->response->getBody();
+ while (!$body->eof()) {
+ $this->downloadedFile .= $body->read(8192);
+ }
+ $body->close();
+ }
+
+ /**
+ * @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'
+ );
+ }
+
+ /**
+ * @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'
+ );
+ }
+
+ /**
+ * @Then the downloaded zip file contains a file named :fileName with the contents of :sourceFileName from :user data
+ */
+ public function theDownloadedZipFileContainsAFileNamedWithTheContentsOfFromData($fileName, $sourceFileName, $user) {
+ $fileHeaderRegExp = '/';
+ $fileHeaderRegExp .= "\x50\x4B\x03\x04"; // Local file header signature
+ $fileHeaderRegExp .= '.{22,22}'; // Ignore from "version needed to extract" to "uncompressed size"
+ $fileHeaderRegExp .= preg_quote(pack('v', strlen($fileName)), '/'); // File name length
+ $fileHeaderRegExp .= '(.{2,2})'; // Get "extra field length"
+ $fileHeaderRegExp .= preg_quote($fileName, '/'); // File name
+ $fileHeaderRegExp .= '/s'; // PCRE_DOTALL, so all characters (including bytes that happen to be new line characters) match
+
+ // assertRegExp is not used to prevent the whole file from being printed
+ // 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'
+ );
+
+ $extraFieldLength = unpack('vextraFieldLength', $matches[1])['extraFieldLength'];
+ $expectedFileContents = file_get_contents($this->getDataDirectory() . "/$user/files" . $sourceFileName);
+
+ $fileHeaderAndContentRegExp = '/';
+ $fileHeaderAndContentRegExp .= "\x50\x4B\x03\x04"; // Local file header signature
+ $fileHeaderAndContentRegExp .= '.{22,22}'; // Ignore from "version needed to extract" to "uncompressed size"
+ $fileHeaderAndContentRegExp .= preg_quote(pack('v', strlen($fileName)), '/'); // File name length
+ $fileHeaderAndContentRegExp .= '.{2,2}'; // Ignore "extra field length"
+ $fileHeaderAndContentRegExp .= preg_quote($fileName, '/'); // File name
+ $fileHeaderAndContentRegExp .= '.{' . $extraFieldLength . ',' . $extraFieldLength . '}'; // Ignore "extra field"
+ $fileHeaderAndContentRegExp .= preg_quote($expectedFileContents, '/'); // File contents
+ $fileHeaderAndContentRegExp .= '/s'; // PCRE_DOTALL, so all characters (including bytes that happen to be new line characters) match
+
+ // assertRegExp is not used to prevent the whole file from being printed
+ // 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'
+ );
+ }
+
+ /**
+ * @Then the downloaded zip file contains a folder named :folderName
+ */
+ public function theDownloadedZipFileContainsAFolderNamed($folderName) {
+ $folderHeaderRegExp = '/';
+ $folderHeaderRegExp .= "\x50\x4B\x03\x04"; // Local file header signature
+ $folderHeaderRegExp .= '.{22,22}'; // Ignore from "version needed to extract" to "uncompressed size"
+ $folderHeaderRegExp .= preg_quote(pack('v', strlen($folderName)), '/'); // File name length
+ $folderHeaderRegExp .= '.{2,2}'; // Ignore "extra field length"
+ $folderHeaderRegExp .= preg_quote($folderName, '/'); // File name
+ $folderHeaderRegExp .= '/s'; // PCRE_DOTALL, so all characters (including bytes that happen to be new line characters) match
+
+ // assertRegExp is not used to prevent the whole file from being printed
+ // in case of error.
+ Assert::assertEquals(
+ 1, preg_match($folderHeaderRegExp, $this->downloadedFile),
+ '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
new file mode 100644
index 00000000000..32387869edd
--- /dev/null
+++ b/build/integration/features/bootstrap/FakeSMTPHelper.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+// Code below modified from https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/fakeSMTP.php,
+// which is under the MIT license (https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/LICENSE)
+
+/**
+ * fakeSMTP - A PHP / inetd fake smtp server.
+ * Allows client<->server interaction
+ * The comunication is based upon the SMPT standards defined in http://www.lesnikowski.com/mail/Rfc/rfc2821.txt
+ */
+
+class fakeSMTP {
+ public $logFile = false;
+ public $serverHello = 'fakeSMTP ESMTP PHP Mail Server Ready';
+
+ public function __construct($fd) {
+ $this->mail = [];
+ $this->mail['ipaddress'] = false;
+ $this->mail['emailSender'] = '';
+ $this->mail['emailRecipients'] = [];
+ $this->mail['emailSubject'] = false;
+ $this->mail['rawEmail'] = false;
+ $this->mail['emailHeaders'] = false;
+ $this->mail['emailBody'] = false;
+
+ $this->fd = $fd;
+ }
+
+ public function receive() {
+ $hasValidFrom = false;
+ $hasValidTo = false;
+ $receivingData = false;
+ $header = true;
+ $this->reply('220 ' . $this->serverHello);
+ $this->mail['ipaddress'] = $this->detectIP();
+ while ($data = fgets($this->fd)) {
+ $data = preg_replace('@\r\n@', "\n", $data);
+
+ if (!$receivingData) {
+ $this->log($data);
+ }
+
+ if (!$receivingData && preg_match('/^MAIL FROM:\s?<(.*)>/i', $data, $match)) {
+ if (preg_match('/(.*)@\[.*\]/i', $match[1]) || $match[1] != '' || $this->validateEmail($match[1])) {
+ $this->mail['emailSender'] = $match[1];
+ $this->reply('250 2.1.0 Ok');
+ $hasValidFrom = true;
+ } else {
+ $this->reply('551 5.1.7 Bad sender address syntax');
+ }
+ } elseif (!$receivingData && preg_match('/^RCPT TO:\s?<(.*)>/i', $data, $match)) {
+ if (!$hasValidFrom) {
+ $this->reply('503 5.5.1 Error: need MAIL command');
+ } else {
+ if (preg_match('/postmaster@\[.*\]/i', $match[1]) || $this->validateEmail($match[1])) {
+ array_push($this->mail['emailRecipients'], $match[1]);
+ $this->reply('250 2.1.5 Ok');
+ $hasValidTo = true;
+ } else {
+ $this->reply('501 5.1.3 Bad recipient address syntax ' . $match[1]);
+ }
+ }
+ } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) {
+ $this->reply('250 2.0.0 Ok');
+ $hasValidFrom = false;
+ $hasValidTo = false;
+ } 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]);
+ } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) {
+ if (!$hasValidTo) {
+ $this->reply('503 5.5.1 Error: need RCPT command');
+ } else {
+ $this->reply('354 Ok Send data ending with <CRLF>.<CRLF>');
+ $receivingData = true;
+ }
+ } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) {
+ $this->reply('250 HELO ' . $this->mail['ipaddress']);
+ } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) {
+ break;
+ } elseif (!$receivingData) {
+ //~ $this->reply('250 Ok');
+ $this->reply('502 5.5.2 Error: command not recognized');
+ } 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));
+ $splitmail = explode("\n\n", $this->mail['rawEmail'], 2);
+ if (count($splitmail) == 2) {
+ $this->mail['emailHeaders'] = $splitmail[0];
+ $this->mail['emailBody'] = $splitmail[1];
+ $headers = preg_replace("/ \s+/", ' ', preg_replace("/\n\s/", ' ', $this->mail['emailHeaders']));
+ $headerlines = explode("\n", $headers);
+ for ($i = 0; $i < count($headerlines); $i++) {
+ if (preg_match('/^Subject: (.*)/i', $headerlines[$i], $matches)) {
+ $this->mail['emailSubject'] = trim($matches[1]);
+ }
+ }
+ } else {
+ $this->mail['emailBody'] = $splitmail[0];
+ }
+ set_time_limit(5); // Just run the exit to prevent open threads / abuse
+ } elseif ($receivingData) {
+ $this->mail['rawEmail'] .= $data;
+ }
+ }
+ /* Say good bye */
+ $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);
+ }
+ }
+
+ private function reply($s) {
+ $this->log("REPLY:$s");
+ fwrite($this->fd, $s . "\r\n");
+ }
+
+ private function detectIP() {
+ $raw = explode(':', stream_socket_get_name($this->fd, true));
+ return $raw[0];
+ }
+
+ private function validateEmail($email) {
+ return preg_match('/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/', strtolower($email));
+ }
+
+ private function generateRandom($length = 8) {
+ $password = '';
+ $possible = '2346789BCDFGHJKLMNPQRTVWXYZ';
+ $maxlength = strlen($possible);
+ $i = 0;
+ for ($i = 0; $i < $length; $i++) {
+ $char = substr($possible, mt_rand(0, $maxlength - 1), 1);
+ if (!strstr($password, $char)) {
+ $password .= $char;
+ }
+ }
+ return $password;
+ }
+}
+
+$socket = stream_socket_server('tcp://127.0.0.1:2525', $errno, $errstr);
+if (!$socket) {
+ exit();
+}
+
+while ($fd = stream_socket_accept($socket)) {
+ $fakeSMTP = new fakeSMTP($fd);
+ $fakeSMTP->receive();
+}
+
+fclose($socket);
diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php
index 21ca8d87295..ab37556f931 100644
--- a/build/integration/features/bootstrap/FeatureContext.php
+++ b/build/integration/features/bootstrap/FeatureContext.php
@@ -1,14 +1,29 @@
<?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\Behat\Context\Context;
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 2809c6974fa..95dc8119ad6 100644
--- a/build/integration/features/bootstrap/FederationContext.php
+++ b/build/integration/features/bootstrap/FederationContext.php
@@ -1,9 +1,14 @@
<?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\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
-use GuzzleHttp\Client;
-use GuzzleHttp\Message\ResponseInterface;
+use Behat\Gherkin\Node\TableNode;
+use PHPUnit\Framework\Assert;
require __DIR__ . '/../../vendor/autoload.php';
@@ -11,8 +16,45 @@ require __DIR__ . '/../../vendor/autoload.php';
* Federation context.
*/
class FederationContext implements Context, SnippetAcceptingContext {
+ use WebDav;
+ use AppConfiguration;
+ use CommandLine;
+
+ /** @var string */
+ private static $phpFederatedServerPid = '';
- use Sharing;
+ /** @var string */
+ private $lastAcceptedRemoteShareId;
+
+ /**
+ * @BeforeScenario
+ * @AfterScenario
+ *
+ * The server is started also after the scenarios to ensure that it is
+ * properly cleaned up if stopped.
+ */
+ public function startFederatedServer() {
+ if (self::$phpFederatedServerPid !== '') {
+ return;
+ }
+
+ $port = getenv('PORT_FED');
+
+ self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!');
+ }
+
+ /**
+ * @BeforeScenario
+ */
+ public function cleanupRemoteStorages() {
+ // Ensure that dangling remote storages from previous tests will not
+ // interfere with the current scenario.
+ // The storages must be cleaned before each scenario; they can not be
+ // cleaned after each scenario, as this hook is executed before the hook
+ // that removes the users, so the shares would be still valid and thus
+ // the storages would not be dangling yet.
+ $this->runOcc(['sharing:cleanup-remote-storages']);
+ }
/**
* @Given /^User "([^"]*)" from server "(LOCAL|REMOTE)" shares "([^"]*)" with user "([^"]*)" from server "(LOCAL|REMOTE)"$/
@@ -23,8 +65,8 @@ class FederationContext implements Context, SnippetAcceptingContext {
* @param string $shareeUser
* @param string $shareeServer "LOCAL" or "REMOTE"
*/
- public function federateSharing($sharerUser, $sharerServer, $sharerPath, $shareeUser, $shareeServer){
- if ($shareeServer == "REMOTE"){
+ public function federateSharing($sharerUser, $sharerServer, $sharerPath, $shareeUser, $shareeServer) {
+ if ($shareeServer == 'REMOTE') {
$shareWith = "$shareeUser@" . substr($this->remoteBaseUrl, 0, -4);
} else {
$shareWith = "$shareeUser@" . substr($this->localBaseUrl, 0, -4);
@@ -34,21 +76,146 @@ class FederationContext implements Context, SnippetAcceptingContext {
$this->usingServer($previous);
}
+
+ /**
+ * @Given /^User "([^"]*)" from server "(LOCAL|REMOTE)" shares "([^"]*)" with group "([^"]*)" from server "(LOCAL|REMOTE)"$/
+ *
+ * @param string $sharerUser
+ * @param string $sharerServer "LOCAL" or "REMOTE"
+ * @param string $sharerPath
+ * @param string $shareeUser
+ * @param string $shareeServer "LOCAL" or "REMOTE"
+ */
+ public function federateGroupSharing($sharerUser, $sharerServer, $sharerPath, $shareeGroup, $shareeServer) {
+ if ($shareeServer == 'REMOTE') {
+ $shareWith = "$shareeGroup@" . substr($this->remoteBaseUrl, 0, -4);
+ } else {
+ $shareWith = "$shareeGroup@" . substr($this->localBaseUrl, 0, -4);
+ }
+ $previous = $this->usingServer($sharerServer);
+ $this->createShare($sharerUser, $sharerPath, 9, $shareWith, null, null, null);
+ $this->usingServer($previous);
+ }
+
+ /**
+ * @Then remote share :count is returned with
+ *
+ * @param int $number
+ * @param TableNode $body
+ */
+ public function remoteShareXIsReturnedWith(int $number, TableNode $body) {
+ $this->theHTTPStatusCodeShouldBe('200');
+ $this->theOCSStatusCodeShouldBe('100');
+
+ if (!($body instanceof TableNode)) {
+ return;
+ }
+
+ $returnedShare = $this->getXmlResponse()->data[0];
+ if ($returnedShare->element) {
+ $returnedShare = $returnedShare->element[$number];
+ }
+
+ $defaultExpectedFields = [
+ 'id' => 'A_NUMBER',
+ 'remote_id' => 'A_NUMBER',
+ 'accepted' => '1',
+ ];
+ $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash());
+
+ foreach ($expectedFields as $field => $value) {
+ $this->assertFieldIsInReturnedShare($field, $value, $returnedShare);
+ }
+ }
+
/**
* @When /^User "([^"]*)" from server "(LOCAL|REMOTE)" accepts last pending share$/
* @param string $user
* @param string $server
*/
- public function acceptLastPendingShare($user, $server){
+ 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 = $this->response->xml()->data[0]->element[0]->id;
+ $share_id = simplexml_load_string($this->response->getBody())->data[0]->element[0]->id;
$this->sendingToWith('POST', "/apps/files_sharing/api/v1/remote_shares/pending/{$share_id}", null);
$this->theHTTPStatusCodeShouldBe('200');
$this->theOCSStatusCodeShouldBe('100');
$this->usingServer($previous);
+
+ $this->lastAcceptedRemoteShareId = $share_id;
+ }
+
+ /**
+ * @When /^user "([^"]*)" deletes last accepted remote share$/
+ * @param string $user
+ */
+ public function deleteLastAcceptedRemoteShare($user) {
+ $this->asAn($user);
+ $this->sendingToWith('DELETE', '/apps/files_sharing/api/v1/remote_shares/' . $this->lastAcceptedRemoteShareId, null);
+ }
+
+ /**
+ * @When /^remote server is stopped$/
+ */
+ public function remoteServerIsStopped() {
+ if (self::$phpFederatedServerPid === '') {
+ return;
+ }
+
+ exec('kill ' . self::$phpFederatedServerPid);
+
+ 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
new file mode 100644
index 00000000000..0c437f28a72
--- /dev/null
+++ b/build/integration/features/bootstrap/FilesDropContext.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Behat\Context\Context;
+use Behat\Behat\Context\SnippetAcceptingContext;
+use GuzzleHttp\Client;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+class FilesDropContext implements Context, SnippetAcceptingContext {
+ use WebDav;
+
+ /**
+ * @When Dropping file :path with :content
+ */
+ public function droppingFileWith($path, $content, $nickname = null) {
+ $client = new Client();
+ $options = [];
+ if (count($this->lastShareData->data->element) > 0) {
+ $token = $this->lastShareData->data[0]->token;
+ } else {
+ $token = $this->lastShareData->data[0]->token;
+ }
+
+ $base = substr($this->baseUrl, 0, -4);
+ $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$path");
+
+ $options['headers'] = [
+ 'X-REQUESTED-WITH' => 'XMLHttpRequest',
+ ];
+
+ if ($nickname) {
+ $options['headers']['X-NC-NICKNAME'] = $nickname;
+ }
+
+ $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content);
+
+ try {
+ $this->response = $client->request('PUT', $fullUrl, $options);
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+
+ /**
+ * @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, $nickname = null) {
+ $client = new Client();
+ $options = [];
+ if (count($this->lastShareData->data->element) > 0) {
+ $token = $this->lastShareData->data[0]->token;
+ } else {
+ $token = $this->lastShareData->data[0]->token;
+ }
+
+ $base = substr($this->baseUrl, 0, -4);
+ $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$folder");
+
+ $options['headers'] = [
+ '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
new file mode 100644
index 00000000000..986dced77a1
--- /dev/null
+++ b/build/integration/features/bootstrap/LDAPContext.php
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Behat\Context\Context;
+use Behat\Gherkin\Node\TableNode;
+use PHPUnit\Framework\Assert;
+
+class LDAPContext implements Context {
+ use AppConfiguration,
+ CommandLine,
+ Sharing; // Pulls in BasicStructure
+
+ protected $configID;
+
+ protected $apiUrl;
+
+ /** @AfterScenario */
+ public function teardown() {
+ if ($this->configID === null) {
+ return;
+ }
+ $this->disableLDAPConfiguration(); # via occ in case of big config issues
+ $this->asAn('admin');
+ $this->sendingTo('DELETE', $this->apiUrl . '/' . $this->configID);
+ }
+
+ /**
+ * @Given /^the response should contain a tag "([^"]*)"$/
+ */
+ public function theResponseShouldContainATag($arg1) {
+ $configID = simplexml_load_string($this->response->getBody())->data[0]->$arg1;
+ Assert::assertInstanceOf(SimpleXMLElement::class, $configID[0]);
+ }
+
+ /**
+ * @Given /^creating an LDAP configuration at "([^"]*)"$/
+ */
+ public function creatingAnLDAPConfigurationAt($apiUrl) {
+ $this->apiUrl = $apiUrl;
+ $this->sendingToWith('POST', $this->apiUrl, null);
+ $configElements = simplexml_load_string($this->response->getBody())->data[0]->configID;
+ $this->configID = $configElements[0];
+ }
+
+ /**
+ * @When /^deleting the LDAP configuration$/
+ */
+ public function deletingTheLDAPConfiguration() {
+ $this->sendingToWith('DELETE', $this->apiUrl . '/' . $this->configID, null);
+ }
+
+ /**
+ * @Given /^the response should contain a tag "([^"]*)" with value "([^"]*)"$/
+ */
+ public function theResponseShouldContainATagWithValue($tagName, $expectedValue) {
+ $data = simplexml_load_string($this->response->getBody())->data[0]->$tagName;
+ Assert::assertEquals($expectedValue, $data[0]);
+ }
+
+ /**
+ * @When /^getting the LDAP configuration with showPassword "([^"]*)"$/
+ */
+ public function gettingTheLDAPConfigurationWithShowPassword($showPassword) {
+ $this->sendingToWith(
+ 'GET',
+ $this->apiUrl . '/' . $this->configID . '?showPassword=' . $showPassword,
+ null
+ );
+ }
+
+ /**
+ * @Given /^setting the LDAP configuration to$/
+ */
+ public function settingTheLDAPConfigurationTo(TableNode $configData) {
+ $this->sendingToWith('PUT', $this->apiUrl . '/' . $this->configID, $configData);
+ }
+
+ /**
+ * @Given /^having a valid LDAP configuration$/
+ */
+ public function havingAValidLDAPConfiguration() {
+ $this->asAn('admin');
+ $this->creatingAnLDAPConfigurationAt('/apps/user_ldap/api/v1/config');
+ $data = new TableNode([
+ ['configData[ldapHost]', getenv('LDAP_HOST') ?: 'openldap'],
+ ['configData[ldapPort]', '389'],
+ ['configData[ldapBase]', 'dc=nextcloud,dc=ci'],
+ ['configData[ldapAgentName]', 'cn=admin,dc=nextcloud,dc=ci'],
+ ['configData[ldapAgentPassword]', 'admin'],
+ ['configData[ldapUserFilter]', '(&(objectclass=inetorgperson))'],
+ ['configData[ldapLoginFilter]', '(&(objectclass=inetorgperson)(uid=%uid))'],
+ ['configData[ldapUserDisplayName]', 'displayname'],
+ ['configData[ldapGroupDisplayName]', 'cn'],
+ ['configData[ldapEmailAttribute]', 'mail'],
+ ['configData[ldapConfigurationActive]', '1'],
+ ]);
+ $this->settingTheLDAPConfigurationTo($data);
+ $this->asAn('');
+ }
+
+ /**
+ * @Given /^looking up details for the first result matches expectations$/
+ * @param TableNode $expectations
+ */
+ public function lookingUpDetailsForTheFirstResult(TableNode $expectations) {
+ $userResultElements = simplexml_load_string($this->response->getBody())->data[0]->users[0]->element;
+ $userResults = json_decode(json_encode($userResultElements), 1);
+ $userId = array_shift($userResults);
+
+ $this->sendingTo('GET', '/cloud/users/' . $userId);
+ $this->theRecordFieldsShouldMatch($expectations);
+ }
+
+ /**
+ * @Given /^modify LDAP configuration$/
+ */
+ public function modifyLDAPConfiguration(TableNode $table) {
+ $originalAsAn = $this->currentUser;
+ $this->asAn('admin');
+ $configData = $table->getRows();
+ foreach ($configData as &$row) {
+ if (str_contains($row[0], 'Host') && getenv('LDAP_HOST')) {
+ $row[1] = str_replace('openldap', getenv('LDAP_HOST'), $row[1]);
+ }
+ $row[0] = 'configData[' . $row[0] . ']';
+ }
+ $this->settingTheLDAPConfigurationTo(new TableNode($configData));
+ $this->asAn($originalAsAn);
+ }
+
+ /**
+ * @Given /^the "([^"]*)" result should match$/
+ */
+ public function theGroupResultShouldMatch(string $type, TableNode $expectations) {
+ $listReturnedElements = simplexml_load_string($this->response->getBody())->data[0]->$type[0]->element;
+ $extractedIDsArray = json_decode(json_encode($listReturnedElements), 1);
+ foreach ($expectations->getRows() as $expectation) {
+ if ((int)$expectation[1] === 1) {
+ Assert::assertContains($expectation[0], $extractedIDsArray);
+ } else {
+ Assert::assertNotContains($expectation[0], $extractedIDsArray);
+ }
+ }
+ }
+
+ /**
+ * @Given /^Expect ServerException on failed web login as "([^"]*)"$/
+ */
+ public function expectServerExceptionOnFailedWebLoginAs($login) {
+ try {
+ $this->loggingInUsingWebAs($login);
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ Assert::assertEquals(500, $e->getResponse()->getStatusCode());
+ return;
+ }
+ Assert::assertTrue(false, 'expected Exception not received');
+ }
+
+ /**
+ * @Given /^the "([^"]*)" result should contain "([^"]*)" of$/
+ */
+ public function theResultShouldContainOf($type, $expectedCount, TableNode $expectations) {
+ $listReturnedElements = simplexml_load_string($this->response->getBody())->data[0]->$type[0]->element;
+ $extractedIDsArray = json_decode(json_encode($listReturnedElements), 1);
+ $uidsFound = 0;
+ foreach ($expectations->getRows() as $expectation) {
+ if (in_array($expectation[0], $extractedIDsArray)) {
+ $uidsFound++;
+ }
+ }
+ Assert::assertSame((int)$expectedCount, $uidsFound);
+ }
+
+ /**
+ * @Given /^the record's fields should match$/
+ */
+ public function theRecordFieldsShouldMatch(TableNode $expectations) {
+ foreach ($expectations->getRowsHash() as $k => $v) {
+ $value = (string)simplexml_load_string($this->response->getBody())->data[0]->$k;
+ Assert::assertEquals($v, $value, "got $value");
+ }
+
+ $backend = (string)simplexml_load_string($this->response->getBody())->data[0]->backend;
+ Assert::assertEquals('LDAP', $backend);
+ }
+
+ public function disableLDAPConfiguration() {
+ $configKey = $this->configID . 'ldap_configuration_active';
+ $this->invokingTheCommand('config:app:set user_ldap ' . $configKey . ' --value="0"');
+ }
+
+ protected function resetAppConfigs() {
+ // not implemented
+ }
+}
diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php
new file mode 100644
index 00000000000..d48ed6399c5
--- /dev/null
+++ b/build/integration/features/bootstrap/Mail.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+trait Mail {
+ // CommandLine trait is expected to be used in the class that uses this
+ // trait.
+
+ /**
+ * @var string
+ */
+ private $fakeSmtpServerPid;
+
+ /**
+ * @AfterScenario
+ */
+ public function killDummyMailServer() {
+ if (!$this->fakeSmtpServerPid) {
+ return;
+ }
+
+ exec('kill ' . $this->fakeSmtpServerPid);
+
+ $this->invokingTheCommand('config:system:delete mail_smtpport');
+ }
+
+ /**
+ * @Given /^dummy mail server is listening$/
+ */
+ public function dummyMailServerIsListening() {
+ // Default smtpport (25) is restricted for regular users, so the
+ // 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 $!');
+ }
+}
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 feeb850ae7d..935ad2a4a1d 100644
--- a/build/integration/features/bootstrap/Provisioning.php
+++ b/build/integration/features/bootstrap/Provisioning.php
@@ -1,7 +1,15 @@
<?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;
require __DIR__ . '/../../vendor/autoload.php';
@@ -16,7 +24,7 @@ trait Provisioning {
/** @var array */
private $createdRemoteGroups = [];
-
+
/** @var array */
private $createdGroups = [];
@@ -29,13 +37,29 @@ 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;
}
$this->userExists($user);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertEquals(200, $this->response->getStatusCode());
+ }
+ /**
+ * @Given /^user "([^"]*)" with displayname "((?:[^"]|\\")*)" exists$/
+ * @param string $user
+ */
+ public function assureUserWithDisplaynameExists($user, $displayname) {
+ try {
+ $this->userExists($user);
+ } catch (\GuzzleHttp\Exception\ClientException $ex) {
+ $previous_user = $this->currentUser;
+ $this->currentUser = 'admin';
+ $this->creatingTheUser($user, $displayname);
+ $this->currentUser = $previous_user;
+ }
+ $this->userExists($user);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
@@ -47,22 +71,22 @@ trait Provisioning {
$this->userExists($user);
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
- PHPUnit_Framework_Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
+ Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
return;
}
$previous_user = $this->currentUser;
- $this->currentUser = "admin";
+ $this->currentUser = 'admin';
$this->deletingTheUser($user);
$this->currentUser = $previous_user;
try {
$this->userExists($user);
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
- PHPUnit_Framework_Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
+ Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
}
}
- public function creatingTheUser($user) {
+ public function creatingTheUser($user, $displayname = '') {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users";
$client = new Client();
$options = [];
@@ -70,13 +94,19 @@ trait Provisioning {
$options['auth'] = $this->adminUser;
}
- $options['body'] = [
- 'userid' => $user,
- 'password' => '123456'
- ];
+ $options['form_params'] = [
+ 'userid' => $user,
+ 'password' => '123456'
+ ];
+ if ($displayname !== '') {
+ $options['form_params']['displayName'] = $displayname;
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
- $this->response = $client->send($client->createRequest("POST", $fullUrl, $options));
- if ($this->currentServer === 'LOCAL'){
+ $this->response = $client->post($fullUrl, $options);
+ if ($this->currentServer === 'LOCAL') {
$this->createdUsers[$user] = $user;
} elseif ($this->currentServer === 'REMOTE') {
$this->createdRemoteUsers[$user] = $user;
@@ -86,13 +116,175 @@ trait Provisioning {
$options2 = [
'auth' => [$user, '123456'],
];
- $url = $fullUrl.'/'.$user;
- $client->send($client->createRequest('GET', $url, $options2));
+ $options2['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+ $url = $fullUrl . '/' . $user;
+ $client->get($url, $options2);
+ }
+
+ /**
+ * @Then /^user "([^"]*)" has$/
+ *
+ * @param string $user
+ * @param TableNode|null $settings
+ */
+ public function userHasSetting($user, $settings) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$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',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ foreach ($settings->getRows() as $setting) {
+ $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1);
+ 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), 'Data wrong for field: ' . $setting[0]);
+ }
+ } elseif (isset($value[0])) {
+ Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]);
+ } else {
+ 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]);
+ }
+ }
+ }
+
+ /**
+ * @Then /^group "([^"]*)" has$/
+ *
+ * @param string $user
+ * @param TableNode|null $settings
+ */
+ public function groupHasSetting($group, $settings) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group";
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$this->currentUser, $this->regularUser];
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ $groupDetails = simplexml_load_string($response->getBody())->data[0]->groups[0]->element;
+ foreach ($settings->getRows() as $setting) {
+ $value = json_decode(json_encode($groupDetails->{$setting[0]}), 1);
+ if (isset($value[0])) {
+ Assert::assertEqualsCanonicalizing($setting[1], $value[0]);
+ } else {
+ Assert::assertEquals('', $setting[1]);
+ }
+ }
+ }
+
+
+ /**
+ * @Then /^user "([^"]*)" has editable fields$/
+ *
+ * @param string $user
+ * @param TableNode|null $fields
+ */
+ public function userHasEditableFields($user, $fields) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields";
+ if ($user !== 'self') {
+ $fullUrl .= '/' . $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',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ $fieldsArray = json_decode(json_encode(simplexml_load_string($response->getBody())->data->element), 1);
+
+ $expectedFields = $fields->getRows();
+ $expectedFields = $this->simplifyArray($expectedFields);
+ Assert::assertEquals($expectedFields, $fieldsArray);
+ }
+
+ /**
+ * @Then /^search users by phone for region "([^"]*)" with$/
+ *
+ * @param string $user
+ * @param TableNode|null $settings
+ */
+ public function searchUserByPhone($region, TableNode $searchTable) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone";
+ $client = new Client();
+ $options = [];
+ $options['auth'] = $this->adminUser;
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+
+ $search = [];
+ foreach ($searchTable->getRows() as $row) {
+ if (!isset($search[$row[0]])) {
+ $search[$row[0]] = [];
+ }
+ $search[$row[0]][] = $row[1];
+ }
+
+ $options['form_params'] = [
+ 'location' => $region,
+ 'search' => $search,
+ ];
+
+ $this->response = $client->post($fullUrl, $options);
}
public function createUser($user) {
$previous_user = $this->currentUser;
- $this->currentUser = "admin";
+ $this->currentUser = 'admin';
$this->creatingTheUser($user);
$this->userExists($user);
$this->currentUser = $previous_user;
@@ -100,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;
@@ -108,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;
@@ -116,17 +308,20 @@ 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;
}
- public function userExists($user){
+ public function userExists($user) {
$fullUrl = $this->baseUrl . "v2.php/cloud/users/$user";
$client = new Client();
$options = [];
$options['auth'] = $this->adminUser;
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true'
+ ];
$this->response = $client->get($fullUrl, $options);
}
@@ -143,12 +338,15 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
$respondedArray = $this->getArrayOfGroupsResponded($this->response);
sort($respondedArray);
- PHPUnit_Framework_Assert::assertContains($group, $respondedArray);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertContains($group, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
public function userBelongsToGroup($user, $group) {
@@ -158,14 +356,17 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
$respondedArray = $this->getArrayOfGroupsResponded($this->response);
if (array_key_exists($group, $respondedArray)) {
- return True;
- } else{
- return False;
+ return true;
+ } else {
+ return false;
}
}
@@ -174,11 +375,11 @@ trait Provisioning {
* @param string $user
* @param string $group
*/
- public function assureUserBelongsToGroup($user, $group){
+ public function assureUserBelongsToGroup($user, $group) {
$previous_user = $this->currentUser;
- $this->currentUser = "admin";
+ $this->currentUser = 'admin';
- if (!$this->userBelongsToGroup($user, $group)){
+ if (!$this->userBelongsToGroup($user, $group)) {
$this->addingUserToGroup($user, $group);
}
@@ -198,12 +399,15 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
- $groups = array($group);
+ $groups = [$group];
$respondedArray = $this->getArrayOfGroupsResponded($this->response);
- PHPUnit_Framework_Assert::assertNotEquals($groups, $respondedArray, "", 0.0, 10, true);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertNotEqualsCanonicalizing($groups, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
@@ -218,12 +422,15 @@ trait Provisioning {
$options['auth'] = $this->adminUser;
}
- $options['body'] = [
- 'groupid' => $group,
- ];
+ $options['form_params'] = [
+ 'groupid' => $group,
+ ];
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
- $this->response = $client->send($client->createRequest("POST", $fullUrl, $options));
- if ($this->currentServer === 'LOCAL'){
+ $this->response = $client->post($fullUrl, $options);
+ if ($this->currentServer === 'LOCAL') {
$this->createdGroups[$group] = $group;
} elseif ($this->currentServer === 'REMOTE') {
$this->createdRemoteGroups[$group] = $group;
@@ -231,6 +438,27 @@ trait Provisioning {
}
/**
+ * @When /^assure user "([^"]*)" is disabled$/
+ */
+ public function assureUserIsDisabled($user) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/disable";
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+ // TODO: fix hack
+ $options['form_params'] = [
+ 'foo' => 'bar'
+ ];
+
+ $this->response = $client->put($fullUrl, $options);
+ }
+
+ /**
* @When /^Deleting the user "([^"]*)"$/
* @param string $user
*/
@@ -241,8 +469,11 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
- $this->response = $client->send($client->createRequest("DELETE", $fullUrl, $options));
+ $this->response = $client->delete($fullUrl, $options);
}
/**
@@ -256,8 +487,17 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
- $this->response = $client->send($client->createRequest("DELETE", $fullUrl, $options));
+ $this->response = $client->delete($fullUrl, $options);
+
+ if ($this->currentServer === 'LOCAL') {
+ unset($this->createdGroups[$group]);
+ } elseif ($this->currentServer === 'REMOTE') {
+ unset($this->createdRemoteGroups[$group]);
+ }
}
/**
@@ -269,7 +509,6 @@ trait Provisioning {
$this->userExists($user);
$this->groupExists($group);
$this->addingUserToGroup($user, $group);
-
}
/**
@@ -284,12 +523,15 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
- $options['body'] = [
- 'groupid' => $group,
- ];
+ $options['form_params'] = [
+ 'groupid' => $group,
+ ];
- $this->response = $client->send($client->createRequest("POST", $fullUrl, $options));
+ $this->response = $client->post($fullUrl, $options);
}
@@ -298,6 +540,9 @@ trait Provisioning {
$client = new Client();
$options = [];
$options['auth'] = $this->adminUser;
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
}
@@ -311,12 +556,12 @@ 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;
}
$this->groupExists($group);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
@@ -328,18 +573,18 @@ trait Provisioning {
$this->groupExists($group);
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
- PHPUnit_Framework_Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
+ Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
return;
}
$previous_user = $this->currentUser;
- $this->currentUser = "admin";
+ $this->currentUser = 'admin';
$this->deletingTheGroup($group);
$this->currentUser = $previous_user;
try {
$this->groupExists($group);
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
- PHPUnit_Framework_Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
+ Assert::assertEquals(404, $ex->getResponse()->getStatusCode());
}
}
@@ -355,12 +600,37 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
$respondedArray = $this->getArrayOfSubadminsResponded($this->response);
sort($respondedArray);
- PHPUnit_Framework_Assert::assertContains($user, $respondedArray);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertContains($user, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
+ }
+
+ /**
+ * @Given /^Assure user "([^"]*)" is subadmin of group "([^"]*)"$/
+ * @param string $user
+ * @param string $group
+ */
+ public function assureUserIsSubadminOfGroup($user, $group) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/subadmins";
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ }
+ $options['form_params'] = [
+ 'groupid' => $group
+ ];
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+ $this->response = $client->post($fullUrl, $options);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
@@ -375,73 +645,99 @@ trait Provisioning {
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
$respondedArray = $this->getArrayOfSubadminsResponded($this->response);
sort($respondedArray);
- PHPUnit_Framework_Assert::assertNotContains($user, $respondedArray);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertNotContains($user, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
* @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);
- PHPUnit_Framework_Assert::assertEquals($usersSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($usersSimplified, $respondedArray);
}
+ }
+ /**
+ * @Then /^phone matches returned are$/
+ * @param TableNode|null $usersList
+ */
+ public function thePhoneUsersShouldBe($usersList) {
+ if ($usersList instanceof TableNode) {
+ $users = $usersList->getRowsHash();
+ $listCheckedElements = simplexml_load_string($this->response->getBody())->data;
+ $respondedArray = json_decode(json_encode($listCheckedElements), true);
+ Assert::assertEquals($users, $respondedArray);
+ }
+ }
+
+ /**
+ * @Then /^detailed users returned are$/
+ * @param TableNode|null $usersList
+ */
+ public function theDetailedUsersShouldBe($usersList) {
+ if ($usersList instanceof TableNode) {
+ $users = $usersList->getRows();
+ $usersSimplified = $this->simplifyArray($users);
+ $respondedArray = $this->getArrayOfDetailedUsersResponded($this->response);
+ $respondedArray = array_keys($respondedArray);
+ Assert::assertEquals($usersSimplified, $respondedArray);
+ }
}
/**
* @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);
- PHPUnit_Framework_Assert::assertEquals($groupsSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($groupsSimplified, $respondedArray);
}
-
}
/**
* @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);
- PHPUnit_Framework_Assert::assertEquals($groupsSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($groupsSimplified, $respondedArray);
}
-
}
/**
* @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);
- PHPUnit_Framework_Assert::assertEquals($appsSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($appsSimplified, $respondedArray);
}
-
}
/**
* @Then /^subadmin users returned are$/
- * @param \Behat\Gherkin\Node\TableNode|null $groupsList
+ * @param TableNode|null $groupsList
*/
public function theSubadminUsersShouldBe($groupsList) {
$this->theSubadminGroupsShouldBe($groupsList);
@@ -449,44 +745,60 @@ trait Provisioning {
/**
* Parses the xml answer to get the array of users returned.
+ *
* @param ResponseInterface $resp
* @return array
*/
public function getArrayOfUsersResponded($resp) {
- $listCheckedElements = $resp->xml()->data[0]->users[0]->element;
+ $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->users[0]->element;
+ $extractedElementsArray = json_decode(json_encode($listCheckedElements), 1);
+ return $extractedElementsArray;
+ }
+
+ /**
+ * Parses the xml answer to get the array of detailed users returned.
+ *
+ * @param ResponseInterface $resp
+ * @return array
+ */
+ public function getArrayOfDetailedUsersResponded($resp) {
+ $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->users;
$extractedElementsArray = json_decode(json_encode($listCheckedElements), 1);
return $extractedElementsArray;
}
/**
* Parses the xml answer to get the array of groups returned.
+ *
* @param ResponseInterface $resp
* @return array
*/
public function getArrayOfGroupsResponded($resp) {
- $listCheckedElements = $resp->xml()->data[0]->groups[0]->element;
+ $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->groups[0]->element;
$extractedElementsArray = json_decode(json_encode($listCheckedElements), 1);
return $extractedElementsArray;
}
/**
* Parses the xml answer to get the array of apps returned.
+ *
* @param ResponseInterface $resp
* @return array
*/
public function getArrayOfAppsResponded($resp) {
- $listCheckedElements = $resp->xml()->data[0]->apps[0]->element;
+ $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->apps[0]->element;
$extractedElementsArray = json_decode(json_encode($listCheckedElements), 1);
return $extractedElementsArray;
}
/**
* Parses the xml answer to get the array of subadmins returned.
+ *
* @param ResponseInterface $resp
* @return array
*/
public function getArrayOfSubadminsResponded($resp) {
- $listCheckedElements = $resp->xml()->data[0]->element;
+ $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->element;
$extractedElementsArray = json_decode(json_encode($listCheckedElements), 1);
return $extractedElementsArray;
}
@@ -497,17 +809,20 @@ 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') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
$respondedArray = $this->getArrayOfAppsResponded($this->response);
- PHPUnit_Framework_Assert::assertContains($app, $respondedArray);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertContains($app, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
@@ -515,17 +830,84 @@ 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') {
+ $options['auth'] = $this->adminUser;
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+
+ $this->response = $client->get($fullUrl, $options);
+ $respondedArray = $this->getArrayOfAppsResponded($this->response);
+ Assert::assertContains($app, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
+ }
+
+ /**
+ * @Given /^app "([^"]*)" is not enabled$/
+ *
+ * Checks that the app is disabled or not installed.
+ *
+ * @param string $app
+ */
+ public function appIsNotEnabled($app) {
+ $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=enabled';
$client = new Client();
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
$respondedArray = $this->getArrayOfAppsResponded($this->response);
- PHPUnit_Framework_Assert::assertContains($app, $respondedArray);
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ Assert::assertNotContains($app, $respondedArray);
+ Assert::assertEquals(200, $this->response->getStatusCode());
+ }
+
+ /**
+ * @Then /^user "([^"]*)" is disabled$/
+ * @param string $user
+ */
+ public function userIsDisabled($user) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+
+ $this->response = $client->get($fullUrl, $options);
+ // false in xml is empty
+ Assert::assertTrue(empty(simplexml_load_string($this->response->getBody())->data[0]->enabled));
+ }
+
+ /**
+ * @Then /^user "([^"]*)" is enabled$/
+ * @param string $user
+ */
+ public function userIsEnabled($user) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+
+ $this->response = $client->get($fullUrl, $options);
+ // boolean to string is integer
+ Assert::assertEquals('1', simplexml_load_string($this->response->getBody())->data[0]->enabled);
}
/**
@@ -533,39 +915,50 @@ trait Provisioning {
* @param string $user
* @param string $quota
*/
- public function userHasAQuotaOf($user, $quota)
- {
- $body = new \Behat\Gherkin\Node\TableNode([
+ public function userHasAQuotaOf($user, $quota) {
+ $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);
}
/**
* @Given user :user has unlimited quota
* @param string $user
*/
- public function userHasUnlimitedQuota($user)
- {
+ public function userHasUnlimitedQuota($user) {
$this->userHasAQuotaOf($user, 'none');
}
/**
+ * Returns home path of the given user
+ *
+ * @param string $user
+ */
+ public function getUserHome($user) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
+ $client = new Client();
+ $options = [];
+ $options['auth'] = $this->adminUser;
+ $this->response = $client->get($fullUrl, $options);
+ return simplexml_load_string($this->response->getBody())->data[0]->home;
+ }
+
+ /**
* @BeforeScenario
* @AfterScenario
*/
- public function cleanupUsers()
- {
+ public function cleanupUsers() {
$previousServer = $this->currentServer;
$this->usingServer('LOCAL');
- foreach($this->createdUsers as $user) {
+ foreach ($this->createdUsers as $user) {
$this->deleteUser($user);
}
$this->usingServer('REMOTE');
- foreach($this->createdRemoteUsers as $remoteUser) {
+ foreach ($this->createdRemoteUsers as $remoteUser) {
$this->deleteUser($remoteUser);
}
$this->usingServer($previousServer);
@@ -575,17 +968,50 @@ trait Provisioning {
* @BeforeScenario
* @AfterScenario
*/
- public function cleanupGroups()
- {
+ public function cleanupGroups() {
$previousServer = $this->currentServer;
$this->usingServer('LOCAL');
- foreach($this->createdGroups as $group) {
+ foreach ($this->createdGroups as $group) {
$this->deleteGroup($group);
}
$this->usingServer('REMOTE');
- foreach($this->createdRemoteGroups as $remoteGroup) {
- $this->deleteUser($remoteGroup);
+ foreach ($this->createdRemoteGroups as $remoteGroup) {
+ $this->deleteGroup($remoteGroup);
}
$this->usingServer($previousServer);
}
+
+ /**
+ * @Then /^user "([^"]*)" has not$/
+ */
+ public function userHasNotSetting($user, TableNode $settings) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$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',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ foreach ($settings->getRows() as $setting) {
+ $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1);
+ if (isset($value[0])) {
+ if (in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) {
+ $expectedValues = explode(';', $setting[1]);
+ foreach ($expectedValues as $expected) {
+ Assert::assertFalse(in_array($expected, $value, true));
+ }
+ } else {
+ Assert::assertNotEqualsCanonicalizing($setting[1], $value[0]);
+ }
+ } else {
+ Assert::assertNotEquals('', $setting[1]);
+ }
+ }
+ }
}
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
new file mode 100644
index 00000000000..6102f686ea7
--- /dev/null
+++ b/build/integration/features/bootstrap/RemoteContext.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Behat\Context\Context;
+use OCP\Http\Client\IClientService;
+use PHPUnit\Framework\Assert;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+/**
+ * Remote context.
+ */
+class RemoteContext implements Context {
+ /** @var \OC\Remote\Instance */
+ protected $remoteInstance;
+
+ /** @var \OC\Remote\Credentials */
+ protected $credentails;
+
+ /** @var \OC\Remote\User */
+ protected $userResult;
+
+ protected $remoteUrl;
+
+ protected $lastException;
+
+ public function __construct($remote) {
+ require_once __DIR__ . '/../../../../lib/base.php';
+ $this->remoteUrl = $remote;
+ }
+
+ protected function getApiClient() {
+ return new \OC\Remote\Api\OCS($this->remoteInstance, $this->credentails, \OC::$server->get(IClientService::class));
+ }
+
+ /**
+ * @Given /^using remote server "(REMOTE|NON_EXISTING)"$/
+ *
+ * @param string $remoteServer "NON_EXISTING" or "REMOTE"
+ */
+ public function selectRemoteInstance($remoteServer) {
+ if ($remoteServer == 'REMOTE') {
+ $baseUri = $this->remoteUrl;
+ } else {
+ $baseUri = 'nonexistingnextcloudserver.local';
+ }
+ $this->lastException = null;
+ try {
+ $this->remoteInstance = new \OC\Remote\Instance($baseUri, \OC::$server->getMemCacheFactory()->createLocal(), \OC::$server->get(IClientService::class));
+ // trigger the status request
+ $this->remoteInstance->getProtocol();
+ } catch (\Exception $e) {
+ $this->lastException = $e;
+ }
+ }
+
+ /**
+ * @Then /^the remote version should be "([^"]*)"$/
+ * @param string $version
+ */
+ public function theRemoteVersionShouldBe($version) {
+ if ($version === '__current_version__') {
+ $version = \OC::$server->getConfig()->getSystemValue('version', '0.0.0.0');
+ }
+
+ Assert::assertEquals($version, $this->remoteInstance->getVersion());
+ }
+
+ /**
+ * @Then /^the remote protocol should be "([^"]*)"$/
+ * @param string $protocol
+ */
+ public function theRemoteProtocolShouldBe($protocol) {
+ Assert::assertEquals($protocol, $this->remoteInstance->getProtocol());
+ }
+
+ /**
+ * @Given /^using credentials "([^"]*)", "([^"]*)"/
+ * @param string $user
+ * @param string $password
+ */
+ public function usingCredentials($user, $password) {
+ $this->credentails = new \OC\Remote\Credentials($user, $password);
+ }
+
+ /**
+ * @When /^getting the remote user info for "([^"]*)"$/
+ * @param string $user
+ */
+ public function remoteUserInfo($user) {
+ $this->lastException = null;
+ try {
+ $this->userResult = $this->getApiClient()->getUser($user);
+ } catch (\Exception $e) {
+ $this->lastException = $e;
+ }
+ }
+
+ /**
+ * @Then /^the remote user should have userid "([^"]*)"$/
+ * @param string $user
+ */
+ public function remoteUserId($user) {
+ Assert::assertEquals($user, $this->userResult->getUserId());
+ }
+
+ /**
+ * @Then /^the request should throw a "([^"]*)"$/
+ * @param string $class
+ */
+ public function lastError($class) {
+ Assert::assertEquals($class, get_class($this->lastException));
+ }
+
+ /**
+ * @Then /^the capability "([^"]*)" is "([^"]*)"$/
+ * @param string $key
+ * @param string $value
+ */
+ public function hasCapability($key, $value) {
+ try {
+ $capabilities = $this->getApiClient()->getCapabilities();
+ } catch (\Exception $e) {
+ Assert::assertInstanceOf($value, $e);
+ $this->lastException = $e;
+ return;
+ }
+ $current = $capabilities;
+ $parts = explode('.', $key);
+ foreach ($parts as $part) {
+ if ($current !== null) {
+ $current = isset($current[$part]) ? $current[$part] : null;
+ }
+ }
+ Assert::assertEquals($value, $current);
+ }
+}
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
new file mode 100644
index 00000000000..49a4fe92822
--- /dev/null
+++ b/build/integration/features/bootstrap/Search.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Gherkin\Node\TableNode;
+use PHPUnit\Framework\Assert;
+
+trait Search {
+ // BasicStructure trait is expected to be used in the class that uses this
+ // trait.
+
+ /**
+ * @When /^searching for "([^"]*)"$/
+ * @param string $query
+ */
+ public function searchingFor(string $query) {
+ $this->searchForInApp($query, '');
+ }
+
+ /**
+ * @When /^searching for "([^"]*)" in app "([^"]*)"$/
+ * @param string $query
+ * @param string $app
+ */
+ public function searchingForInApp(string $query, string $app) {
+ $url = '/index.php/core/search';
+
+ $parameters[] = 'query=' . $query;
+ $parameters[] = 'inApps[]=' . $app;
+
+ $url .= '?' . implode('&', $parameters);
+
+ $this->sendingAToWithRequesttoken('GET', $url);
+ }
+
+ /**
+ * @Then /^the list of search results has "(\d+)" results$/
+ */
+ public function theListOfSearchResultsHasResults(int $count) {
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $searchResults = json_decode($this->response->getBody());
+
+ Assert::assertEquals($count, count($searchResults));
+ }
+
+ /**
+ * @Then /^search result "(\d+)" contains$/
+ *
+ * @param int $number
+ * @param TableNode $body
+ */
+ public function searchResultXContains(int $number, TableNode $body) {
+ if (!($body instanceof TableNode)) {
+ return;
+ }
+
+ $searchResults = json_decode($this->response->getBody(), $asAssociativeArray = true);
+ $searchResult = $searchResults[$number];
+
+ foreach ($body->getRowsHash() as $expectedField => $expectedValue) {
+ if (!array_key_exists($expectedField, $searchResult)) {
+ Assert::fail("$expectedField was not found in response");
+ }
+
+ Assert::assertEquals($expectedValue, $searchResult[$expectedField], "Field '$expectedField' does not match ({$searchResult[$expectedField]})");
+ }
+ }
+}
diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php
new file mode 100644
index 00000000000..aa131cec597
--- /dev/null
+++ b/build/integration/features/bootstrap/SetupContext.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Behat\Context\Context;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+
+/**
+ * Setup context.
+ */
+class SetupContext implements Context {
+ use BasicStructure;
+}
diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php
index bd08ae6e138..37e0e63e547 100644
--- a/build/integration/features/bootstrap/ShareesContext.php
+++ b/build/integration/features/bootstrap/ShareesContext.php
@@ -1,8 +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\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
-use GuzzleHttp\Message\ResponseInterface;
require __DIR__ . '/../../vendor/autoload.php';
@@ -11,63 +15,12 @@ require __DIR__ . '/../../vendor/autoload.php';
* Features context.
*/
class ShareesContext implements Context, SnippetAcceptingContext {
- use Provisioning;
+ use Sharing;
use AppConfiguration;
- /**
- * @When /^getting sharees for$/
- * @param \Behat\Gherkin\Node\TableNode $body
- */
- public function whenGettingShareesFor($body) {
- $url = '/apps/files_sharing/api/v1/sharees';
- if ($body instanceof \Behat\Gherkin\Node\TableNode) {
- $parameters = [];
- foreach ($body->getRowsHash() as $key => $value) {
- $parameters[] = $key . '=' . $value;
- }
- if (!empty($parameters)) {
- $url .= '?' . implode('&', $parameters);
- }
- }
-
- $this->sendingTo('GET', $url);
- }
-
- /**
- * @Then /^"([^"]*)" sharees returned (are|is empty)$/
- * @param string $shareeType
- * @param string $isEmpty
- * @param \Behat\Gherkin\Node\TableNode|null $shareesList
- */
- public function thenListOfSharees($shareeType, $isEmpty, $shareesList = null) {
- if ($isEmpty !== 'is empty') {
- $sharees = $shareesList->getRows();
- $respondedArray = $this->getArrayOfShareesResponded($this->response, $shareeType);
- PHPUnit_Framework_Assert::assertEquals($sharees, $respondedArray);
- } else {
- $respondedArray = $this->getArrayOfShareesResponded($this->response, $shareeType);
- PHPUnit_Framework_Assert::assertEmpty($respondedArray);
- }
- }
-
- public function getArrayOfShareesResponded(ResponseInterface $response, $shareeType) {
- $elements = $response->xml()->data;
- $elements = json_decode(json_encode($elements), 1);
- if (strpos($shareeType, 'exact ') === 0) {
- $elements = $elements['exact'];
- $shareeType = substr($shareeType, 6);
- }
-
- $sharees = [];
- foreach ($elements[$shareeType] as $element) {
- $sharees[] = [$element['label'], $element['value']['shareType'], $element['value']['shareWith']];
- }
- return $sharees;
- }
-
protected function resetAppConfigs() {
- $this->modifyServerConfig('core', 'shareapi_only_share_with_group_members', 'no');
- $this->modifyServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
- $this->modifyServerConfig('core', 'shareapi_allow_group_sharing', 'yes');
+ $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members');
+ $this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration');
+ $this->deleteServerConfig('core', 'shareapi_allow_group_sharing');
}
}
diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php
index d423a28f196..0cc490ff110 100644
--- a/build/integration/features/bootstrap/Sharing.php
+++ b/build/integration/features/bootstrap/Sharing.php
@@ -1,7 +1,14 @@
<?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;
+use Psr\Http\Message\ResponseInterface;
require __DIR__ . '/../../vendor/autoload.php';
@@ -16,95 +23,175 @@ trait Sharing {
/** @var SimpleXMLElement */
private $lastShareData = null;
+ /** @var SimpleXMLElement[] */
+ private $storedShareData = [];
+
/** @var int */
private $savedShareId = null;
+ /** @var ResponseInterface */
+ private $response;
+
/**
* @Given /^as "([^"]*)" creating a share with$/
* @param string $user
- * @param \Behat\Gherkin\Node\TableNode|null $body
+ * @param TableNode|null $body
*/
public function asCreatingAShareWith($user, $body) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares";
$client = new Client();
- $options = [];
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
if ($user === 'admin') {
$options['auth'] = $this->adminUser;
} else {
$options['auth'] = [$user, $this->regularUser];
}
- if ($body instanceof \Behat\Gherkin\Node\TableNode) {
+ if ($body instanceof TableNode) {
$fd = $body->getRowsHash();
- if (array_key_exists('expireDate', $fd)){
+ if (array_key_exists('expireDate', $fd)) {
$dateModification = $fd['expireDate'];
- $fd['expireDate'] = date('Y-m-d', strtotime($dateModification));
+ if ($dateModification === 'null') {
+ $fd['expireDate'] = null;
+ } elseif (!empty($dateModification)) {
+ $fd['expireDate'] = date('Y-m-d', strtotime($dateModification));
+ } else {
+ $fd['expireDate'] = '';
+ }
}
- $options['body'] = $fd;
+ $options['form_params'] = $fd;
}
try {
- $this->response = $client->send($client->createRequest("POST", $fullUrl, $options));
+ $this->response = $client->request('POST', $fullUrl, $options);
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
}
- $this->lastShareData = $this->response->xml();
+ $this->lastShareData = simplexml_load_string($this->response->getBody());
+ }
+
+ /**
+ * @When /^save the last share data as "([^"]*)"$/
+ */
+ public function saveLastShareData($name) {
+ $this->storedShareData[$name] = $this->lastShareData;
+ }
+
+ /**
+ * @When /^restore the last share data from "([^"]*)"$/
+ */
+ public function restoreLastShareData($name) {
+ $this->lastShareData = $this->storedShareData[$name];
}
/**
* @When /^creating a share with$/
- * @param \Behat\Gherkin\Node\TableNode|null $body
+ * @param TableNode|null $body
*/
public function creatingShare($body) {
$this->asCreatingAShareWith($this->currentUser, $body);
}
/**
- * @Then /^Public shared file "([^"]*)" can be downloaded$/
+ * @When /^accepting last share$/
*/
- public function checkPublicSharedFile($filename) {
- $client = new Client();
- $options = [];
- if (count($this->lastShareData->data->element) > 0){
+ 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->theHTTPStatusCodeShouldBe('200');
+ }
+
+ /**
+ * @When /^user "([^"]*)" accepts last share$/
+ *
+ * @param string $user
+ */
+ public function userAcceptsLastShare(string $user) {
+ // "As userXXX" and "user userXXX accepts last share" steps are not
+ // expected to be used in the same scenario, but restore the user just
+ // in case.
+ $previousUser = $this->currentUser;
+
+ $this->currentUser = $user;
+
+ $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->currentUser = $previousUser;
+
+ $this->theHTTPStatusCodeShouldBe('200');
+ }
+
+ /**
+ * @Then /^last link share can be downloaded$/
+ */
+ public function lastLinkShareCanBeDownloaded() {
+ if (count($this->lastShareData->data->element) > 0) {
$url = $this->lastShareData->data[0]->url;
- }
- else{
+ } else {
$url = $this->lastShareData->data->url;
}
- $fullUrl = $url . "/download";
- $options['save_to'] = "./$filename";
- $this->response = $client->get($fullUrl, $options);
- $finfo = new finfo;
- $fileinfo = $finfo->file("./$filename", FILEINFO_MIME_TYPE);
- PHPUnit_Framework_Assert::assertEquals($fileinfo, "text/plain");
- if (file_exists("./$filename")) {
- unlink("./$filename");
- }
+ $fullUrl = $url . '/download';
+ $this->checkDownload($fullUrl, null, 'text/plain');
}
/**
- * @Then /^Public shared file "([^"]*)" with password "([^"]*)" can be downloaded$/
+ * @Then /^last share can be downloaded$/
*/
- public function checkPublicSharedFileWithPassword($filename, $password) {
- $client = new Client();
- $options = [];
- if (count($this->lastShareData->data->element) > 0){
+ public function lastShareCanBeDownloaded() {
+ if (count($this->lastShareData->data->element) > 0) {
$token = $this->lastShareData->data[0]->token;
+ } else {
+ $token = $this->lastShareData->data->token;
}
- else{
+
+ $fullUrl = substr($this->baseUrl, 0, -4) . 'index.php/s/' . $token . '/download';
+ $this->checkDownload($fullUrl, null, 'text/plain');
+ }
+
+ /**
+ * @Then /^last share with password "([^"]*)" can be downloaded$/
+ */
+ public function lastShareWithPasswordCanBeDownloaded($password) {
+ if (count($this->lastShareData->data->element) > 0) {
+ $token = $this->lastShareData->data[0]->token;
+ } else {
$token = $this->lastShareData->data->token;
}
- $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav";
- $options['auth'] = [$token, $password];
- $options['save_to'] = "./$filename";
- $this->response = $client->get($fullUrl, $options);
- $finfo = new finfo;
- $fileinfo = $finfo->file("./$filename", FILEINFO_MIME_TYPE);
- PHPUnit_Framework_Assert::assertEquals($fileinfo, "text/plain");
- if (file_exists("./$filename")) {
- unlink("./$filename");
+ $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/";
+ $this->checkDownload($fullUrl, ['', $password], 'text/plain');
+ }
+
+ private function checkDownload($url, $auth = null, $mimeType = null) {
+ if ($auth !== null) {
+ $options['auth'] = $auth;
+ }
+ $options['stream'] = true;
+
+ $client = new Client();
+ $this->response = $client->get($url, $options);
+ Assert::assertEquals(200, $this->response->getStatusCode());
+
+ $buf = '';
+ $body = $this->response->getBody();
+ while (!$body->eof()) {
+ // read everything
+ $buf .= $body->read(8192);
+ }
+ $body->close();
+
+ if ($mimeType !== null) {
+ $finfo = new finfo;
+ Assert::assertEquals($mimeType, $finfo->buffer($buf, FILEINFO_MIME_TYPE));
}
}
@@ -112,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 = [];
@@ -121,130 +208,139 @@ trait Sharing {
} else {
$options['auth'] = [$this->currentUser, $this->regularUser];
}
- $date = date('Y-m-d', strtotime("+3 days"));
- $options['body'] = ['expireDate' => $date];
- $this->response = $client->send($client->createRequest("PUT", $fullUrl, $options));
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
+ $date = date('Y-m-d', strtotime('+3 days'));
+ $options['form_params'] = ['expireDate' => $date];
+ $this->response = $this->response = $client->request('PUT', $fullUrl, $options);
+ Assert::assertEquals(200, $this->response->getStatusCode());
}
/**
* @When /^Updating last share with$/
- * @param \Behat\Gherkin\Node\TableNode|null $body
+ * @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 = [];
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
if ($this->currentUser === 'admin') {
$options['auth'] = $this->adminUser;
} else {
$options['auth'] = [$this->currentUser, $this->regularUser];
}
- if ($body instanceof \Behat\Gherkin\Node\TableNode) {
+ if ($body instanceof TableNode) {
$fd = $body->getRowsHash();
- if (array_key_exists('expireDate', $fd)){
+ if (array_key_exists('expireDate', $fd)) {
$dateModification = $fd['expireDate'];
$fd['expireDate'] = date('Y-m-d', strtotime($dateModification));
}
- $options['body'] = $fd;
+ $options['form_params'] = $fd;
}
try {
- $this->response = $client->send($client->createRequest("PUT", $fullUrl, $options));
+ $this->response = $client->request('PUT', $fullUrl, $options);
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
}
-
- PHPUnit_Framework_Assert::assertEquals(200, $this->response->getStatusCode());
}
public function createShare($user,
- $path = null,
- $shareType = null,
- $shareWith = null,
- $publicUpload = null,
- $password = null,
- $permissions = null){
+ $path = null,
+ $shareType = null,
+ $shareWith = null,
+ $publicUpload = null,
+ $password = null,
+ $permissions = null,
+ $viewOnly = false) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares";
$client = new Client();
- $options = [];
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
if ($user === 'admin') {
$options['auth'] = $this->adminUser;
} else {
$options['auth'] = [$user, $this->regularUser];
}
- $fd = [];
- if (!is_null($path)){
- $fd['path'] = $path;
+ $body = [];
+ if (!is_null($path)) {
+ $body['path'] = $path;
+ }
+ if (!is_null($shareType)) {
+ $body['shareType'] = $shareType;
}
- if (!is_null($shareType)){
- $fd['shareType'] = $shareType;
+ if (!is_null($shareWith)) {
+ $body['shareWith'] = $shareWith;
}
- if (!is_null($shareWith)){
- $fd['shareWith'] = $shareWith;
+ if (!is_null($publicUpload)) {
+ $body['publicUpload'] = $publicUpload;
}
- if (!is_null($publicUpload)){
- $fd['publicUpload'] = $publicUpload;
+ if (!is_null($password)) {
+ $body['password'] = $password;
}
- if (!is_null($password)){
- $fd['password'] = $password;
+ if (!is_null($permissions)) {
+ $body['permissions'] = $permissions;
}
- if (!is_null($permissions)){
- $fd['permissions'] = $permissions;
+
+ if ($viewOnly === true) {
+ $body['attributes'] = json_encode([['scope' => 'permissions', 'key' => 'download', 'value' => false]]);
}
- $options['body'] = $fd;
+ $options['form_params'] = $body;
try {
- $this->response = $client->send($client->createRequest("POST", $fullUrl, $options));
- $this->lastShareData = $this->response->xml();
+ $this->response = $client->request('POST', $fullUrl, $options);
+ $this->lastShareData = simplexml_load_string($this->response->getBody());
} catch (\GuzzleHttp\Exception\ClientException $ex) {
$this->response = $ex->getResponse();
+ throw new \Exception($this->response->getBody());
}
}
- public function isFieldInResponse($field, $contentExpected){
- $data = $this->response->xml()->data[0];
- if ((string)$field == 'expiration'){
- $contentExpected = date('Y-m-d', strtotime($contentExpected)) . " 00:00:00";
+ 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 (count($data->element) > 0){
- foreach($data as $element) {
- if ($contentExpected == "A_TOKEN"){
+ if (count($data->element) > 0) {
+ foreach ($data as $element) {
+ 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 ((string)$element->$field == $contentExpected){
- return True;
- }
- else{
+ } elseif ($contentExpected == 'AN_URL') {
+ return $this->isExpectedUrl((string)$element->$field, 'index.php/s/');
+ } elseif ((string)$element->$field == $contentExpected) {
+ return true;
+ } else {
print($element->$field);
}
}
- return False;
+ return false;
} else {
- if ($contentExpected == "A_TOKEN"){
- return (strlen((string)$data->$field) == 15);
- }
- elseif ($contentExpected == "A_NUMBER"){
- return is_numeric((string)$data->$field);
- }
- elseif($contentExpected == "AN_URL"){
- return $this->isExpectedUrl((string)$data->$field, "index.php/s/");
+ if ($contentExpected == 'A_TOKEN') {
+ return (strlen((string)$data->$field) == 15);
+ } 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 == $data->$field) {
+ return true;
+ } else {
+ print($data->$field);
}
- elseif ($data->$field == $contentExpected){
- return True;
- }
- return False;
+ return false;
}
}
@@ -253,8 +349,8 @@ trait Sharing {
*
* @param string $filename
*/
- public function checkSharedFileInResponse($filename){
- PHPUnit_Framework_Assert::assertEquals(True, $this->isFieldInResponse('file_target', "/$filename"));
+ public function checkSharedFileInResponse($filename) {
+ Assert::assertEquals(true, $this->isFieldInResponse('file_target', "/$filename"));
}
/**
@@ -262,8 +358,8 @@ trait Sharing {
*
* @param string $filename
*/
- public function checkSharedFileNotInResponse($filename){
- PHPUnit_Framework_Assert::assertEquals(False, $this->isFieldInResponse('file_target', "/$filename"));
+ public function checkSharedFileNotInResponse($filename) {
+ Assert::assertEquals(false, $this->isFieldInResponse('file_target', "/$filename"));
}
/**
@@ -271,8 +367,8 @@ trait Sharing {
*
* @param string $user
*/
- public function checkSharedUserInResponse($user){
- PHPUnit_Framework_Assert::assertEquals(True, $this->isFieldInResponse('share_with', "$user"));
+ public function checkSharedUserInResponse($user) {
+ Assert::assertEquals(true, $this->isFieldInResponse('share_with', "$user"));
}
/**
@@ -280,28 +376,32 @@ trait Sharing {
*
* @param string $user
*/
- public function checkSharedUserNotInResponse($user){
- PHPUnit_Framework_Assert::assertEquals(False, $this->isFieldInResponse('share_with', "$user"));
+ public function checkSharedUserNotInResponse($user) {
+ Assert::assertEquals(false, $this->isFieldInResponse('share_with', "$user"));
}
- public function isUserOrGroupInSharedData($userOrGroup){
- $data = $this->response->xml()->data[0];
- foreach($data as $element) {
- if ($element->share_with == $userOrGroup){
- return True;
+ public function isUserOrGroupInSharedData($userOrGroup, $permissions = null) {
+ $data = simplexml_load_string($this->response->getBody())->data[0];
+ foreach ($data as $element) {
+ if ($element->share_with == $userOrGroup && ($permissions === null || $permissions == $element->permissions)) {
+ return true;
}
}
- return False;
+ return false;
}
/**
- * @Given /^file "([^"]*)" of user "([^"]*)" is shared with user "([^"]*)"$/
+ * @Given /^(file|folder|entry) "([^"]*)" of user "([^"]*)" is shared with user "([^"]*)"( with permissions ([\d]*))?( view-only)?$/
*
* @param string $filepath
* @param string $user1
* @param string $user2
*/
- public function assureFileIsShared($filepath, $user1, $user2){
+ public function assureFileIsShared($entry, $filepath, $user1, $user2, $withPerms = null, $permissions = null, $viewOnly = null) {
+ // when view-only is set, permissions is empty string instead of null...
+ if ($permissions === '') {
+ $permissions = null;
+ }
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares" . "?path=$filepath";
$client = new Client();
$options = [];
@@ -310,24 +410,31 @@ trait Sharing {
} else {
$options['auth'] = [$user1, $this->regularUser];
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
- if ($this->isUserOrGroupInSharedData($user2)){
+ if ($this->isUserOrGroupInSharedData($user2, $permissions)) {
return;
} else {
- $this->createShare($user1, $filepath, 0, $user2, null, null, null);
+ $this->createShare($user1, $filepath, 0, $user2, null, null, $permissions, $viewOnly !== null);
}
$this->response = $client->get($fullUrl, $options);
- PHPUnit_Framework_Assert::assertEquals(True, $this->isUserOrGroupInSharedData($user2));
+ Assert::assertEquals(true, $this->isUserOrGroupInSharedData($user2, $permissions));
}
/**
- * @Given /^file "([^"]*)" of user "([^"]*)" is shared with group "([^"]*)"$/
+ * @Given /^(file|folder|entry) "([^"]*)" of user "([^"]*)" is shared with group "([^"]*)"( with permissions ([\d]*))?( view-only)?$/
*
* @param string $filepath
* @param string $user
* @param string $group
*/
- public function assureFileIsSharedWithGroup($filepath, $user, $group){
+ public function assureFileIsSharedWithGroup($entry, $filepath, $user, $group, $withPerms = null, $permissions = null, $viewOnly = null) {
+ // when view-only is set, permissions is empty string instead of null...
+ if ($permissions === '') {
+ $permissions = null;
+ }
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares" . "?path=$filepath";
$client = new Client();
$options = [];
@@ -336,79 +443,190 @@ trait Sharing {
} else {
$options['auth'] = [$user, $this->regularUser];
}
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
$this->response = $client->get($fullUrl, $options);
- if ($this->isUserOrGroupInSharedData($group)){
+ if ($this->isUserOrGroupInSharedData($group, $permissions)) {
return;
} else {
- $this->createShare($user, $filepath, 1, $group, null, null, null);
+ $this->createShare($user, $filepath, 1, $group, null, null, $permissions, $viewOnly !== null);
}
$this->response = $client->get($fullUrl, $options);
- PHPUnit_Framework_Assert::assertEquals(True, $this->isUserOrGroupInSharedData($group));
+ Assert::assertEquals(true, $this->isUserOrGroupInSharedData($group, $permissions));
}
/**
* @When /^Deleting last share$/
*/
- public function deletingLastShare(){
+ 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);
}
/**
* @When /^Getting info of last share$/
*/
- public function gettingInfoOfLastShare(){
+ 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);
}
/**
* @Then /^last share_id is included in the answer$/
*/
- public function checkingLastShareIDIsIncluded(){
+ public function checkingLastShareIDIsIncluded() {
$share_id = $this->lastShareData->data[0]->id;
- if (!$this->isFieldInResponse('id', $share_id)){
- PHPUnit_Framework_Assert::fail("Share id $share_id not found in response");
+ if (!$this->isFieldInResponse('id', $share_id)) {
+ Assert::fail("Share id $share_id not found in response");
}
}
/**
* @Then /^last share_id is not included in the answer$/
*/
- public function checkingLastShareIDIsNotIncluded(){
+ public function checkingLastShareIDIsNotIncluded() {
$share_id = $this->lastShareData->data[0]->id;
- if ($this->isFieldInResponse('id', $share_id)){
- PHPUnit_Framework_Assert::fail("Share id $share_id has been found in response");
+ if ($this->isFieldInResponse('id', $share_id)) {
+ Assert::fail("Share id $share_id has been found in response");
}
}
/**
* @Then /^Share fields of last share match with$/
- * @param \Behat\Gherkin\Node\TableNode|null $body
+ * @param TableNode|null $body
*/
- public function checkShareFields($body){
- if ($body instanceof \Behat\Gherkin\Node\TableNode) {
+ public function checkShareFields($body) {
+ if ($body instanceof TableNode) {
$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);
+ 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, 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)){
- PHPUnit_Framework_Assert::fail("$field" . " doesn't have value " . "$value");
+ if (!$this->isFieldInResponse($field, $value)) {
+ Assert::fail("$field" . " doesn't have value " . "$value");
}
}
}
}
/**
+ * @Then the list of returned shares has :count shares
+ */
+ public function theListOfReturnedSharesHasShares(int $count) {
+ $this->theHTTPStatusCodeShouldBe('200');
+ $this->theOCSStatusCodeShouldBe('100');
+
+ $returnedShares = $this->getXmlResponse()->data[0];
+
+ Assert::assertEquals($count, count($returnedShares->element));
+ }
+
+ /**
+ * @Then share :count is returned with
+ *
+ * @param int $number
+ * @param TableNode $body
+ */
+ public function shareXIsReturnedWith(int $number, TableNode $body) {
+ $this->theHTTPStatusCodeShouldBe('200');
+ $this->theOCSStatusCodeShouldBe('100');
+
+ if (!($body instanceof TableNode)) {
+ return;
+ }
+
+ $returnedShare = $this->getXmlResponse()->data[0];
+ if ($returnedShare->element) {
+ $returnedShare = $returnedShare->element[$number];
+ }
+
+ $defaultExpectedFields = [
+ 'id' => 'A_NUMBER',
+ 'permissions' => '19',
+ 'stime' => 'A_NUMBER',
+ 'parent' => '',
+ 'expiration' => '',
+ 'token' => '',
+ 'storage' => 'A_NUMBER',
+ 'item_source' => 'A_NUMBER',
+ 'file_source' => 'A_NUMBER',
+ 'file_parent' => 'A_NUMBER',
+ 'mail_send' => '0'
+ ];
+ $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash());
+
+ 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)) {
+ $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 ($expectedFields['share_with'] === 'private_conversation') {
+ $expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/';
+ } else {
+ $expectedFields['share_with'] = FeatureContext::getTokenForIdentifier($expectedFields['share_with']);
+ }
+ }
+
+ foreach ($expectedFields as $field => $value) {
+ $this->assertFieldIsInReturnedShare($field, $value, $returnedShare);
+ }
+ }
+
+ /**
+ * @return SimpleXMLElement
+ */
+ private function getXmlResponse(): \SimpleXMLElement {
+ return simplexml_load_string($this->response->getBody());
+ }
+
+ /**
+ * @param string $field
+ * @param string $contentExpected
+ * @param \SimpleXMLElement $returnedShare
+ */
+ private function assertFieldIsInReturnedShare(string $field, string $contentExpected, \SimpleXMLElement $returnedShare) {
+ if ($contentExpected === 'IGNORE') {
+ return;
+ }
+
+ if (!property_exists($returnedShare, $field)) {
+ Assert::fail("$field was not found in response");
+ }
+
+ if ($field === 'expiration' && !empty($contentExpected)) {
+ $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00';
+ }
+
+ if ($contentExpected === 'A_NUMBER') {
+ Assert::assertTrue(is_numeric((string)$returnedShare->$field), "Field '$field' is not a number: " . $returnedShare->$field);
+ } elseif ($contentExpected === 'A_TOKEN') {
+ // A token is composed by 15 characters from
+ // ISecureRandom::CHAR_HUMAN_READABLE.
+ Assert::assertRegExp('/^[abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789]{15}$/', (string)$returnedShare->$field, "Field '$field' is not a token");
+ } elseif (strpos($contentExpected, 'REGEXP ') === 0) {
+ Assert::assertRegExp(substr($contentExpected, strlen('REGEXP ')), (string)$returnedShare->$field, "Field '$field' does not match");
+ } else {
+ Assert::assertEquals($contentExpected, (string)$returnedShare->$field, "Field '$field' does not match");
+ }
+ }
+
+ /**
* @Then As :user remove all shares from the file named :fileName
*/
public function asRemoveAllSharesFromTheFileNamed($user, $fileName) {
@@ -423,12 +641,13 @@ trait Sharing {
],
'headers' => [
'Content-Type' => 'application/json',
+ 'OCS-APIREQUEST' => 'true',
],
]
);
$json = json_decode($res->getBody()->getContents(), true);
$deleted = false;
- foreach($json['ocs']['data'] as $data) {
+ foreach ($json['ocs']['data'] as $data) {
if (stripslashes($data['path']) === $fileName) {
$id = $data['id'];
$client->delete(
@@ -440,6 +659,7 @@ trait Sharing {
],
'headers' => [
'Content-Type' => 'application/json',
+ 'OCS-APIREQUEST' => 'true',
],
]
);
@@ -447,7 +667,7 @@ trait Sharing {
}
}
- if($deleted === false) {
+ if ($deleted === false) {
throw new \Exception("Could not delete file $fileName");
}
}
@@ -455,19 +675,67 @@ trait Sharing {
/**
* @When save last share id
*/
- public function saveLastShareId()
- {
- $this->savedShareId = $this->lastShareData['data']['id'];
+ public function saveLastShareId() {
+ $this->savedShareId = ($this->lastShareData['data']['id'] ?? null);
}
/**
* @Then share ids should match
*/
- public function shareIdsShouldMatch()
- {
- if ($this->savedShareId !== $this->lastShareData['data']['id']) {
+ public function shareIdsShouldMatch() {
+ if ($this->savedShareId !== ($this->lastShareData['data']['id'] ?? null)) {
throw new \Exception('Expected the same link share to be returned');
}
}
-}
+ /**
+ * @When /^getting sharees for$/
+ * @param TableNode $body
+ */
+ public function whenGettingShareesFor($body) {
+ $url = '/apps/files_sharing/api/v1/sharees';
+ if ($body instanceof TableNode) {
+ $parameters = [];
+ foreach ($body->getRowsHash() as $key => $value) {
+ $parameters[] = $key . '=' . $value;
+ }
+ if (!empty($parameters)) {
+ $url .= '?' . implode('&', $parameters);
+ }
+ }
+
+ $this->sendingTo('GET', $url);
+ }
+
+ /**
+ * @Then /^"([^"]*)" sharees returned (are|is empty)$/
+ * @param string $shareeType
+ * @param string $isEmpty
+ * @param TableNode|null $shareesList
+ */
+ public function thenListOfSharees($shareeType, $isEmpty, $shareesList = null) {
+ if ($isEmpty !== 'is empty') {
+ $sharees = $shareesList->getRows();
+ $respondedArray = $this->getArrayOfShareesResponded($this->response, $shareeType);
+ Assert::assertEquals($sharees, $respondedArray);
+ } else {
+ $respondedArray = $this->getArrayOfShareesResponded($this->response, $shareeType);
+ Assert::assertEmpty($respondedArray);
+ }
+ }
+
+ public function getArrayOfShareesResponded(ResponseInterface $response, $shareeType) {
+ $elements = simplexml_load_string($response->getBody())->data;
+ $elements = json_decode(json_encode($elements), 1);
+ if (strpos($shareeType, 'exact ') === 0) {
+ $elements = $elements['exact'];
+ $shareeType = substr($shareeType, 6);
+ }
+
+ $sharees = [];
+ foreach ($elements[$shareeType] as $element) {
+ $sharees[] = [$element['label'], $element['value']['shareType'], $element['value']['shareWith']];
+ }
+ return $sharees;
+ }
+}
diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php
new file mode 100644
index 00000000000..a9dd99108a9
--- /dev/null
+++ b/build/integration/features/bootstrap/SharingContext.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 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';
+
+
+/**
+ * Features context.
+ */
+class SharingContext implements Context, SnippetAcceptingContext {
+ use WebDav;
+ use Trashbin;
+ use AppConfiguration;
+ use CommandLine;
+ use Activity;
+
+ protected function resetAppConfigs() {
+ $this->deleteServerConfig('core', 'shareapi_default_permissions');
+ $this->deleteServerConfig('core', 'shareapi_default_internal_expire_date');
+ $this->deleteServerConfig('core', 'shareapi_internal_expire_after_n_days');
+ $this->deleteServerConfig('core', 'internal_defaultExpDays');
+ $this->deleteServerConfig('core', 'shareapi_enforce_links_password');
+ $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 10d0b9ae545..c64626de68d 100644
--- a/build/integration/features/bootstrap/TagsContext.php
+++ b/build/integration/features/bootstrap/TagsContext.php
@@ -1,24 +1,10 @@
<?php
+
/**
- * @author Lukas Reschke <lukas@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
require __DIR__ . '/../../vendor/autoload.php';
use Behat\Gherkin\Node\TableNode;
@@ -26,7 +12,7 @@ use GuzzleHttp\Client;
use GuzzleHttp\Message\ResponseInterface;
class TagsContext implements \Behat\Behat\Context\Context {
- /** @var string */
+ /** @var string */
private $baseUrl;
/** @var Client */
private $client;
@@ -47,7 +33,7 @@ class TagsContext implements \Behat\Behat\Context\Context {
}
/** @BeforeScenario */
- public function tearUpScenario() {
+ public function setUpScenario() {
$this->client = new Client();
}
@@ -55,9 +41,9 @@ class TagsContext implements \Behat\Behat\Context\Context {
public function tearDownScenario() {
$user = 'admin';
$tags = $this->requestTagsForUser($user);
- foreach($tags as $tagId => $tag) {
+ foreach ($tags as $tagId => $tag) {
$this->response = $this->client->delete(
- $this->baseUrl . '/remote.php/dav/systemtags/'.$tagId,
+ $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
[
'auth' => [
$user,
@@ -82,7 +68,8 @@ class TagsContext implements \Behat\Behat\Context\Context {
],
]
);
- } catch (\GuzzleHttp\Exception\ClientException $e) {}
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ }
}
/**
@@ -90,35 +77,43 @@ class TagsContext implements \Behat\Behat\Context\Context {
* @return string
*/
private function getPasswordForUser($userName) {
- if($userName === 'admin') {
+ if ($userName === 'admin') {
return 'admin';
}
return '123456';
}
/**
- * @When :user creates a :type tag with name :name
* @param string $user
* @param string $type
* @param string $name
- * @throws \Exception
+ * @param string $groups
*/
- public function createsATagWithName($user, $type, $name) {
- $userVisible = 'true';
- $userAssignable = 'true';
+ private function createTag($user, $type, $name, $groups = null) {
+ $userVisible = true;
+ $userAssignable = true;
switch ($type) {
case 'normal':
break;
case 'not user-assignable':
- $userAssignable = 'false';
+ $userAssignable = false;
break;
case 'not user-visible':
- $userVisible = 'false';
+ $userVisible = false;
break;
default:
throw new \Exception('Unsupported type');
}
+ $body = [
+ 'name' => $name,
+ 'userVisible' => $userVisible,
+ 'userAssignable' => $userAssignable,
+ ];
+ if ($groups !== null) {
+ $body['groups'] = $groups;
+ }
+
try {
$this->response = $this->client->post(
$this->baseUrl . '/remote.php/dav/systemtags/',
@@ -130,22 +125,45 @@ class TagsContext implements \Behat\Behat\Context\Context {
'headers' => [
'Content-Type' => 'application/json',
],
- 'body' => '{"name":"'.$name.'","userVisible":'.$userVisible.',"userAssignable":'.$userAssignable.'}',
+ 'body' => json_encode($body)
]
);
- } catch (\GuzzleHttp\Exception\ClientException $e){
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
$this->response = $e->getResponse();
}
}
/**
+ * @When :user creates a :type tag with name :name
+ * @param string $user
+ * @param string $type
+ * @param string $name
+ * @throws \Exception
+ */
+ public function createsATagWithName($user, $type, $name) {
+ $this->createTag($user, $type, $name);
+ }
+
+ /**
+ * @When :user creates a :type tag with name :name and groups :groups
+ * @param string $user
+ * @param string $type
+ * @param string $name
+ * @param string $groups
+ * @throws \Exception
+ */
+ public function createsATagWithNameAndGroups($user, $type, $name, $groups) {
+ $this->createTag($user, $type, $name, $groups);
+ }
+
+ /**
* @Then The response should have a status code :statusCode
* @param int $statusCode
* @throws \Exception
*/
public function theResponseShouldHaveAStatusCode($statusCode) {
- if((int)$statusCode !== $this->response->getStatusCode()) {
- throw new \Exception("Expected $statusCode, got ".$this->response->getStatusCode());
+ if ((int)$statusCode !== $this->response->getStatusCode()) {
+ throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode());
}
}
@@ -155,21 +173,30 @@ class TagsContext implements \Behat\Behat\Context\Context {
* @param string $user
* @return array
*/
- private function requestTagsForUser($user) {
+ private function requestTagsForUser($user, $withGroups = false) {
try {
- $request = $this->client->createRequest(
- 'PROPFIND',
- $this->baseUrl . '/remote.php/dav/systemtags/',
- [
- 'body' => '<?xml version="1.0"?>
+ $body = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:id />
<oc:display-name />
<oc:user-visible />
- <oc:user-assignable />
+ <oc:user-assignable />
+ <oc:can-assign />
+';
+
+ if ($withGroups) {
+ $body .= '<oc:groups />';
+ }
+
+ $body .= '
</d:prop>
-</d:propfind>',
+</d:propfind>';
+ $this->response = $this->client->request(
+ 'PROPFIND',
+ $this->baseUrl . '/remote.php/dav/systemtags/',
+ [
+ 'body' => $body,
'auth' => [
$user,
$this->getPasswordForUser($user),
@@ -179,7 +206,6 @@ class TagsContext implements \Behat\Behat\Context\Context {
],
]
);
- $this->response = $this->client->send($request);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$this->response = $e->getResponse();
}
@@ -187,17 +213,22 @@ class TagsContext implements \Behat\Behat\Context\Context {
$tags = [];
$service = new Sabre\Xml\Service();
$parsed = $service->parse($this->response->getBody()->getContents());
- foreach($parsed as $entry) {
+ foreach ($parsed as $entry) {
$singleEntry = $entry['value'][1]['value'][0]['value'];
- if(empty($singleEntry[0]['value'])) {
+ if (empty($singleEntry[0]['value'])) {
continue;
}
+ // FIXME: use actual property names instead of guessing index position
$tags[$singleEntry[0]['value']] = [
'display-name' => $singleEntry[1]['value'],
'user-visible' => $singleEntry[2]['value'],
'user-assignable' => $singleEntry[3]['value'],
+ 'can-assign' => $singleEntry[4]['value'],
];
+ if (isset($singleEntry[5])) {
+ $tags[$singleEntry[0]['value']]['groups'] = $singleEntry[5]['value'];
+ }
}
return $tags;
@@ -212,42 +243,117 @@ class TagsContext implements \Behat\Behat\Context\Context {
public function theFollowingTagsShouldExistFor($user, TableNode $table) {
$tags = $this->requestTagsForUser($user);
- if(count($table->getRows()) !== count($tags)) {
+ 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)
)
);
}
- 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]
+ 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]
) {
unset($tags[$key]);
}
}
}
- if(count($tags) !== 0) {
+ if (count($tags) !== 0) {
throw new \Exception('Not expected response');
}
}
/**
+ * @Then the user :user :can assign The :type tag with name :tagName
+ */
+ public function theUserCanAssignTheTag($user, $can, $type, $tagName) {
+ $foundTag = $this->findTag($type, $tagName, $user);
+ if ($foundTag === null) {
+ throw new \Exception('No matching tag found');
+ }
+
+ if ($can === 'can') {
+ $expected = 'true';
+ } elseif ($can === 'cannot') {
+ $expected = 'false';
+ } else {
+ throw new \Exception('Invalid condition, must be "can" or "cannot"');
+ }
+
+ if ($foundTag['can-assign'] !== $expected) {
+ throw new \Exception('Tag cannot be assigned by user');
+ }
+ }
+
+ /**
+ * @Then The :type tag with name :tagName has the groups :groups
+ */
+ public function theTagHasGroup($type, $tagName, $groups) {
+ $foundTag = $this->findTag($type, $tagName, 'admin', true);
+ if ($foundTag === null) {
+ throw new \Exception('No matching tag found');
+ }
+
+ if ($foundTag['groups'] !== $groups) {
+ throw new \Exception('Tag has groups "' . $foundTag['group'] . '" instead of the expected "' . $groups . '"');
+ }
+ }
+
+ /**
* @Then :count tags should exist for :user
* @param int $count
* @param string $user
* @throws \Exception
*/
- public function tagsShouldExistFor($count, $user) {
- if((int)$count !== count($this->requestTagsForUser($user))) {
- throw new \Exception("Expected $count tags, got ".count($this->requestTagsForUser($user)));
+ public function tagsShouldExistFor($count, $user) {
+ if ((int)$count !== count($this->requestTagsForUser($user))) {
+ throw new \Exception("Expected $count tags, got " . count($this->requestTagsForUser($user)));
+ }
+ }
+
+ /**
+ * Find tag by type and name
+ *
+ * @param string $type tag type
+ * @param string $tagName tag name
+ * @param string $user retrieved from which user
+ * @param bool $withGroups whether to also query the tag's groups
+ *
+ * @return array tag values or null if not found
+ */
+ private function findTag($type, $tagName, $user = 'admin', $withGroups = false) {
+ $tags = $this->requestTagsForUser($user, $withGroups);
+ $userAssignable = 'true';
+ $userVisible = 'true';
+ switch ($type) {
+ case 'normal':
+ break;
+ case 'not user-assignable':
+ $userAssignable = 'false';
+ break;
+ case 'not user-visible':
+ $userVisible = 'false';
+ break;
+ default:
+ throw new \Exception('Unsupported type');
}
+
+ $foundTag = null;
+ foreach ($tags as $tag) {
+ if ($tag['display-name'] === $tagName
+ && $tag['user-visible'] === $userVisible
+ && $tag['user-assignable'] === $userAssignable) {
+ $foundTag = $tag;
+ break;
+ }
+ }
+ return $foundTag;
}
/**
@@ -257,8 +363,8 @@ class TagsContext implements \Behat\Behat\Context\Context {
private function findTagIdByName($name) {
$tags = $this->requestTagsForUser('admin');
$tagId = 0;
- foreach($tags as $id => $tag) {
- if($tag['display-name'] === $name) {
+ foreach ($tags as $id => $tag) {
+ if ($tag['display-name'] === $name) {
$tagId = $id;
break;
}
@@ -275,12 +381,12 @@ class TagsContext implements \Behat\Behat\Context\Context {
*/
public function editsTheTagWithNameAndSetsItsNameTo($user, $oldName, $newName) {
$tagId = $this->findTagIdByName($oldName);
- if($tagId === 0) {
+ if ($tagId === 0) {
throw new \Exception('Could not find tag to rename');
}
try {
- $request = $this->client->createRequest(
+ $this->response = $this->client->request(
'PROPPATCH',
$this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
[
@@ -298,7 +404,43 @@ class TagsContext implements \Behat\Behat\Context\Context {
],
]
);
- $this->response = $this->client->send($request);
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * @When :user edits the tag with name :oldNmae and sets its groups to :groups
+ * @param string $user
+ * @param string $oldName
+ * @param string $groups
+ * @throws \Exception
+ */
+ public function editsTheTagWithNameAndSetsItsGroupsTo($user, $oldName, $groups) {
+ $tagId = $this->findTagIdByName($oldName);
+ if ($tagId === 0) {
+ throw new \Exception('Could not find tag to rename');
+ }
+
+ try {
+ $this->response = $this->client->request(
+ 'PROPPATCH',
+ $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
+ [
+ 'body' => '<?xml version="1.0"?>
+<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
+ <d:set>
+ <d:prop>
+ <oc:groups>' . $groups . '</oc:groups>
+ </d:prop>
+ </d:set>
+</d:propertyupdate>',
+ 'auth' => [
+ $user,
+ $this->getPasswordForUser($user),
+ ],
+ ]
+ );
} catch (\GuzzleHttp\Exception\ClientException $e) {
$this->response = $e->getResponse();
}
@@ -309,7 +451,7 @@ class TagsContext implements \Behat\Behat\Context\Context {
* @param string $user
* @param string $name
*/
- public function deletesTheTagWithName($user, $name) {
+ public function deletesTheTagWithName($user, $name) {
$tagId = $this->findTagIdByName($name);
try {
$this->response = $this->client->delete(
@@ -335,10 +477,10 @@ class TagsContext implements \Behat\Behat\Context\Context {
* @return int
*/
private function getFileIdForPath($path, $user) {
- $url = $this->baseUrl.'/remote.php/webdav/'.$path;
- $credentials = base64_encode($user .':'.$this->getPasswordForUser($user));
- $context = stream_context_create(array(
- 'http' => array(
+ $url = $this->baseUrl . '/remote.php/webdav/' . $path;
+ $credentials = base64_encode($user . ':' . $this->getPasswordForUser($user));
+ $context = stream_context_create([
+ 'http' => [
'method' => 'PROPFIND',
'header' => "Authorization: Basic $credentials\r\nContent-Type: application/x-www-form-urlencoded",
'content' => '<?xml version="1.0"?>
@@ -347,27 +489,27 @@ class TagsContext implements \Behat\Behat\Context\Context {
<oc:fileid />
</d:prop>
</d:propfind>'
- )
- ));
+ ]
+ ]);
$response = file_get_contents($url, false, $context);
- preg_match_all('/\<oc:fileid\>(.*)\<\/oc:fileid\>/', $response, $matches);
+ preg_match_all('/\<oc:fileid\>(.*?)\<\/oc:fileid\>/', $response, $matches);
return (int)$matches[1][0];
}
/**
- * @When :taggingUser adds the tag :tagName to :fileName shared by :sharingUser
+ * @When /^"([^"]*)" adds the tag "([^"]*)" to "([^"]*)" (shared|owned) by "([^"]*)"$/
* @param string $taggingUser
* @param string $tagName
* @param string $fileName
* @param string $sharingUser
*/
- public function addsTheTagToSharedBy($taggingUser, $tagName, $fileName, $sharingUser) {
+ public function addsTheTagToSharedBy($taggingUser, $tagName, $fileName, $sharedOrOwnedBy, $sharingUser) {
$fileId = $this->getFileIdForPath($fileName, $sharingUser);
$tagId = $this->findTagIdByName($tagName);
try {
$this->response = $this->client->put(
- $this->baseUrl.'/remote.php/dav/systemtags-relations/files/'.$fileId.'/'.$tagId,
+ $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $fileId . '/' . $tagId,
[
'auth' => [
$taggingUser,
@@ -381,23 +523,23 @@ class TagsContext implements \Behat\Behat\Context\Context {
}
/**
- * @Then :fileName shared by :sharingUser has the following tags
+ * @Then /^"([^"]*)" (shared|owned) by "([^"]*)" has the following tags$/
* @param string $fileName
* @param string $sharingUser
* @param TableNode $table
* @throws \Exception
*/
- public function sharedByHasTheFollowingTags($fileName, $sharingUser, TableNode $table) {
+ public function sharedByHasTheFollowingTags($fileName, $sharedOrOwnedBy, $sharingUser, TableNode $table) {
$loadedExpectedTags = $table->getTable();
$expectedTags = [];
- foreach($loadedExpectedTags as $expected) {
+ foreach ($loadedExpectedTags as $expected) {
$expectedTags[] = $expected[0];
}
// Get the real tags
- $request = $this->client->createRequest(
+ $response = $this->client->request(
'PROPFIND',
- $this->baseUrl.'/remote.php/dav/systemtags-relations/files/'.$this->getFileIdForPath($fileName, $sharingUser),
+ $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser),
[
'auth' => [
$sharingUser,
@@ -413,19 +555,18 @@ class TagsContext implements \Behat\Behat\Context\Context {
</d:prop>
</d:propfind>',
]
- );
- $response = $this->client->send($request)->getBody()->getContents();
- preg_match_all('/\<oc:display-name\>(.*)\<\/oc:display-name\>/', $response, $realTags);
+ )->getBody()->getContents();
+ preg_match_all('/\<oc:display-name\>(.*?)\<\/oc:display-name\>/', $response, $realTags);
- foreach($expectedTags as $key => $row) {
- foreach($realTags as $tag) {
- if($tag[0] === $row) {
+ foreach ($expectedTags as $key => $row) {
+ foreach ($realTags as $tag) {
+ if ($tag[0] === $row) {
unset($expectedTags[$key]);
}
}
}
- if(count($expectedTags) !== 0) {
+ if (count($expectedTags) !== 0) {
throw new \Exception('Not all tags found.');
}
}
@@ -441,13 +582,13 @@ class TagsContext implements \Behat\Behat\Context\Context {
public function sharedByHasTheFollowingTagsFor($fileName, $sharingUser, $user, TableNode $table) {
$loadedExpectedTags = $table->getTable();
$expectedTags = [];
- foreach($loadedExpectedTags as $expected) {
+ foreach ($loadedExpectedTags as $expected) {
$expectedTags[] = $expected[0];
}
// Get the real tags
try {
- $request = $this->client->createRequest(
+ $this->response = $this->client->request(
'PROPFIND',
$this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser),
[
@@ -466,25 +607,24 @@ class TagsContext implements \Behat\Behat\Context\Context {
</d:propfind>',
]
);
- $this->response = $this->client->send($request)->getBody()->getContents();
} catch (\GuzzleHttp\Exception\ClientException $e) {
$this->response = $e->getResponse();
}
- preg_match_all('/\<oc:display-name\>(.*)\<\/oc:display-name\>/', $this->response, $realTags);
+ preg_match_all('/\<oc:display-name\>(.*?)\<\/oc:display-name\>/', $this->response->getBody()->getContents(), $realTags);
$realTags = array_filter($realTags);
$expectedTags = array_filter($expectedTags);
- foreach($expectedTags as $key => $row) {
- foreach($realTags as $tag) {
- foreach($tag as $index => $foo) {
- if($tag[$index] === $row) {
+ foreach ($expectedTags as $key => $row) {
+ foreach ($realTags as $tag) {
+ foreach ($tag as $index => $foo) {
+ if ($tag[$index] === $row) {
unset($expectedTags[$key]);
}
}
}
}
- if(count($expectedTags) !== 0) {
+ if (count($expectedTags) !== 0) {
throw new \Exception('Not all tags found.');
}
}
@@ -502,7 +642,7 @@ class TagsContext implements \Behat\Behat\Context\Context {
try {
$this->response = $this->client->delete(
- $this->baseUrl.'/remote.php/dav/systemtags-relations/files/'.$fileId.'/'.$tagId,
+ $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $fileId . '/' . $tagId,
[
'auth' => [
$user,
diff --git a/build/integration/features/bootstrap/TalkContext.php b/build/integration/features/bootstrap/TalkContext.php
new file mode 100644
index 00000000000..6f351c30ccf
--- /dev/null
+++ b/build/integration/features/bootstrap/TalkContext.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use Behat\Behat\Context\Context;
+
+class TalkContext implements Context {
+ /**
+ * @BeforeFeature @Talk
+ * @BeforeScenario @Talk
+ */
+ public static function skipTestsIfTalkIsNotInstalled() {
+ if (!TalkContext::isTalkInstalled()) {
+ throw new Exception('Talk needs to be installed to run features or scenarios tagged with @Talk');
+ }
+ }
+
+ /**
+ * @AfterScenario @Talk
+ */
+ public static function disableTalk() {
+ TalkContext::runOcc(['app:disable', 'spreed']);
+ }
+
+ private static function isTalkInstalled(): bool {
+ $appList = TalkContext::runOcc(['app:list']);
+
+ return strpos($appList, 'spreed') !== false;
+ }
+
+ private static function runOcc(array $args): string {
+ // Based on "runOcc" from CommandLine trait (which can not be used due
+ // to not being static and being already used in other sibling
+ // contexts).
+ $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);
+
+ return $lastStdOut;
+ }
+}
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
new file mode 100644
index 00000000000..dfcc23289a7
--- /dev/null
+++ b/build/integration/features/bootstrap/Trashbin.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+use DMS\PHPUnitExtensions\ArraySubset\Assert as AssertArraySubset;
+use PHPUnit\Framework\Assert;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+/**
+ * Trashbin functions
+ */
+trait Trashbin {
+ // WebDav trait is expected to be used in the class that uses this trait.
+
+ /**
+ * @When User :user empties trashbin
+ * @param string $user user
+ */
+ public function emptyTrashbin($user) {
+ $client = $this->getSabreClient($user);
+ $response = $client->request('DELETE', $this->makeSabrePath($user, 'trash', 'trashbin'));
+ Assert::assertEquals(204, $response['statusCode']);
+ }
+
+ private function findFullTrashname($user, $name) {
+ $rootListing = $this->listTrashbinFolder($user, '/');
+
+ foreach ($rootListing as $href => $rootItem) {
+ if ($rootItem['{http://nextcloud.org/ns}trashbin-filename'] === $name) {
+ return basename($href);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the full /startofpath.dxxxx/rest/of/path from /startofpath/rest/of/path
+ */
+ private function getFullTrashPath($user, $path) {
+ if ($path !== '' && $path !== '/') {
+ $parts = explode('/', $path);
+ $fullName = $this->findFullTrashname($user, $parts[1]);
+ if ($fullName === null) {
+ Assert::fail("cant find $path in trash");
+ return '/dummy_full_path_not_found';
+ }
+ $parts[1] = $fullName;
+
+ $path = implode('/', $parts);
+ }
+ return $path;
+ }
+
+ /**
+ * List trashbin folder
+ *
+ * @param string $user user
+ * @param string $path path
+ * @return array response
+ */
+ public function listTrashbinFolder($user, $path) {
+ $path = $this->getFullTrashPath($user, $path);
+ $client = $this->getSabreClient($user);
+
+ $results = $client->propfind($this->makeSabrePath($user, 'trash' . $path, 'trashbin'), [
+ '{http://nextcloud.org/ns}trashbin-filename',
+ '{http://nextcloud.org/ns}trashbin-original-location',
+ '{http://nextcloud.org/ns}trashbin-deletion-time'
+ ], 1);
+ $results = array_filter($results, function (array $item) {
+ return isset($item['{http://nextcloud.org/ns}trashbin-filename']);
+ });
+ if ($path !== '' && $path !== '/') {
+ array_shift($results);
+ }
+ return $results;
+ }
+
+ /**
+ * @Then /^user "([^"]*)" in trash folder "([^"]*)" should have the following elements$/
+ * @param string $user
+ * @param string $folder
+ * @param \Behat\Gherkin\Node\TableNode|null $expectedElements
+ */
+ public function checkTrashContents($user, $folder, $expectedElements) {
+ $elementList = $this->listTrashbinFolder($user, $folder);
+ $trashContent = array_filter(array_map(function (array $item) {
+ return $item['{http://nextcloud.org/ns}trashbin-filename'];
+ }, $elementList));
+ if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) {
+ $elementRows = $expectedElements->getRows();
+ $elementsSimplified = $this->simplifyArray($elementRows);
+ foreach ($elementsSimplified as $expectedElement) {
+ $expectedElement = ltrim($expectedElement, '/');
+ if (array_search($expectedElement, $trashContent) === false) {
+ Assert::fail("$expectedElement" . ' is not in trash listing');
+ }
+ }
+ }
+ }
+
+ /**
+ * @Then /^as "([^"]*)" the (file|folder) "([^"]*)" exists in trash$/
+ * @param string $user
+ * @param string $type
+ * @param string $file
+ */
+ public function checkTrashContains($user, $type, $file) {
+ $parent = dirname($file);
+ if ($parent === '.') {
+ $parent = '/';
+ }
+ $name = basename($file);
+ $elementList = $this->listTrashbinFolder($user, $parent);
+ $trashContent = array_filter(array_map(function (array $item) {
+ return $item['{http://nextcloud.org/ns}trashbin-filename'];
+ }, $elementList));
+
+ AssertArraySubset::assertArraySubset([$name], array_values($trashContent));
+ }
+
+ /**
+ * @Then /^user "([^"]*)" in trash folder "([^"]*)" should have (\d+) elements?$/
+ * @param string $user
+ * @param string $folder
+ * @param \Behat\Gherkin\Node\TableNode|null $expectedElements
+ */
+ public function checkTrashSize($user, $folder, $expectedCount) {
+ $elementList = $this->listTrashbinFolder($user, $folder);
+ Assert::assertEquals($expectedCount, count($elementList));
+ }
+
+ /**
+ * @When /^user "([^"]*)" in restores "([^"]*)" from trash$/
+ * @param string $user
+ * @param string $file
+ */
+ public function restoreFromTrash($user, $file) {
+ $file = $this->getFullTrashPath($user, $file);
+ $url = $this->makeSabrePath($user, 'trash' . $file, 'trashbin');
+ $client = $this->getSabreClient($user);
+ $response = $client->request('MOVE', $url, null, [
+ 'Destination' => $this->makeSabrePath($user, 'restore/' . basename($file), 'trashbin'),
+ ]);
+ Assert::assertEquals(201, $response['statusCode']);
+ return;
+ }
+}
diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php
index 2ef5f252f11..2cb37002ac0 100644
--- a/build/integration/features/bootstrap/WebDav.php
+++ b/build/integration/features/bootstrap/WebDav.php
@@ -1,8 +1,16 @@
<?php
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
use GuzzleHttp\Client as GClient;
-use GuzzleHttp\Message\ResponseInterface;
+use PHPUnit\Framework\Assert;
+use Psr\Http\Message\ResponseInterface;
use Sabre\DAV\Client as SClient;
+use Sabre\DAV\Xml\Property\ResourceType;
require __DIR__ . '/../../vendor/autoload.php';
@@ -10,10 +18,17 @@ require __DIR__ . '/../../vendor/autoload.php';
trait WebDav {
use Sharing;
- /** @var string*/
- private $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;
/** @var ResponseInterface */
private $response;
+ private array $parsedResponse = [];
+ private string $s3MultipartDestination;
+ private string $uploadId;
+ /** @var string[] */
+ private array $parts = [];
/**
* @Given /^using dav path "([^"]*)"$/
@@ -22,52 +37,103 @@ trait WebDav {
$this->davPath = $davPath;
}
- public function makeDavRequest($user, $method, $path, $headers, $body = null){
- $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . "$path";
+ /**
+ * @Given /^using old dav path$/
+ */
+ public function usingOldDavPath() {
+ $this->davPath = 'remote.php/webdav';
+ $this->usingOldDavPath = true;
+ }
+
+ /**
+ * @Given /^using new dav path$/
+ */
+ public function usingNewDavPath() {
+ $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;
+ }
+
+ public function getDavFilesPath($user) {
+ if ($this->usingOldDavPath === true) {
+ return $this->davPath;
+ } else {
+ return $this->davPath . '/files/' . $user;
+ }
+ }
+
+ 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') {
+ $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . "$path";
+ } else {
+ $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path";
+ }
$client = new GClient();
- $options = [];
+ $options = [
+ 'headers' => $headers,
+ 'body' => $body
+ ];
if ($user === 'admin') {
$options['auth'] = $this->adminUser;
- } else {
+ } elseif ($user !== '') {
$options['auth'] = [$user, $this->regularUser];
}
- $request = $client->createRequest($method, $fullUrl, $options);
- if (!is_null($headers)){
- foreach ($headers as $key => $value) {
- $request->addHeader($key, $value);
- }
- }
-
- if (!is_null($body)) {
- $request->setBody($body);
- }
+ return $client->request($method, $fullUrl, $options);
+ }
- return $client->send($request);
+ /**
+ * @Given /^User "([^"]*)" moved (file|folder|entry) "([^"]*)" to "([^"]*)"$/
+ * @param string $user
+ * @param string $fileSource
+ * @param string $fileDestination
+ */
+ 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);
+ Assert::assertEquals(201, $this->response->getStatusCode());
}
/**
- * @Given /^User "([^"]*)" moved file "([^"]*)" to "([^"]*)"$/
+ * @When /^User "([^"]*)" moves (file|folder|entry) "([^"]*)" to "([^"]*)"$/
* @param string $user
* @param string $fileSource
* @param string $fileDestination
*/
- public function userMovedFile($user, $fileSource, $fileDestination){
- $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath;
+ public function userMovesFile($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);
- PHPUnit_Framework_Assert::assertEquals(201, $this->response->getStatusCode());
+ try {
+ $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers);
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
}
/**
- * @When /^User "([^"]*)" moves file "([^"]*)" to "([^"]*)"$/
+ * @When /^User "([^"]*)" copies file "([^"]*)" to "([^"]*)"$/
* @param string $user
* @param string $fileSource
* @param string $fileDestination
*/
- public function userMovesFile($user, $fileSource, $fileDestination){
- $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath;
+ public function userCopiesFileTo($user, $fileSource, $fileDestination) {
+ $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user);
$headers['Destination'] = $fullUrl . $fileDestination;
- $this->response = $this->makeDavRequest($user, "MOVE", $fileSource, $headers);
+ try {
+ $this->response = $this->makeDavRequest($user, 'COPY', $fileSource, $headers);
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx and 5xx responses cause an exception
+ $this->response = $e->getResponse();
+ }
}
/**
@@ -75,36 +141,91 @@ trait WebDav {
* @param string $fileSource
* @param string $range
*/
- public function downloadFileWithRange($fileSource, $range){
+ 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);
}
/**
* @When /^Downloading last public shared file with range "([^"]*)"$/
* @param string $range
*/
- public function downloadPublicFileWithRange($range){
+ public function downloadPublicFileWithRange($range) {
$token = $this->lastShareData->data->token;
- $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav";
- $headers['Range'] = $range;
+ $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token";
$client = new GClient();
$options = [];
- $options['auth'] = [$token, ""];
+ $options['headers'] = [
+ 'Range' => $range
+ ];
+
+ $this->response = $client->request('GET', $fullUrl, $options);
+ }
+
+ /**
+ * @When /^Downloading last public shared file inside a folder "([^"]*)" with range "([^"]*)"$/
+ * @param string $range
+ */
+ public function downloadPublicFileInsideAFolderWithRange($path, $range) {
+ $token = $this->lastShareData->data->token;
+ $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$path";
- $request = $client->createRequest("GET", $fullUrl, $options);
- $request->addHeader('Range', $range);
+ $client = new GClient();
+ $options = [
+ 'headers' => [
+ 'Range' => $range
+ ]
+ ];
- $this->response = $client->send($request);
+ $this->response = $client->request('GET', $fullUrl, $options);
}
/**
* @Then /^Downloaded content should be "([^"]*)"$/
* @param string $content
*/
- public function downloadedContentShouldBe($content){
- PHPUnit_Framework_Assert::assertEquals($content, (string)$this->response->getBody());
+ public function downloadedContentShouldBe($content) {
+ Assert::assertEquals($content, (string)$this->response->getBody());
+ }
+
+ /**
+ * @Then /^File "([^"]*)" should have prop "([^"]*):([^"]*)" equal to "([^"]*)"$/
+ * @param string $file
+ * @param string $prefix
+ * @param string $prop
+ * @param string $value
+ */
+ 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"];
+ Assert::assertEquals($property, $value);
+ }
+
+ /**
+ * @Then /^Image search should work$/
+ */
+ public function search(): void {
+ $this->searchFile($this->currentUser);
+ Assert::assertEquals(207, $this->response->getStatusCode());
+ }
+
+ /**
+ * @Then /^Favorite search should work$/
+ */
+ public function searchFavorite(): void {
+ $this->searchFile(
+ $this->currentUser,
+ '<oc:favorite/>',
+ null,
+ '<d:eq>
+ <d:prop>
+ <oc:favorite/>
+ </d:prop>
+ <d:literal>yes</d:literal>
+ </d:eq>'
+ );
+ Assert::assertEquals(207, $this->response->getStatusCode());
}
/**
@@ -113,39 +234,87 @@ trait WebDav {
* @param string $range
* @param string $content
*/
- public function downloadedContentWhenDownloadindShouldBe($fileSource, $range, $content){
+ public function downloadedContentWhenDownloadindShouldBe($fileSource, $range, $content) {
$this->downloadFileWithRange($fileSource, $range);
$this->downloadedContentShouldBe($content);
}
/**
+ * @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
*/
public function downloadingFile($fileName) {
- $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileName, []);
+ try {
+ $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileName, []);
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
}
/**
- * @Then The following headers should be set
- * @param \Behat\Gherkin\Node\TableNode $table
- * @throws \Exception
+ * @When Downloading public file :filename
*/
- public function theFollowingHeadersShouldBeSet(\Behat\Gherkin\Node\TableNode $table) {
- foreach($table->getTable() as $header) {
- $headerName = $header[0];
- $expectedHeaderValue = $header[1];
- $returnedHeader = $this->response->getHeader($headerName);
- if($returnedHeader !== $expectedHeaderValue) {
- throw new \Exception(
- sprintf(
- "Expected value '%s' for header '%s', got '%s'",
- $expectedHeaderValue,
- $headerName,
- $returnedHeader
- )
- );
- }
+ 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();
}
}
@@ -155,7 +324,7 @@ trait WebDav {
* @throws \Exception
*/
public function downloadedContentShouldStartWith($start) {
- if(strpos($this->response->getBody()->getContents(), $start) !== 0) {
+ if (strpos($this->response->getBody()->getContents(), $start) !== 0) {
throw new \Exception(
sprintf(
"Expected '%s', got '%s'",
@@ -167,12 +336,13 @@ trait WebDav {
}
/**
- * @Then /^as "([^"]*)" gets properties of folder "([^"]*)" with$/
+ * @Then /^as "([^"]*)" gets properties of (file|folder|entry) "([^"]*)" with$/
* @param string $user
+ * @param string $elementType
* @param string $path
* @param \Behat\Gherkin\Node\TableNode|null $propertiesTable
*/
- public function asGetsPropertiesOfFolderWith($user, $path, $propertiesTable) {
+ public function asGetsPropertiesOfFolderWith($user, $elementType, $path, $propertiesTable) {
$properties = null;
if ($propertiesTable instanceof \Behat\Gherkin\Node\TableNode) {
foreach ($propertiesTable->getRows() as $row) {
@@ -183,19 +353,67 @@ trait WebDav {
}
/**
+ * @Then /^as "([^"]*)" the (file|folder|entry) "([^"]*)" does not exist$/
+ * @param string $user
+ * @param string $entry
+ * @param string $path
+ * @param \Behat\Gherkin\Node\TableNode|null $propertiesTable
+ */
+ public function asTheFileOrFolderDoesNotExist($user, $entry, $path) {
+ $client = $this->getSabreClient($user);
+ $response = $client->request('HEAD', $this->makeSabrePath($user, $path));
+ if ($response['statusCode'] !== 404) {
+ throw new \Exception($entry . ' "' . $path . '" expected to not exist (status code ' . $response['statusCode'] . ', expected 404)');
+ }
+
+ return $response;
+ }
+
+ /**
+ * @Then /^as "([^"]*)" the (file|folder|entry) "([^"]*)" exists$/
+ * @param string $user
+ * @param string $entry
+ * @param string $path
+ */
+ public function asTheFileOrFolderExists($user, $entry, $path) {
+ $this->response = $this->listFolder($user, $path, 0);
+ }
+
+ /**
+ * @Then the response should be empty
+ * @throws \Exception
+ */
+ public function theResponseShouldBeEmpty(): void {
+ $response = ($this->response instanceof ResponseInterface) ? $this->convertResponseToDavEntries() : $this->response;
+ if ($response === []) {
+ return;
+ }
+
+ throw new \Exception('response is not empty');
+ }
+
+ /**
* @Then the single response should contain a property :key with value :value
* @param string $key
* @param string $expectedValue
* @throws \Exception
*/
public function theSingleResponseShouldContainAPropertyWithValue($key, $expectedValue) {
- $keys = $this->response;
- if (!array_key_exists($key, $keys)) {
+ $response = ($this->response instanceof ResponseInterface) ? $this->convertResponseToDavSingleEntry() : $this->response;
+ if (!array_key_exists($key, $response)) {
throw new \Exception("Cannot find property \"$key\" with \"$expectedValue\"");
}
- $value = $keys[$key];
- if ($value !== $expectedValue) {
+ $value = $response[$key];
+ if ($value instanceof ResourceType) {
+ $value = $value->getValue();
+ if (empty($value)) {
+ $value = '';
+ } else {
+ $value = $value[0];
+ }
+ }
+ if ($value != $expectedValue) {
throw new \Exception("Property \"$key\" found with value \"$value\", expected \"$expectedValue\"");
}
}
@@ -203,11 +421,10 @@ trait WebDav {
/**
* @Then the response should contain a share-types property with
*/
- public function theResponseShouldContainAShareTypesPropertyWith($table)
- {
+ 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 = [];
@@ -250,33 +467,190 @@ trait WebDav {
}
}
-
/*Returns the elements of a propfind, $folderDepth requires 1 to see elements without children*/
- public function listFolder($user, $path, $folderDepth, $properties = null){
+ public function listFolder($user, $path, $folderDepth, $properties = null) {
+ $client = $this->getSabreClient($user);
+ if (!$properties) {
+ $properties = [
+ '{DAV:}getetag'
+ ];
+ }
+
+ $response = $client->propfind($this->makeSabrePath($user, $path), $properties, $folderDepth);
+
+ return $response;
+ }
+
+ /**
+ * Returns the elements of a profind command
+ * @param string $properties properties which needs to be included in the report
+ * @param string $filterRules filter-rules to choose what needs to appear in the report
+ */
+ public function propfindFile(string $user, string $path, string $properties = '') {
+ $client = $this->getSabreClient($user);
+
+ $body = '<?xml version="1.0" encoding="utf-8" ?>
+ <d:propfind xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns"
+ xmlns:nc="http://nextcloud.org/ns"
+ xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:prop>
+ ' . $properties . '
+ </d:prop>
+ </d:propfind>';
+
+ $response = $client->request('PROPFIND', $this->makeSabrePath($user, $path), $body);
+ $parsedResponse = $client->parseMultistatus($response['body']);
+ return $parsedResponse;
+ }
+
+ /**
+ * Returns the elements of a searc command
+ * @param string $properties properties which needs to be included in the report
+ * @param string $filterRules filter-rules to choose what needs to appear in the report
+ */
+ public function searchFile(string $user, ?string $properties = null, ?string $scope = null, ?string $condition = null) {
+ $client = $this->getSabreClient($user);
+
+ if ($properties === null) {
+ $properties = '<oc:fileid /> <d:getlastmodified /> <d:getetag /> <d:getcontenttype /> <d:getcontentlength /> <nc:has-preview /> <oc:favorite /> <d:resourcetype />';
+ }
+
+ if ($condition === null) {
+ $condition = '<d:and>
+ <d:or>
+ <d:eq>
+ <d:prop>
+ <d:getcontenttype/>
+ </d:prop>
+ <d:literal>image/png</d:literal>
+ </d:eq>
+
+ <d:eq>
+ <d:prop>
+ <d:getcontenttype/>
+ </d:prop>
+ <d:literal>image/jpeg</d:literal>
+ </d:eq>
+
+ <d:eq>
+ <d:prop>
+ <d:getcontenttype/>
+ </d:prop>
+ <d:literal>image/heic</d:literal>
+ </d:eq>
+
+ <d:eq>
+ <d:prop>
+ <d:getcontenttype/>
+ </d:prop>
+ <d:literal>video/mp4</d:literal>
+ </d:eq>
+
+ <d:eq>
+ <d:prop>
+ <d:getcontenttype/>
+ </d:prop>
+ <d:literal>video/quicktime</d:literal>
+ </d:eq>
+ </d:or>
+ <d:eq>
+ <d:prop>
+ <oc:owner-id/>
+ </d:prop>
+ <d:literal>' . $user . '</d:literal>
+ </d:eq>
+</d:and>';
+ }
+
+ if ($scope === null) {
+ $scope = '<d:href>/files/' . $user . '</d:href><d:depth>infinity</d:depth>';
+ }
+
+ $body = '<?xml version="1.0" encoding="UTF-8"?>
+<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns" xmlns:ns="https://github.com/icewind1991/SearchDAV/ns" xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:basicsearch>
+ <d:select>
+ <d:prop>' . $properties . '</d:prop>
+ </d:select>
+ <d:from><d:scope>' . $scope . '</d:scope></d:from>
+ <d:where>' . $condition . '</d:where>
+ <d:orderby>
+ <d:order>
+ <d:prop><d:getlastmodified/></d:prop>
+ <d:descending/>
+ </d:order>
+ </d:orderby>
+ <d:limit>
+ <d:nresults>35</d:nresults>
+ <ns:firstresult>0</ns:firstresult>
+ </d:limit>
+ </d:basicsearch>
+</d:searchrequest>';
+
+ try {
+ $this->response = $this->makeDavRequest($user, 'SEARCH', '', [
+ 'Content-Type' => 'text/xml'
+ ], $body, '');
+
+ var_dump((string)$this->response->getBody());
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /* Returns the elements of a report command
+ * @param string $user
+ * @param string $path
+ * @param string $properties properties which needs to be included in the report
+ * @param string $filterRules filter-rules to choose what needs to appear in the report
+ */
+ public function reportFolder($user, $path, $properties, $filterRules) {
+ $client = $this->getSabreClient($user);
+
+ $body = '<?xml version="1.0" encoding="utf-8" ?>
+ <oc:filter-files xmlns:a="DAV:" xmlns:oc="http://owncloud.org/ns" >
+ <a:prop>
+ ' . $properties . '
+ </a:prop>
+ <oc:filter-rules>
+ ' . $filterRules . '
+ </oc:filter-rules>
+ </oc:filter-files>';
+
+ $response = $client->request('REPORT', $this->makeSabrePath($user, $path), $body);
+ $parsedResponse = $client->parseMultistatus($response['body']);
+ return $parsedResponse;
+ }
+
+ public function makeSabrePath($user, $path, $type = 'files') {
+ if ($type === 'files') {
+ return $this->encodePath($this->getDavFilesPath($user) . $path);
+ } else {
+ return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path);
+ }
+ }
+
+ public function getSabreClient($user) {
$fullUrl = substr($this->baseUrl, 0, -4);
- $settings = array(
+ $settings = [
'baseUri' => $fullUrl,
'userName' => $user,
- );
+ ];
if ($user === 'admin') {
$settings['password'] = $this->adminUser[1];
} else {
$settings['password'] = $this->regularUser;
}
+ $settings['authType'] = SClient::AUTH_BASIC;
- $client = new SClient($settings);
-
- if (!$properties) {
- $properties = [
- '{DAV:}getetag'
- ];
- }
-
- $response = $client->propfind($this->davPath . '/' . ltrim($path, '/'), $properties, $folderDepth);
-
- return $response;
+ return new SClient($settings);
}
/**
@@ -284,15 +658,15 @@ trait WebDav {
* @param string $user
* @param \Behat\Gherkin\Node\TableNode|null $expectedElements
*/
- public function checkElementList($user, $expectedElements){
+ public function checkElementList($user, $expectedElements) {
$elementList = $this->listFolder($user, '/', 3);
if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) {
$elementRows = $expectedElements->getRows();
$elementsSimplified = $this->simplifyArray($elementRows);
- foreach($elementsSimplified as $expectedElement) {
- $webdavPath = "/" . $this->davPath . $expectedElement;
- if (!array_key_exists($webdavPath,$elementList)){
- PHPUnit_Framework_Assert::fail("$webdavPath" . " is not in propfind answer");
+ foreach ($elementsSimplified as $expectedElement) {
+ $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement;
+ if (!array_key_exists($webdavPath, $elementList)) {
+ Assert::fail("$webdavPath" . ' is not in propfind answer');
}
}
}
@@ -304,41 +678,65 @@ trait WebDav {
* @param string $source
* @param string $destination
*/
- public function userUploadsAFileTo($user, $source, $destination)
- {
- $file = \GuzzleHttp\Stream\Stream::factory(fopen($source, 'r'));
+ 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) {
- // 4xx and 5xx responses cause an exception
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
$this->response = $e->getResponse();
}
}
/**
+ * @When User :user adds a file of :bytes bytes to :destination
+ * @param string $user
+ * @param string $bytes
+ * @param string $destination
+ */
+ public function userAddsAFileTo($user, $bytes, $destination) {
+ $filename = 'filespecificSize.txt';
+ $this->createFileSpecificSize($filename, $bytes);
+ Assert::assertEquals(1, file_exists("work/$filename"));
+ $this->userUploadsAFileTo($user, "work/$filename", $destination);
+ $this->removeFile('work/', $filename);
+ $expectedElements = new \Behat\Gherkin\Node\TableNode([["$destination"]]);
+ $this->checkElementList($user, $expectedElements);
+ }
+
+ /**
* @When User :user uploads file with content :content to :destination
*/
- public function userUploadsAFileWithContentTo($user, $content, $destination)
- {
- $file = \GuzzleHttp\Stream\Stream::factory($content);
+ 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) {
- // 4xx and 5xx responses cause an exception
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
$this->response = $e->getResponse();
}
}
/**
- * @When User :user deletes file :file
+ * @When /^User "([^"]*)" deletes (file|folder) "([^"]*)"$/
* @param string $user
+ * @param string $type
* @param string $file
*/
- public function userDeletesFile($user, $file) {
+ public function userDeletesFile($user, $type, $file) {
try {
$this->response = $this->makeDavRequest($user, 'DELETE', $file, []);
} catch (\GuzzleHttp\Exception\ServerException $e) {
- // 4xx and 5xx responses cause an exception
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
$this->response = $e->getResponse();
}
}
@@ -348,30 +746,460 @@ trait WebDav {
* @param string $user
* @param string $destination
*/
- public function userCreatedAFolder($user, $destination){
+ public function userCreatedAFolder($user, $destination) {
try {
- $this->response = $this->makeDavRequest($user, "MKCOL", $destination, []);
+ $destination = '/' . ltrim($destination, '/');
+ $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, []);
} catch (\GuzzleHttp\Exception\ServerException $e) {
- // 4xx and 5xx responses cause an exception
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
$this->response = $e->getResponse();
}
}
/**
- * @Given user :user uploads chunk file :num of :total with :data to :destination
+ * @Given user :user uploads bulked files :name1 with :content1 and :name2 with :content2 and :name3 with :content3
* @param string $user
- * @param int $num
- * @param int $total
- * @param string $data
- * @param string $destination
+ * @param string $name1
+ * @param string $content1
+ * @param string $name2
+ * @param string $content2
+ * @param string $name3
+ * @param string $content3
*/
- public function userUploadsChunkFileOfWithToWithChecksum($user, $num, $total, $data, $destination)
- {
- $num -= 1;
- $data = \GuzzleHttp\Stream\Stream::factory($data);
- $file = $destination . '-chunking-42-'.$total.'-'.$num;
- $this->makeDavRequest($user, 'PUT', $file, ['OC-Chunked' => '1'], $data);
+ public function userUploadsBulkedFiles($user, $name1, $content1, $name2, $content2, $name3, $content3) {
+ $boundary = 'boundary_azertyuiop';
+
+ $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 .= "\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 .= "\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 .= "\r\n";
+ $body .= $content3 . "\r\n";
+ $body .= '--' . $boundary . "--\r\n";
+
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $body);
+ rewind($stream);
+
+ $client = new GClient();
+ $options = [
+ 'auth' => [$user, $this->regularUser],
+ 'headers' => [
+ '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);
}
-}
+ /**
+ * @Given user :user creates a new chunking upload with id :id
+ */
+ public function userCreatesANewChunkingUploadWithId($user, $id) {
+ $this->parts = [];
+ $destination = '/uploads/' . $user . '/' . $id;
+ $this->makeDavRequest($user, 'MKCOL', $destination, [], null, 'uploads');
+ }
+
+ /**
+ * @Given user :user uploads new chunk file :num with :data to id :id
+ */
+ 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');
+ }
+
+ /**
+ * @Given user :user moves new chunk file with id :id to :dest
+ */
+ public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest) {
+ $source = '/uploads/' . $user . '/' . $id . '/.file';
+ $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest;
+ $this->makeDavRequest($user, 'MOVE', $source, [
+ 'Destination' => $destination
+ ], null, 'uploads');
+ }
+
+ /**
+ * @Then user :user moves new chunk file with id :id to :dest with size :size
+ */
+ public function userMovesNewChunkFileWithIdToMychunkedfileWithSize($user, $id, $dest, $size) {
+ $source = '/uploads/' . $user . '/' . $id . '/.file';
+ $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest;
+
+ try {
+ $this->response = $this->makeDavRequest($user, 'MOVE', $source, [
+ 'Destination' => $destination,
+ 'OC-Total-Length' => $size
+ ], null, 'uploads');
+ } catch (\GuzzleHttp\Exception\BadResponseException $ex) {
+ $this->response = $ex->getResponse();
+ }
+ }
+
+
+ /**
+ * @Given user :user creates a new chunking v2 upload with id :id and destination :targetDestination
+ */
+ public function userCreatesANewChunkingv2UploadWithIdAndDestination($user, $id, $targetDestination) {
+ $this->s3MultipartDestination = $this->getTargetDestination($user, $targetDestination);
+ $this->newUploadId();
+ $destination = '/uploads/' . $user . '/' . $this->getUploadId($id);
+ $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [
+ 'Destination' => $this->s3MultipartDestination,
+ ], null, 'uploads');
+ }
+
+ /**
+ * @Given user :user uploads new chunk v2 file :num to id :id
+ */
+ public function userUploadsNewChunkv2FileToIdAndDestination($user, $num, $id) {
+ $data = \GuzzleHttp\Psr7\Utils::streamFor(fopen('/tmp/part-upload-' . $num, 'r'));
+ $destination = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/' . $num;
+ $this->response = $this->makeDavRequest($user, 'PUT', $destination, [
+ 'Destination' => $this->s3MultipartDestination
+ ], $data, 'uploads');
+ }
+
+ /**
+ * @Given user :user moves new chunk v2 file with id :id
+ */
+ public function userMovesNewChunkv2FileWithIdToMychunkedfileAndDestination($user, $id) {
+ $source = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/.file';
+ try {
+ $this->response = $this->makeDavRequest($user, 'MOVE', $source, [
+ 'Destination' => $this->s3MultipartDestination,
+ ], null, 'uploads');
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
+ $this->response = $e->getResponse();
+ }
+ }
+
+ private function getTargetDestination(string $user, string $destination): string {
+ return substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $destination;
+ }
+
+ private function getUploadId(string $id): string {
+ return $id . '-' . $this->uploadId;
+ }
+ private function newUploadId() {
+ $this->uploadId = (string)time();
+ }
+
+ /**
+ * @Given /^Downloading file "([^"]*)" as "([^"]*)"$/
+ */
+ public function downloadingFileAs($fileName, $user) {
+ try {
+ $this->response = $this->makeDavRequest($user, 'GET', $fileName, []);
+ } catch (\GuzzleHttp\Exception\ServerException $e) {
+ // 5xx responses cause a server exception
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * URL encodes the given path but keeps the slashes
+ *
+ * @param string $path to encode
+ * @return string encoded path
+ */
+ private function encodePath($path) {
+ // slashes need to stay
+ return str_replace('%2F', '/', rawurlencode($path));
+ }
+
+ /**
+ * @When user :user favorites element :path
+ */
+ public function userFavoritesElement($user, $path) {
+ $this->response = $this->changeFavStateOfAnElement($user, $path, 1, 0, null);
+ }
+
+ /**
+ * @When user :user unfavorites element :path
+ */
+ public function userUnfavoritesElement($user, $path) {
+ $this->response = $this->changeFavStateOfAnElement($user, $path, 0, 0, null);
+ }
+
+ /*Set the elements of a proppatch, $folderDepth requires 1 to see elements without children*/
+ public function changeFavStateOfAnElement($user, $path, $favOrUnfav, $folderDepth, $properties = null) {
+ $fullUrl = substr($this->baseUrl, 0, -4);
+ $settings = [
+ 'baseUri' => $fullUrl,
+ 'userName' => $user,
+ ];
+ if ($user === 'admin') {
+ $settings['password'] = $this->adminUser[1];
+ } else {
+ $settings['password'] = $this->regularUser;
+ }
+ $settings['authType'] = SClient::AUTH_BASIC;
+
+ $client = new SClient($settings);
+ if (!$properties) {
+ $properties = [
+ '{http://owncloud.org/ns}favorite' => $favOrUnfav
+ ];
+ }
+
+ $response = $client->proppatch($this->getDavFilesPath($user) . $path, $properties, $folderDepth);
+ return $response;
+ }
+
+ /**
+ * @Given user :user stores etag of element :path
+ */
+ public function userStoresEtagOfElement($user, $path) {
+ $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]);
+ $this->asGetsPropertiesOfFolderWith($user, 'entry', $path, $propertiesTable);
+ $pathETAG[$path] = $this->response['{DAV:}getetag'];
+ $this->storedETAG[$user] = $pathETAG;
+ }
+
+ /**
+ * @Then etag of element :path of user :user has not changed
+ */
+ public function checkIfETAGHasNotChanged($path, $user) {
+ $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]);
+ $this->asGetsPropertiesOfFolderWith($user, 'entry', $path, $propertiesTable);
+ Assert::assertEquals($this->response['{DAV:}getetag'], $this->storedETAG[$user][$path]);
+ }
+
+ /**
+ * @Then etag of element :path of user :user has changed
+ */
+ public function checkIfETAGHasChanged($path, $user) {
+ $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]);
+ $this->asGetsPropertiesOfFolderWith($user, 'entry', $path, $propertiesTable);
+ Assert::assertNotEquals($this->response['{DAV:}getetag'], $this->storedETAG[$user][$path]);
+ }
+
+ /**
+ * @When Connecting to dav endpoint
+ */
+ public function connectingToDavEndpoint() {
+ try {
+ $this->response = $this->makeDavRequest(null, 'PROPFIND', '', []);
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ $this->response = $e->getResponse();
+ }
+ }
+
+ /**
+ * @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() {
+ $headers = $this->response->getHeaders();
+ foreach ($headers as $headerName => $headerValues) {
+ // if a header has multiple values, they must be different
+ if (count($headerValues) > 1 && count(array_unique($headerValues)) < count($headerValues)) {
+ throw new \Exception('Duplicate header found: ' . $headerName);
+ }
+ }
+ }
+
+ /**
+ * @Then /^user "([^"]*)" in folder "([^"]*)" should have favorited the following elements$/
+ * @param string $user
+ * @param string $folder
+ * @param \Behat\Gherkin\Node\TableNode|null $expectedElements
+ */
+ public function checkFavoritedElements($user, $folder, $expectedElements) {
+ $elementList = $this->reportFolder($user,
+ $folder,
+ '<oc:favorite/>',
+ '<oc:favorite>1</oc:favorite>');
+ if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) {
+ $elementRows = $expectedElements->getRows();
+ $elementsSimplified = $this->simplifyArray($elementRows);
+ foreach ($elementsSimplified as $expectedElement) {
+ $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement;
+ if (!array_key_exists($webdavPath, $elementList)) {
+ Assert::fail("$webdavPath" . ' is not in report answer');
+ }
+ }
+ }
+ }
+
+ /**
+ * @When /^User "([^"]*)" deletes everything from folder "([^"]*)"$/
+ * @param string $user
+ * @param string $folder
+ */
+ public function userDeletesEverythingInFolder($user, $folder) {
+ $elementList = $this->listFolder($user, $folder, 1);
+ $elementListKeys = array_keys($elementList);
+ array_shift($elementListKeys);
+ $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);
+ }
+ }
+
+
+ /**
+ * @param string $user
+ * @param string $path
+ * @return int
+ */
+ private function getFileIdForPath($user, $path) {
+ $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'];
+ }
+
+ /**
+ * @Given /^User "([^"]*)" stores id of file "([^"]*)"$/
+ * @param string $user
+ * @param string $path
+ */
+ public function userStoresFileIdForPath($user, $path) {
+ $this->storedFileID = $this->getFileIdForPath($user, $path);
+ }
+
+ /**
+ * @Given /^User "([^"]*)" checks id of file "([^"]*)"$/
+ * @param string $user
+ * @param string $path
+ */
+ public function userChecksFileIdForPath($user, $path) {
+ $currentFileID = $this->getFileIdForPath($user, $path);
+ Assert::assertEquals($currentFileID, $this->storedFileID);
+ }
+
+ /**
+ * @Given /^user "([^"]*)" creates a file locally with "([^"]*)" x 5 MB chunks$/
+ */
+ public function userCreatesAFileLocallyWithChunks($arg1, $chunks) {
+ $this->parts = [];
+ for ($i = 1;$i <= (int)$chunks;$i++) {
+ $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;
+ }
+ }
+
+ /**
+ * @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);
+ file_put_contents('/tmp/part-upload-' . $id, str_repeat($randomletter, (int)$size * 1024 * 1024));
+ $this->parts[] = '/tmp/part-upload-' . $id;
+ }
+
+ /**
+ * @Then /^Downloaded content should be the created file$/
+ */
+ public function downloadedContentShouldBeTheCreatedFile() {
+ $content = '';
+ sort($this->parts);
+ foreach ($this->parts as $part) {
+ $content .= file_get_contents($part);
+ }
+ Assert::assertEquals($content, (string)$this->response->getBody());
+ }
+
+ /**
+ * @Then /^the S3 multipart upload was successful with status "([^"]*)"$/
+ */
+ public function theSmultipartUploadWasSuccessful($status) {
+ Assert::assertEquals((int)$status, $this->response->getStatusCode());
+ }
+
+ /**
+ * @Then /^the upload should fail on object storage$/
+ */
+ public function theUploadShouldFailOnObjectStorage() {
+ $descriptor = [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ];
+ $process = proc_open('php occ config:system:get objectstore --no-ansi', $descriptor, $pipes, '../../');
+ $lastCode = proc_close($process);
+ if ($lastCode === 0) {
+ $this->theHTTPStatusCodeShouldBe(500);
+ }
+ }
+
+ /**
+ * @return array
+ * @throws Exception
+ */
+ private function convertResponseToDavSingleEntry(): array {
+ $results = $this->convertResponseToDavEntries();
+ if (count($results) > 1) {
+ throw new \Exception('result is empty or contain more than one (1) entry');
+ }
+
+ return array_shift($results);
+ }
+
+ /**
+ * @return array
+ */
+ private function convertResponseToDavEntries(): array {
+ $client = $this->getSabreClient($this->currentUser);
+ $parsedResponse = $client->parseMultiStatus((string)$this->response->getBody());
+
+ $results = [];
+ foreach ($parsedResponse as $href => $statusList) {
+ $results[$href] = $statusList[200] ?? [];
+ }
+
+ return $results;
+ }
+}