diff --git a/.codacy.yml b/.codacy.yml index c9dd79b0b..1d94123ca 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -16,5 +16,5 @@ exclude_paths: - 'tests/acceptance/expected-failures-*.md' - 'tests/acceptance/features/bootstrap/**' - 'tests/TestHelpers/**' - + - 'tests/acceptance/run.sh' ... diff --git a/.drone.star b/.drone.star index b964328bd..fba5c14f2 100644 --- a/.drone.star +++ b/.drone.star @@ -716,7 +716,6 @@ def localApiTestPipeline(ctx): "steps": skipIfUnchanged(ctx, "acceptance-tests") + restoreBuildArtifactCache(ctx, "ocis-binary-amd64", "ocis/bin") + ocisServer(storage, params["accounts_hash_difficulty"], extra_server_environment = params["extraServerEnvironment"]) + - restoreBuildArtifactCache(ctx, "testrunner", dirs["core"]) + localApiTests(suite, storage, params["extraEnvironment"]) + failEarly(ctx, early_fail), "services": redisForOCStorage(storage), @@ -736,7 +735,6 @@ def localApiTests(suite, storage, extra_environment = {}): environment = { "TEST_WITH_GRAPH_API": "true", "PATH_TO_OCIS": dirs["base"], - "PATH_TO_CORE": "%s/%s" % (dirs["base"], dirs["core"]), "TEST_SERVER_URL": "https://ocis-server:9200", "OCIS_REVA_DATA_ROOT": "%s" % (dirs["ocisRevaDataRoot"] if storage == "owncloud" else ""), "OCIS_SKELETON_STRATEGY": "%s" % ("copy" if storage == "owncloud" else "upload"), diff --git a/Makefile b/Makefile index 51c56eb06..bac62842d 100644 --- a/Makefile +++ b/Makefile @@ -106,11 +106,11 @@ PARALLEL_BEHAT_YML=tests/parallelDeployAcceptance/config/behat.yml .PHONY: test-acceptance-api test-acceptance-api: vendor-bin/behat/vendor - BEHAT_BIN=$(BEHAT_BIN) $(PATH_TO_CORE)/tests/acceptance/run.sh --remote --type api + BEHAT_BIN=$(BEHAT_BIN) $(PWD)/tests/acceptance/run.sh --type api .PHONY: test-paralleldeployment-api test-paralleldeployment-api: vendor-bin/behat/vendor - BEHAT_BIN=$(BEHAT_BIN) BEHAT_YML=$(PARALLEL_BEHAT_YML) $(PATH_TO_CORE)/tests/acceptance/run.sh --type api + BEHAT_BIN=$(BEHAT_BIN) BEHAT_YML=$(PARALLEL_BEHAT_YML) $(PWD)/tests/acceptance/run.sh --type api vendor/bamarni/composer-bin-plugin: composer.lock composer install diff --git a/composer.json b/composer.json index dddfbd2bc..ddff8e347 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,8 @@ "bamarni/composer-bin-plugin": true } }, - "require": { - }, "require-dev": { + "ext-simplexml": "*", "bamarni/composer-bin-plugin": "^1.4" }, "extra": { diff --git a/tests/TestHelpers/GraphHelper.php b/tests/TestHelpers/GraphHelper.php index 14335d6b8..d52b4cf55 100644 --- a/tests/TestHelpers/GraphHelper.php +++ b/tests/TestHelpers/GraphHelper.php @@ -84,7 +84,7 @@ class GraphHelper { */ public static function getFullUrl(string $baseUrl, string $path): string { $fullUrl = $baseUrl; - if (\substr($fullUrl, -1) !== '/') { + if (!str_ends_with($fullUrl, '/')) { $fullUrl .= '/'; } $fullUrl .= 'graph/v1.0/' . $path; diff --git a/tests/TestHelpers/HttpRequestHelper.php b/tests/TestHelpers/HttpRequestHelper.php new file mode 100644 index 000000000..c6b567b7a --- /dev/null +++ b/tests/TestHelpers/HttpRequestHelper.php @@ -0,0 +1,622 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +namespace TestHelpers; + +use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Cookie\CookieJar; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Request; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use SimpleXMLElement; +use Sabre\Xml\LibXMLException; +use Sabre\Xml\Reader; +use GuzzleHttp\Pool; + +/** + * Helper for HTTP requests + */ +class HttpRequestHelper { + public const HTTP_TOO_EARLY = 425; + + /** + * @var string + */ + private static $oCSelectorCookie = null; + + /** + * @return string + */ + public static function getOCSelectorCookie(): string { + return self::$oCSelectorCookie; + } + + /** + * @param string $oCSelectorCookie "owncloud-selector=oc10;path=/;" + * + * @return void + */ + public static function setOCSelectorCookie(string $oCSelectorCookie): void { + self::$oCSelectorCookie = $oCSelectorCookie; + } + + /** + * Some systems-under-test do async post-processing of operations like upload, + * move etc. If a client does a request on the resource before the post-processing + * is finished, then the server should return HTTP_TOO_EARLY "425". Clients are + * expected to retry the request "some time later" (tm). + * + * On such systems, when HTTP_TOO_EARLY status is received, the test code will + * retry the request at 1-second intervals until either some other HTTP status + * is received or the retry-limit is reached. + * + * @return int + */ + public static function numRetriesOnHttpTooEarly():int { + if (OcisHelper::isTestingOnOcisOrReva()) { + // Currently reva and oCIS may return HTTP_TOO_EARLY + // So try up to 10 times before giving up. + return 10; + } + return 0; + } + + /** + * + * @param string|null $url + * @param string|null $xRequestId + * @param string|null $method + * @param string|null $user + * @param string|null $password + * @param array|null $headers ['X-MyHeader' => 'value'] + * @param mixed $body + * @param array|null $config + * @param CookieJar|null $cookies + * @param bool $stream Set to true to stream a response rather + * than download it all up-front. + * @param int|null $timeout + * @param Client|null $client + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function sendRequest( + ?string $url, + ?string $xRequestId, + ?string $method = 'GET', + ?string $user = null, + ?string $password = null, + ?array $headers = null, + $body = null, + ?array $config = null, + ?CookieJar $cookies = null, + bool $stream = false, + ?int $timeout = 0, + ?Client $client = null + ):ResponseInterface { + if ($client === null) { + $client = self::createClient( + $user, + $password, + $config, + $cookies, + $stream, + $timeout + ); + } + /** + * @var RequestInterface $request + */ + $request = self::createRequest( + $url, + $xRequestId, + $method, + $headers, + $body + ); + + if ((\getenv('DEBUG_ACCEPTANCE_REQUESTS') !== false) || (\getenv('DEBUG_ACCEPTANCE_API_CALLS') !== false)) { + $debugRequests = true; + } else { + $debugRequests = false; + } + + if ((\getenv('DEBUG_ACCEPTANCE_RESPONSES') !== false) || (\getenv('DEBUG_ACCEPTANCE_API_CALLS') !== false)) { + $debugResponses = true; + } else { + $debugResponses = false; + } + + if ($debugRequests) { + self::debugRequest($request, $user, $password); + } + + $sendRetryLimit = self::numRetriesOnHttpTooEarly(); + $sendCount = 0; + $sendExceptionHappened = false; + + do { + // The exceptions that might happen here include: + // ConnectException - in that case there is no response. Don't catch the exception. + // RequestException - if there is something in the response then pass it back. + // otherwise re-throw the exception. + // GuzzleException - something else unexpected happened. Don't catch the exception. + try { + $response = $client->send($request); + } catch (RequestException $ex) { + $sendExceptionHappened = true; + $response = $ex->getResponse(); + + //if the response was null for some reason do not return it but re-throw + if ($response === null) { + throw $ex; + } + } + + if ($debugResponses) { + self::debugResponse($response); + } + $sendCount = $sendCount + 1; + $loopAgain = !$sendExceptionHappened && ($response->getStatusCode() === self::HTTP_TOO_EARLY) && ($sendCount <= $sendRetryLimit); + if ($loopAgain) { + // we need to repeat the send request, because we got HTTP_TOO_EARLY + // wait 1 second before sending again, to give the server some time + // to finish whatever post-processing it might be doing. + \sleep(1); + } + } while ($loopAgain); + + return $response; + } + + /** + * Print details about the request. + * + * @param RequestInterface|null $request + * @param string|null $user + * @param string|null $password + * + * @return void + */ + private static function debugRequest(?RequestInterface $request, ?string $user, ?string $password):void { + print("### AUTH: $user:$password\n"); + print("### REQUEST: " . $request->getMethod() . " " . $request->getUri() . "\n"); + self::printHeaders($request->getHeaders()); + self::printBody($request->getBody()); + print("\n### END REQUEST\n"); + } + + /** + * Print details about the response. + * + * @param ResponseInterface|null $response + * + * @return void + */ + private static function debugResponse(?ResponseInterface $response):void { + print("### RESPONSE\n"); + print("Status: " . $response->getStatusCode() . "\n"); + self::printHeaders($response->getHeaders()); + self::printBody($response->getBody()); + print("\n### END RESPONSE\n"); + } + + /** + * Print details about the headers. + * + * @param array|null $headers + * + * @return void + */ + private static function printHeaders(?array $headers):void { + if ($headers) { + print("Headers:\n"); + foreach ($headers as $header => $value) { + if (\is_array($value)) { + print($header . ": " . \implode(', ', $value) . "\n"); + } else { + print($header . ": " . $value . "\n"); + } + } + } else { + print("Headers: none\n"); + } + } + + /** + * Print details about the body. + * + * @param StreamInterface|null $body + * + * @return void + */ + private static function printBody(?StreamInterface $body):void { + print("Body:\n"); + \var_dump($body->getContents()); + // Rewind the stream so that later code can read from the start. + $body->rewind(); + } + + /** + * Send the requests to the server in parallel. + * This function takes an array of requests and an optional client. + * It will send all the requests to the server using the Pool object in guzzle. + * + * @param array|null $requests + * @param Client|null $client + * + * @return array + */ + public static function sendBatchRequest( + ?array $requests, + ?Client $client + ):array { + $results = Pool::batch($client, $requests); + return $results; + } + + /** + * Create a Guzzle Client + * This creates a client object that can be used later to send a request object(s) + * + * @param string|null $user + * @param string|null $password + * @param array|null $config + * @param CookieJar|null $cookies + * @param bool $stream Set to true to stream a response rather + * than download it all up-front. + * @param int|null $timeout + * + * @return Client + */ + public static function createClient( + ?string $user = null, + ?string $password = null, + ?array $config = null, + ?CookieJar $cookies = null, + ?bool $stream = false, + ?int $timeout = 0 + ):Client { + $options = []; + if ($user !== null) { + $options['auth'] = [$user, $password]; + } + if ($config !== null) { + $options['config'] = $config; + } + if ($cookies !== null) { + $options['cookies'] = $cookies; + } + $options['stream'] = $stream; + $options['verify'] = false; + $options['timeout'] = $timeout; + $client = new Client($options); + return $client; + } + + /** + * Create an http request based on given parameters. + * This creates a RequestInterface object that can be used with a client to send a request. + * This enables us to create multiple requests in advance so that we can send them to the server at once in parallel. + * + * @param string|null $url + * @param string|null $xRequestId + * @param string|null $method + * @param array|null $headers ['X-MyHeader' => 'value'] + * @param string|array $body either the actual string to send in the body, + * or an array of key-value pairs to be converted + * into a body with http_build_query. + * + * @return RequestInterface + */ + public static function createRequest( + ?string $url, + ?string $xRequestId = '', + ?string $method = 'GET', + ?array $headers = null, + $body = null + ):RequestInterface { + if ($headers === null) { + $headers = []; + } + if ($xRequestId !== '') { + $headers['X-Request-ID'] = $xRequestId; + } + if (\is_array($body)) { + // when creating the client, it is possible to set 'form_params' and + // the Client constructor sorts out doing this http_build_query stuff. + // But 'new Request' does not have the flexibility to do that. + // So we need to do it here. + $body = \http_build_query($body, '', '&'); + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (OcisHelper::isTestingParallelDeployment()) { + // oCIS cannot handle '/apps/testing' endpoints + // so those requests must be redirected to oC10 server + // change server to oC10 if the request url has `/apps/testing` + if (strpos($url, "/apps/testing") !== false) { + $oCISServerUrl = \getenv('TEST_SERVER_URL'); + $oC10ServerUrl = \getenv('TEST_OC10_URL'); + $url = str_replace($oCISServerUrl, $oC10ServerUrl, $url); + } else { + // set 'owncloud-server' selector cookie for oCIS requests + $headers['Cookie'] = self::getOCSelectorCookie(); + } + } + + $request = new Request( + $method, + $url, + $headers, + $body + ); + return $request; + } + + /** + * same as HttpRequestHelper::sendRequest() but with "GET" as method + * + * @param string|null $url + * @param string|null $xRequestId + * @param string|null $user + * @param string|null $password + * @param array|null $headers ['X-MyHeader' => 'value'] + * @param mixed $body + * @param array|null $config + * @param CookieJar|null $cookies + * @param boolean $stream + * + * @return ResponseInterface + * @throws GuzzleException + * @see HttpRequestHelper::sendRequest() + */ + public static function get( + ?string $url, + ?string $xRequestId, + ?string $user = null, + ?string $password = null, + ?array $headers = null, + $body = null, + ?array $config = null, + ?CookieJar $cookies = null, + ?bool $stream = false + ):ResponseInterface { + return self::sendRequest( + $url, + $xRequestId, + 'GET', + $user, + $password, + $headers, + $body, + $config, + $cookies, + $stream + ); + } + + /** + * same as HttpRequestHelper::sendRequest() but with "POST" as method + * + * @param string|null $url + * @param string|null $xRequestId + * @param string|null $user + * @param string|null $password + * @param array|null $headers ['X-MyHeader' => 'value'] + * @param mixed $body + * @param array|null $config + * @param CookieJar|null $cookies + * @param boolean $stream + * + * @return ResponseInterface + * @throws GuzzleException + * @see HttpRequestHelper::sendRequest() + */ + public static function post( + ?string $url, + ?string $xRequestId, + ?string $user = null, + ?string $password = null, + ?array $headers = null, + $body = null, + ?array $config = null, + ?CookieJar $cookies = null, + ?bool $stream = false + ):ResponseInterface { + return self::sendRequest( + $url, + $xRequestId, + 'POST', + $user, + $password, + $headers, + $body, + $config, + $cookies, + $stream + ); + } + + /** + * same as HttpRequestHelper::sendRequest() but with "PUT" as method + * + * @param string|null $url + * @param string|null $xRequestId + * @param string|null $user + * @param string|null $password + * @param array|null $headers ['X-MyHeader' => 'value'] + * @param mixed $body + * @param array|null $config + * @param CookieJar|null $cookies + * @param boolean $stream + * + * @return ResponseInterface + * @throws GuzzleException + * @see HttpRequestHelper::sendRequest() + */ + public static function put( + ?string $url, + ?string $xRequestId, + ?string $user = null, + ?string $password = null, + ?array $headers = null, + $body = null, + ?array $config = null, + ?CookieJar $cookies = null, + ?bool $stream = false + ):ResponseInterface { + return self::sendRequest( + $url, + $xRequestId, + 'PUT', + $user, + $password, + $headers, + $body, + $config, + $cookies, + $stream + ); + } + + /** + * same as HttpRequestHelper::sendRequest() but with "DELETE" as method + * + * @param string|null $url + * @param string|null $xRequestId + * @param string|null $user + * @param string|null $password + * @param array|null $headers ['X-MyHeader' => 'value'] + * @param mixed $body + * @param array|null $config + * @param CookieJar|null $cookies + * @param boolean $stream + * + * @return ResponseInterface + * @throws GuzzleException + * @see HttpRequestHelper::sendRequest() + * + */ + public static function delete( + ?string $url, + ?string $xRequestId, + ?string $user = null, + ?string $password = null, + ?array $headers = null, + $body = null, + ?array $config = null, + ?CookieJar $cookies = null, + ?bool $stream = false + ):ResponseInterface { + return self::sendRequest( + $url, + $xRequestId, + 'DELETE', + $user, + $password, + $headers, + $body, + $config, + $cookies, + $stream + ); + } + + /** + * Parses the response as XML and returns a SimpleXMLElement with these + * registered namespaces: + * | prefix | namespace | + * | d | DAV: | + * | oc | http://owncloud.org/ns | + * | ocs | http://open-collaboration-services.org/ns | + * + * @param ResponseInterface $response + * @param string|null $exceptionText text to put at the front of exception messages + * + * @return SimpleXMLElement + * @throws Exception + */ + public static function getResponseXml(ResponseInterface $response, ?string $exceptionText = ''):SimpleXMLElement { + // rewind just to make sure we can re-parse it in case it was parsed already... + $response->getBody()->rewind(); + $contents = $response->getBody()->getContents(); + try { + $responseXmlObject = new SimpleXMLElement($contents); + $responseXmlObject->registerXPathNamespace( + 'ocs', + 'http://open-collaboration-services.org/ns' + ); + $responseXmlObject->registerXPathNamespace( + 'oc', + 'http://owncloud.org/ns' + ); + $responseXmlObject->registerXPathNamespace( + 'd', + 'DAV:' + ); + return $responseXmlObject; + } catch (Exception $e) { + if ($exceptionText !== '') { + $exceptionText = $exceptionText . ' '; + } + if ($contents === '') { + throw new Exception($exceptionText . "Received empty response where XML was expected"); + } + $message = $exceptionText . "Exception parsing response body: \"" . $contents . "\""; + throw new Exception($message, 0, $e); + } + } + + /** + * parses the body content of $response and returns an array representing the XML + * This function returns an array with the following three elements: + * * name - The root element name. + * * value - The value for the root element. + * * attributes - An array of attributes. + * + * @param ResponseInterface $response + * + * @return array + */ + public static function parseResponseAsXml(ResponseInterface $response):array { + $body = $response->getBody()->getContents(); + $parsedResponse = []; + if ($body && \substr($body, 0, 1) === '<') { + try { + $reader = new Reader(); + $reader->xml($body); + $parsedResponse = $reader->parse(); + } catch (LibXMLException $e) { + // Sometimes the body can be a real page of HTML and text. + // So it may not be a complete ordinary piece of XML. + // The XML parse might fail with an exception message like: + // Opening and ending tag mismatch: link line 31 and head. + } + } + return $parsedResponse; + } +} diff --git a/tests/TestHelpers/OcisHelper.php b/tests/TestHelpers/OcisHelper.php new file mode 100644 index 000000000..e8627baba --- /dev/null +++ b/tests/TestHelpers/OcisHelper.php @@ -0,0 +1,364 @@ + + * @copyright Copyright (c) 2020 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +namespace TestHelpers; + +use Exception; +use GuzzleHttp\Exception\GuzzleException; + +/** + * Class OcisHelper + * + * Helper functions that are needed to run tests on OCIS + * + * @package TestHelpers + */ +class OcisHelper { + /** + * @return bool + */ + public static function isTestingOnOcis():bool { + return (\getenv("TEST_OCIS") === "true"); + } + + /** + * @return bool + */ + public static function isTestingOnReva():bool { + return (\getenv("TEST_REVA") === "true"); + } + + /** + * @return bool + */ + public static function isTestingOnOcisOrReva():bool { + return (self::isTestingOnOcis() || self::isTestingOnReva()); + } + + /** + * @return bool + */ + public static function isTestingOnOc10():bool { + return (!self::isTestingOnOcisOrReva()); + } + + /** + * @return bool + */ + public static function isTestingParallelDeployment(): bool { + return (\getenv("TEST_PARALLEL_DEPLOYMENT") === "true"); + } + + /** + * @return bool + */ + public static function isTestingWithGraphApi(): bool { + return \getenv('TEST_WITH_GRAPH_API') === 'true'; + } + + /** + * @return bool|string false if no command given or the command as string + */ + public static function getDeleteUserDataCommand() { + $cmd = \getenv("DELETE_USER_DATA_CMD"); + if ($cmd === false || \trim($cmd) === "") { + return false; + } + return $cmd; + } + + /** + * @return string + * @throws Exception + */ + public static function getStorageDriver():string { + $storageDriver = (\getenv("STORAGE_DRIVER")); + if ($storageDriver === false) { + return "OWNCLOUD"; + } + $storageDriver = \strtoupper($storageDriver); + if ($storageDriver !== "OCIS" && $storageDriver !== "EOS" && $storageDriver !== "OWNCLOUD" && $storageDriver !== "S3NG") { + throw new Exception( + "Invalid storage driver. " . + "STORAGE_DRIVER must be OCIS|EOS|OWNCLOUD|S3NG" + ); + } + return $storageDriver; + } + + /** + * @param string|null $user + * + * @return void + * @throws Exception + */ + public static function deleteRevaUserData(?string $user = ""):void { + $deleteCmd = self::getDeleteUserDataCommand(); + if ($deleteCmd === false) { + if (self::getStorageDriver() === "OWNCLOUD") { + self::recurseRmdir(self::getOcisRevaDataRoot() . $user); + } + return; + } + if (self::getStorageDriver() === "EOS") { + $deleteCmd = \str_replace( + "%s", + $user[0] . '/' . $user, + $deleteCmd + ); + } else { + $deleteCmd = \sprintf($deleteCmd, $user); + } + \exec($deleteCmd); + } + + /** + * Helper for Recursive Copy of file/folder + * For more info check this out https://gist.github.com/gserrano/4c9648ec9eb293b9377b + * + * @param string|null $source + * @param string|null $destination + * + * @return void + */ + public static function recurseCopy(?string $source, ?string $destination):void { + $dir = \opendir($source); + @\mkdir($destination); + while (($file = \readdir($dir)) !== false) { + if (($file != '.') && ($file != '..')) { + if (\is_dir($source . '/' . $file)) { + self::recurseCopy($source . '/' . $file, $destination . '/' . $file); + } else { + \copy($source . '/' . $file, $destination . '/' . $file); + } + } + } + \closedir($dir); + } + + /** + * Helper for Recursive Upload of file/folder + * + * @param string|null $baseUrl + * @param string|null $source + * @param string|null $userId + * @param string|null $password + * @param string|null $xRequestId + * @param string|null $destination + * + * @return void + * @throws Exception + */ + public static function recurseUpload( + ?string $baseUrl, + ?string $source, + ?string $userId, + ?string $password, + ?string $xRequestId = '', + ?string $destination = '' + ):void { + if ($destination !== '') { + $response = WebDavHelper::makeDavRequest( + $baseUrl, + $userId, + $password, + "MKCOL", + $destination, + [], + $xRequestId + ); + if ($response->getStatusCode() !== 201) { + throw new Exception("Could not create folder destination" . $response->getBody()->getContents()); + } + } + + $dir = \opendir($source); + while (($file = \readdir($dir)) !== false) { + if (($file != '.') && ($file != '..')) { + $sourcePath = $source . '/' . $file; + $destinationPath = $destination . '/' . $file; + if (\is_dir($sourcePath)) { + self::recurseUpload( + $baseUrl, + $sourcePath, + $userId, + $password, + $xRequestId, + $destinationPath + ); + } else { + $response = UploadHelper::upload( + $baseUrl, + $userId, + $password, + $sourcePath, + $destinationPath, + $xRequestId + ); + $responseStatus = $response->getStatusCode(); + if ($responseStatus !== 201) { + throw new Exception( + "Could not upload skeleton file $sourcePath to $destinationPath for user '$userId' status '$responseStatus' response body: '" + . $response->getBody()->getContents() . "'" + ); + } + } + } + } + \closedir($dir); + } + + /** + * @return int + */ + public static function getLdapPort():int { + $port = \getenv("REVA_LDAP_PORT"); + return $port ? (int)$port : 636; + } + + /** + * @return bool + */ + public static function useSsl():bool { + $useSsl = \getenv("REVA_LDAP_USESSL"); + if ($useSsl === false) { + return (self::getLdapPort() === 636); + } else { + return $useSsl === "true"; + } + } + + /** + * @return string + */ + public static function getBaseDN():string { + $dn = \getenv("REVA_LDAP_BASE_DN"); + return $dn ? $dn : "dc=owncloud,dc=com"; + } + + /** + * @return string + */ + public static function getGroupsOU():string { + $ou = \getenv("REVA_LDAP_GROUPS_OU"); + return $ou ? $ou : "TestGroups"; + } + + /** + * @return string + */ + public static function getUsersOU():string { + $ou = \getenv("REVA_LDAP_USERS_OU"); + return $ou ? $ou : "TestUsers"; + } + + /** + * @return string + */ + public static function getGroupSchema():string { + $schema = \getenv("REVA_LDAP_GROUP_SCHEMA"); + return $schema ? $schema : "rfc2307"; + } + /** + * @return string + */ + public static function getHostname():string { + $hostname = \getenv("REVA_LDAP_HOSTNAME"); + return $hostname ? $hostname : "localhost"; + } + + /** + * @return string + */ + public static function getBindDN():string { + $dn = \getenv("REVA_LDAP_BIND_DN"); + return $dn ? $dn : "cn=admin,dc=owncloud,dc=com"; + } + + /** + * @return string + */ + public static function getBindPassword():string { + $pw = \getenv("REVA_LDAP_BIND_PASSWORD"); + return $pw ? $pw : ""; + } + + /** + * @return string + */ + private static function getOcisRevaDataRoot():string { + $root = \getenv("OCIS_REVA_DATA_ROOT"); + if (($root === false || $root === "") && self::isTestingOnOcisOrReva()) { + $root = "/var/tmp/ocis/owncloud/"; + } + if (!\file_exists($root)) { + echo "WARNING: reva data root folder ($root) does not exist\n"; + } + return $root; + } + + /** + * @param string|null $dir + * + * @return bool + */ + private static function recurseRmdir(?string $dir):bool { + if (\file_exists($dir) === true) { + $files = \array_diff(\scandir($dir), ['.', '..']); + foreach ($files as $file) { + if (\is_dir("$dir/$file")) { + self::recurseRmdir("$dir/$file"); + } else { + \unlink("$dir/$file"); + } + } + return \rmdir($dir); + } + return true; + } + + /** + * On Eos storage backend when the user data is cleared after test run + * Running another test immediately fails. So Send this request to create user home directory + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $password + * @param string|null $xRequestId + * + * @return void + * @throws GuzzleException + */ + public static function createEOSStorageHome( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $xRequestId = '' + ):void { + HttpRequestHelper::get( + $baseUrl . "/ocs/v2.php/apps/notifications/api/v1/notifications", + $xRequestId, + $user, + $password + ); + } +} diff --git a/tests/TestHelpers/OcsApiHelper.php b/tests/TestHelpers/OcsApiHelper.php new file mode 100644 index 000000000..6e0623f42 --- /dev/null +++ b/tests/TestHelpers/OcsApiHelper.php @@ -0,0 +1,103 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ +namespace TestHelpers; + +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * Helper to make requests to the OCS API + * + * @author Artur Neumann + * + */ +class OcsApiHelper { + /** + * @param string|null $baseUrl + * @param string|null $user if set to null no authentication header will be sent + * @param string|null $password + * @param string|null $method HTTP Method + * @param string|null $path + * @param string|null $xRequestId + * @param mixed $body array of key, value pairs e.g ['value' => 'yes'] + * @param int|null $ocsApiVersion (1|2) default 2 + * @param array|null $headers + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function sendRequest( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $method, + ?string $path, + ?string $xRequestId = '', + $body = [], + ?int $ocsApiVersion = 2, + ?array $headers = [] + ):ResponseInterface { + $fullUrl = $baseUrl; + if (\substr($fullUrl, -1) !== '/') { + $fullUrl .= '/'; + } + $fullUrl .= "ocs/v{$ocsApiVersion}.php" . $path; + $headers['OCS-APIREQUEST'] = true; + return HttpRequestHelper::sendRequest($fullUrl, $xRequestId, $method, $user, $password, $headers, $body); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $method HTTP Method + * @param string|null $path + * @param string|null $xRequestId + * @param mixed $body array of key, value pairs e.g ['value' => 'yes'] + * @param int|null $ocsApiVersion (1|2) default 2 + * @param array|null $headers + * + * @return RequestInterface + */ + public static function createOcsRequest( + ?string $baseUrl, + ?string $method, + ?string $path, + ?string $xRequestId = '', + $body = [], + ?int $ocsApiVersion = 2, + ?array $headers = [] + ):RequestInterface { + $fullUrl = $baseUrl; + if (\substr($fullUrl, -1) !== '/') { + $fullUrl .= '/'; + } + $fullUrl .= "ocs/v{$ocsApiVersion}.php" . $path; + return HttpRequestHelper::createRequest( + $fullUrl, + $xRequestId, + $method, + $headers, + $body + ); + } +} diff --git a/tests/TestHelpers/SetupHelper.php b/tests/TestHelpers/SetupHelper.php new file mode 100644 index 000000000..16e7d043e --- /dev/null +++ b/tests/TestHelpers/SetupHelper.php @@ -0,0 +1,1268 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ +namespace TestHelpers; + +use Behat\Testwork\Hook\Scope\HookScope; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Exception\ServerException; +use Exception; +use Psr\Http\Message\ResponseInterface; +use SimpleXMLElement; + +/** + * Helper to setup UI / Integration tests + * + * @author Artur Neumann + * + */ +class SetupHelper extends \PHPUnit\Framework\Assert { + /** + * @var string + */ + private static $ocPath = null; + /** + * @var string + */ + private static $baseUrl = null; + /** + * @var string + */ + private static $adminUsername = null; + /** + * @var string + */ + private static $adminPassword = null; + + /** + * creates a user + * + * @param string|null $userName + * @param string|null $password + * @param string|null $xRequestId + * @param string|null $displayName + * @param string|null $email + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function createUser( + ?string $userName, + ?string $password, + ?string $xRequestId = '', + ?string $displayName = null, + ?string $email = null + ):array { + $occCommand = ['user:add', '--password-from-env']; + if ($displayName !== null) { + $occCommand = \array_merge($occCommand, ["--display-name", $displayName]); + } + if ($email !== null) { + $occCommand = \array_merge($occCommand, ["--email", $email]); + } + \putenv("OC_PASS=" . $password); + return self::runOcc( + \array_merge($occCommand, [$userName]), + $xRequestId + ); + } + + /** + * deletes a user + * + * @param string|null $userName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function deleteUser( + ?string $userName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['user:delete', $userName], + $xRequestId + ); + } + + /** + * + * @param string|null $userName + * @param string|null $app + * @param string|null $key + * @param string|null $value + * @param string|null $xRequestId + * + * @return string[] + * @throws Exception + */ + public static function changeUserSetting( + ?string $userName, + ?string $app, + ?string $key, + ?string $value, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['user:setting', '--value ' . $value, $userName, $app, $key], + $xRequestId + ); + } + + /** + * creates a group + * + * @param string|null $groupName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function createGroup( + ?string $groupName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['group:add', $groupName], + $xRequestId + ); + } + + /** + * adds an existing user to a group, creating the group if it does not exist + * + * @param string|null $groupName + * @param string|null $userName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function addUserToGroup( + ?string $groupName, + ?string $userName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['group:add-member', '--member', $userName, $groupName], + $xRequestId + ); + } + + /** + * removes a user from a group + * + * @param string|null $groupName + * @param string|null $userName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function removeUserFromGroup( + ?string $groupName, + ?string $userName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['group:remove-member', '--member', $userName, $groupName], + $xRequestId + ); + } + + /** + * deletes a group + * + * @param string|null $groupName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function deleteGroup( + ?string $groupName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['group:delete', $groupName], + $xRequestId + ); + } + + /** + * + * @param string|null $xRequestId + * + * @return string[] + * @throws Exception + */ + public static function getGroups( + ?string $xRequestId = '' + ):array { + return \json_decode( + self::runOcc( + ['group:list', '--output=json'], + $xRequestId + )['stdOut'] + ); + } + /** + * + * @param HookScope $scope + * + * @return array of suite context parameters + */ + public static function getSuiteParameters(HookScope $scope):array { + return $scope->getEnvironment()->getSuite() + ->getSettings() ['context'] ['parameters']; + } + + /** + * Fixup OC path so that it always starts with a "/" and does not end with + * a "/". + * + * @param string|null $ocPath + * + * @return string + */ + private static function normaliseOcPath(?string $ocPath):string { + return '/' . \trim($ocPath, '/'); + } + + /** + * + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return void + */ + public static function init( + ?string $adminUsername, + ?string $adminPassword, + ?string $baseUrl, + ?string $ocPath + ): void { + foreach (\func_get_args() as $variableToCheck) { + if (!\is_string($variableToCheck)) { + throw new \InvalidArgumentException( + "mandatory argument missing or wrong type ($variableToCheck => " + . \gettype($variableToCheck) . ")" + ); + } + } + self::$adminUsername = $adminUsername; + self::$adminPassword = $adminPassword; + self::$baseUrl = \rtrim($baseUrl, '/'); + self::$ocPath = self::normaliseOcPath($ocPath); + } + + /** + * + * @return string path to the testing app occ endpoint + * @throws Exception if ocPath has not been set yet + */ + public static function getOcPath():?string { + if (self::$ocPath === null) { + throw new Exception( + "getOcPath called before ocPath is set by init" + ); + } + + return self::$ocPath; + } + + /** + * + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $xRequestId + * + * @return SimpleXMLElement + * @throws GuzzleException + */ + public static function getSysInfo( + ?string $baseUrl, + ?string $adminUsername, + ?string $adminPassword, + ?string $xRequestId = '' + ):SimpleXMLElement { + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "GET", + "/apps/testing/api/v1/sysinfo", + $xRequestId + ); + if ($result->getStatusCode() !== 200) { + throw new \Exception( + "could not get sysinfo " . $result->getReasonPhrase() + ); + } + return HttpRequestHelper::getResponseXml($result, __METHOD__)->data; + } + + /** + * + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $xRequestId + * + * @return string + * @throws GuzzleException + */ + public static function getServerRoot( + ?string $baseUrl, + ?string $adminUsername, + ?string $adminPassword, + ?string $xRequestId = '' + ):string { + $sysInfo = self::getSysInfo( + $baseUrl, + $adminUsername, + $adminPassword, + $xRequestId + ); + // server_root is a SimpleXMLElement object that "wraps" a string. + /// We want the bare string. + return (string) $sysInfo->server_root; + } + + /** + * @param string|null $adminUsername + * @param string|null $callerName + * + * @return string + * @throws Exception + */ + private static function checkAdminUsername(?string $adminUsername, ?string $callerName):?string { + if (self::$adminUsername === null + && $adminUsername === null + ) { + throw new Exception( + "$callerName called without adminUsername - pass the username or call SetupHelper::init" + ); + } + if ($adminUsername === null) { + $adminUsername = self::$adminUsername; + } + return $adminUsername; + } + + /** + * @param string|null $adminPassword + * @param string|null $callerName + * + * @return string + * @throws Exception + */ + private static function checkAdminPassword(?string $adminPassword, ?string $callerName):string { + if (self::$adminPassword === null + && $adminPassword === null + ) { + throw new Exception( + "$callerName called without adminPassword - pass the password or call SetupHelper::init" + ); + } + if ($adminPassword === null) { + $adminPassword = self::$adminPassword; + } + return $adminPassword; + } + + /** + * @param string|null $baseUrl + * @param string|null $callerName + * + * @return string + * @throws Exception + */ + private static function checkBaseUrl(?string $baseUrl, ?string $callerName):?string { + if (self::$baseUrl === null + && $baseUrl === null + ) { + throw new Exception( + "$callerName called without baseUrl - pass the baseUrl or call SetupHelper::init" + ); + } + if ($baseUrl === null) { + $baseUrl = self::$baseUrl; + } + return $baseUrl; + } + + /** + * + * @param string|null $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo' + * @param string|null $xRequestId + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * + * @return void + * @throws GuzzleException + * @throws Exception + */ + public static function mkDirOnServer( + ?string $dirPathFromServerRoot, + ?string $xRequestId = '', + ?string $baseUrl = null, + ?string $adminUsername = null, + ?string $adminPassword = null + ):void { + $baseUrl = self::checkBaseUrl($baseUrl, "mkDirOnServer"); + $adminUsername = self::checkAdminUsername($adminUsername, "mkDirOnServer"); + $adminPassword = self::checkAdminPassword($adminPassword, "mkDirOnServer"); + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "POST", + "/apps/testing/api/v1/dir", + $xRequestId, + ['dir' => $dirPathFromServerRoot] + ); + + if ($result->getStatusCode() !== 200) { + throw new \Exception( + "could not create directory $dirPathFromServerRoot " . $result->getReasonPhrase() + ); + } + } + + /** + * + * @param string|null $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo' + * @param string|null $xRequestId + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * + * @return void + * @throws GuzzleException + * @throws Exception + */ + public static function rmDirOnServer( + ?string $dirPathFromServerRoot, + ?string $xRequestId = '', + ?string $baseUrl = null, + ?string $adminUsername = null, + ?string $adminPassword = null + ):void { + $baseUrl = self::checkBaseUrl($baseUrl, "rmDirOnServer"); + $adminUsername = self::checkAdminUsername($adminUsername, "rmDirOnServer"); + $adminPassword = self::checkAdminPassword($adminPassword, "rmDirOnServer"); + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "DELETE", + "/apps/testing/api/v1/dir", + $xRequestId, + ['dir' => $dirPathFromServerRoot] + ); + + if ($result->getStatusCode() !== 200) { + throw new \Exception( + "could not delete directory $dirPathFromServerRoot " . $result->getReasonPhrase() + ); + } + } + + /** + * + * @param string|null $filePathFromServerRoot e.g. 'app2/myapp/appinfo/info.xml' + * @param string|null $content + * @param string|null $xRequestId + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * + * @return void + * @throws GuzzleException + */ + public static function createFileOnServer( + ?string $filePathFromServerRoot, + ?string $content, + ?string $xRequestId = '', + ?string $baseUrl = null, + ?string $adminUsername = null, + ?string $adminPassword = null + ):void { + $baseUrl = self::checkBaseUrl($baseUrl, "createFileOnServer"); + $adminUsername = self::checkAdminUsername($adminUsername, "createFileOnServer"); + $adminPassword = self::checkAdminPassword($adminPassword, "createFileOnServer"); + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "POST", + "/apps/testing/api/v1/file", + $xRequestId, + [ + 'file' => $filePathFromServerRoot, + 'content' => $content + ] + ); + + if ($result->getStatusCode() !== 200) { + throw new \Exception( + "could not create file $filePathFromServerRoot " . $result->getReasonPhrase() + ); + } + } + + /** + * + * @param string|null $filePathFromServerRoot e.g. 'app2/myapp/appinfo/info.xml' + * @param string|null $xRequestId + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * + * @return void + * @throws GuzzleException + * @throws Exception + */ + public static function deleteFileOnServer( + ?string $filePathFromServerRoot, + ?string $xRequestId = '', + ?string $baseUrl = null, + ?string $adminUsername = null, + ?string $adminPassword = null + ):void { + $baseUrl = self::checkBaseUrl($baseUrl, "deleteFileOnServer"); + $adminUsername = self::checkAdminUsername($adminUsername, "deleteFileOnServer"); + $adminPassword = self::checkAdminPassword($adminPassword, "deleteFileOnServer"); + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "DELETE", + "/apps/testing/api/v1/file", + $xRequestId, + [ + 'file' => $filePathFromServerRoot + ] + ); + + if ($result->getStatusCode() !== 200) { + throw new \Exception( + "could not delete file $filePathFromServerRoot " . $result->getReasonPhrase() + ); + } + } + + /** + * @param string|null $fileInCore e.g. 'app2/myapp/appinfo/info.xml' + * @param string|null $xRequestId + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * + * @return string + * @throws GuzzleException + * @throws Exception + */ + public static function readFileFromServer( + ?string $fileInCore, + ?string $xRequestId = '', + ?string $baseUrl = null, + ?string $adminUsername = null, + ?string $adminPassword = null + ):string { + $baseUrl = self::checkBaseUrl($baseUrl, "readFile"); + $adminUsername = self::checkAdminUsername( + $adminUsername, + "readFile" + ); + $adminPassword = self::checkAdminPassword( + $adminPassword, + "readFile" + ); + + $response = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + 'GET', + "/apps/testing/api/v1/file?file={$fileInCore}", + $xRequestId + ); + self::assertSame( + 200, + $response->getStatusCode(), + "Failed to read the file {$fileInCore}" + ); + $localContent = HttpRequestHelper::getResponseXml($response, __METHOD__); + $localContent = (string)$localContent->data->element->contentUrlEncoded; + return \urldecode($localContent); + } + + /** + * returns the content of a file in a skeleton folder + * + * @param string|null $fileInSkeletonFolder + * @param string|null $xRequestId + * @param string|null $baseUrl + * @param string|null $adminUsername + * @param string|null $adminPassword + * + * @return string content of the file + * @throws GuzzleException + * @throws Exception + */ + public static function readSkeletonFile( + ?string $fileInSkeletonFolder, + ?string $xRequestId = '', + ?string $baseUrl = null, + ?string $adminUsername = null, + ?string $adminPassword = null + ):string { + $baseUrl = self::checkBaseUrl($baseUrl, "readSkeletonFile"); + $adminUsername = self::checkAdminUsername( + $adminUsername, + "readSkeletonFile" + ); + $adminPassword = self::checkAdminPassword( + $adminPassword, + "readSkeletonFile" + ); + + //find the absolute path of the currently set skeletondirectory + $occResponse = self::runOcc( + ['config:system:get', 'skeletondirectory'], + $xRequestId, + $adminUsername, + $adminPassword, + $baseUrl + ); + if ((int) $occResponse['code'] !== 0) { + throw new \Exception( + "could not get current skeletondirectory. " . $occResponse['stdErr'] + ); + } + $skeletonRoot = \trim($occResponse['stdOut']); + + $fileInSkeletonFolder = \rawurlencode("$skeletonRoot/$fileInSkeletonFolder"); + $response = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + 'GET', + "/apps/testing/api/v1/file?file={$fileInSkeletonFolder}&absolute=true", + $xRequestId + ); + self::assertSame( + 200, + $response->getStatusCode(), + "Failed to read the file {$fileInSkeletonFolder}" + ); + $localContent = HttpRequestHelper::getResponseXml($response, __METHOD__); + $localContent = (string)$localContent->data->element->contentUrlEncoded; + $localContent = \urldecode($localContent); + return $localContent; + } + + /** + * enables an app + * + * @param string|null $appName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function enableApp( + ?string $appName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['app:enable', $appName], + $xRequestId + ); + } + + /** + * disables an app + * + * @param string|null $appName + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws Exception + */ + public static function disableApp( + ?string $appName, + ?string $xRequestId = '' + ):array { + return self::runOcc( + ['app:disable', $appName], + $xRequestId + ); + } + + /** + * checks if an app is currently enabled + * + * @param string|null $appName + * @param string|null $xRequestId + * + * @return bool true if enabled, false if disabled or nonexistent + * @throws Exception + */ + public static function isAppEnabled( + ?string $appName, + ?string $xRequestId = '' + ):bool { + $result = self::runOcc( + ['app:list', '^' . $appName . '$'], + $xRequestId + ); + return \strtolower(\substr($result['stdOut'], 0, 7)) === 'enabled'; + } + + /** + * lists app status (enabled or disabled) + * + * @param string|null $appName + * @param string|null $xRequestId + * + * @return bool true if the app needed to be enabled, false otherwise + * @throws Exception + */ + public static function enableAppIfNotEnabled( + ?string $appName, + ?string $xRequestId = '' + ):bool { + if (!self::isAppEnabled($appName, $xRequestId)) { + self::enableApp( + $appName, + $xRequestId + ); + return true; + } + + return false; + } + + /** + * Runs a list of occ commands at once + * + * @param array|null $commands + * @param string|null $xRequestId + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * + * @return array + * @throws GuzzleException + * @throws Exception + */ + public static function runBulkOcc( + ?array $commands, + ?string $xRequestId = '', + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null + ):array { + if (OcisHelper::isTestingOnOcisOrReva()) { + return []; + } + $baseUrl = self::checkBaseUrl($baseUrl, "runOcc"); + $adminUsername = self::checkAdminUsername($adminUsername, "runOcc"); + $adminPassword = self::checkAdminPassword($adminPassword, "runOcc"); + + if (!\is_array($commands)) { + throw new Exception("commands must be an array"); + } + + $isTestingAppEnabledText = "Is the testing app installed and enabled?\n"; + $bodies = []; + + foreach ($commands as $occ) { + if (!\array_key_exists('command', $occ)) { + throw new \InvalidArgumentException("command key is missing in array passed to runBulkOcc"); + } + + $body = [ + 'command' => \implode(' ', $occ['command']) + ]; + + if (isset($occ['envVariables'])) { + $body['env_variables'] = $occ['envVariables']; + } + \array_push($bodies, $body); + } + try { + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "POST", + "/apps/testing/api/v1/occ/bulk?format=json", + $xRequestId, + \json_encode($bodies) + ); + } catch (ServerException $e) { + throw new Exception( + "Could not execute 'occ'. " . + $isTestingAppEnabledText . + $e->getResponse()->getBody() + ); + } + $result = \json_decode($result->getBody()->getContents()); + + return $result->ocs->data; + } + + /** + * invokes an OCC command + * + * @param array|null $args anything behind "occ". + * For example: "files:transfer-ownership" + * @param string|null $xRequestId + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * @param array|null $envVariables + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws GuzzleException + * @throws Exception + */ + public static function runOcc( + ?array $args, + ?string $xRequestId = '', + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null, + ?array $envVariables = null + ):array { + if (OcisHelper::isTestingOnOcisOrReva() && !OcisHelper::isTestingParallelDeployment()) { + return ['code' => '', 'stdOut' => '', 'stdErr' => '' ]; + } + $baseUrl = self::checkBaseUrl($baseUrl, "runOcc"); + $adminUsername = self::checkAdminUsername($adminUsername, "runOcc"); + $adminPassword = self::checkAdminPassword($adminPassword, "runOcc"); + if (self::$ocPath === null + && $ocPath === null + ) { + throw new Exception( + "runOcc called without ocPath - pass the ocPath or call SetupHelper::init" + ); + } + if ($ocPath === null) { + $ocPath = self::$ocPath; + } else { + $ocPath = self::normaliseOcPath($ocPath); + } + + $body = []; + $argsString = \implode(' ', $args); + $body['command'] = $argsString; + + if ($envVariables !== null) { + $body['env_variables'] = $envVariables; + } + + $isTestingAppEnabledText = "Is the testing app installed and enabled?\n"; + + try { + $result = OcsApiHelper::sendRequest( + $baseUrl, + $adminUsername, + $adminPassword, + "POST", + $ocPath, + $xRequestId, + $body + ); + } catch (ServerException $e) { + throw new Exception( + "Could not execute 'occ'. " . + $isTestingAppEnabledText . + $e->getResponse()->getBody() + ); + } + + $return = []; + $contents = $result->getBody()->getContents(); + $resultXml = \simplexml_load_string($contents); + + if ($resultXml === false) { + $status = $result->getStatusCode(); + throw new Exception( + "Response is not valid XML after executing 'occ $argsString'. " . + "HTTP status was $status. " . + $isTestingAppEnabledText . + "Response contents were '$contents'" + ); + } + + $return['code'] = $resultXml->xpath("//ocs/data/code"); + $return['stdOut'] = $resultXml->xpath("//ocs/data/stdOut"); + $return['stdErr'] = $resultXml->xpath("//ocs/data/stdErr"); + + if (!isset($return['code'][0])) { + throw new Exception( + "Return code not found after executing 'occ $argsString'. " . + $isTestingAppEnabledText . + $contents + ); + } + + if (!isset($return['stdOut'][0])) { + throw new Exception( + "Return stdOut not found after executing 'occ $argsString'. " . + $isTestingAppEnabledText . + $contents + ); + } + + if (!isset($return['stdErr'][0])) { + throw new Exception( + "Return stdErr not found after executing 'occ $argsString'. " . + $isTestingAppEnabledText . + $contents + ); + } + + if (!\is_a($return['code'][0], "SimpleXMLElement")) { + throw new Exception( + "Return code is not a SimpleXMLElement after executing 'occ $argsString'. " . + $isTestingAppEnabledText . + $contents + ); + } + + if (!\is_a($return['stdOut'][0], "SimpleXMLElement")) { + throw new Exception( + "Return stdOut is not a SimpleXMLElement after executing 'occ $argsString'. " . + $isTestingAppEnabledText . + $contents + ); + } + + if (!\is_a($return['stdErr'][0], "SimpleXMLElement")) { + throw new Exception( + "Return stdErr is not a SimpleXMLElement after executing 'occ $argsString'. " . + $isTestingAppEnabledText . + $contents + ); + } + + $return['code'] = $return['code'][0]->__toString(); + $return['stdOut'] = $return['stdOut'][0]->__toString(); + $return['stdErr'] = $return['stdErr'][0]->__toString(); + self::resetOpcache( + $baseUrl, + $adminUsername, + $adminPassword, + $xRequestId + ); + return $return; + } + + /** + * @param string $baseUrl + * @param string $user + * @param string $password + * @param string $xRequestId + * + * @return ResponseInterface|null + * @throws GuzzleException + */ + public static function resetOpcache( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $xRequestId = '' + ):?ResponseInterface { + try { + return OcsApiHelper::sendRequest( + $baseUrl, + $user, + $password, + "DELETE", + "/apps/testing/api/v1/opcache", + $xRequestId + ); + } catch (ServerException $e) { + echo "could not reset opcache, if tests fail try to set " . + "'opcache.revalidate_freq=0' in the php.ini file\n"; + } + return null; + } + + /** + * Create local storage mount + * + * @param string|null $mount (name of local storage mount) + * @param string|null $xRequestId + * + * @return string[] associated array with "code", "stdOut", "stdErr", "storageId" + * @throws GuzzleException + */ + public static function createLocalStorageMount( + ?string $mount, + ?string $xRequestId = '' + ):array { + $mountPath = TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/$mount"; + SetupHelper::mkDirOnServer( + $mountPath, + $xRequestId + ); + // files_external:create requires absolute path + $serverRoot = self::getServerRoot( + self::$baseUrl, + self::$adminUsername, + self::$adminPassword, + $xRequestId + ); + $result = self::runOcc( + [ + 'files_external:create', + $mount, + 'local', + 'null::null', + '-c', + 'datadir=' . $serverRoot . '/' . $mountPath + ], + $xRequestId + ); + // stdOut should have a string like "Storage created with id 65" + $storageIdWords = \explode(" ", \trim($result['stdOut'])); + if (\array_key_exists(4, $storageIdWords)) { + $result['storageId'] = (int)$storageIdWords[4]; + } else { + // presumably something went wrong with the files_external:create command + // so return "unknown" to the caller. The result array has the command exit + // code and stdErr output etc., so the caller can process what it likes + // of that information to work out what went wrong. + $result['storageId'] = "unknown"; + } + return $result; + } + + /** + * Get a system config setting, including status code, output and standard + * error output. + * + * @param string|null $key + * @param string|null $xRequestId + * @param string|null $output e.g. json + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws GuzzleException + */ + public static function getSystemConfig( + ?string $key, + ?string $xRequestId = '', + ?string $output = null, + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null + ):array { + $args = []; + $args[] = 'config:system:get'; + $args[] = $key; + + if ($output !== null) { + $args[] = '--output'; + $args[] = $output; + } + + $args[] = '--no-ansi'; + + return self::runOcc( + $args, + $xRequestId, + $adminUsername, + $adminPassword, + $baseUrl, + $ocPath + ); + } + + /** + * Set a system config setting + * + * @param string|null $key + * @param string|null $value + * @param string|null $xRequestId + * @param string|null $type e.g. boolean or json + * @param string|null $output e.g. json + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws GuzzleException + */ + public static function setSystemConfig( + ?string $key, + ?string $value, + ?string $xRequestId = '', + ?string $type = null, + ?string $output = null, + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null + ):array { + $args = []; + $args[] = 'config:system:set'; + $args[] = $key; + $args[] = '--value'; + $args[] = $value; + + if ($type !== null) { + $args[] = '--type'; + $args[] = $type; + } + + if ($output !== null) { + $args[] = '--output'; + $args[] = $output; + } + + $args[] = '--no-ansi'; + if ($baseUrl === null) { + $baseUrl = self::$baseUrl; + } + return self::runOcc( + $args, + $xRequestId, + $adminUsername, + $adminPassword, + $baseUrl, + $ocPath + ); + } + + /** + * Get the value of a system config setting + * + * @param string|null $key + * @param string|null $xRequestId + * @param string|null $output e.g. json + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return string + * @throws GuzzleException if parameters have not been provided yet or the testing app is not enabled + */ + public static function getSystemConfigValue( + ?string $key, + ?string $xRequestId = '', + ?string $output = null, + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null + ):string { + if ($baseUrl === null) { + $baseUrl = self::$baseUrl; + } + return self::getSystemConfig( + $key, + $xRequestId, + $output, + $adminUsername, + $adminPassword, + $baseUrl, + $ocPath + )['stdOut']; + } + + /** + * Finds all lines containing the given text + * + * @param string|null $input stdout or stderr output + * @param string|null $text text to search for + * + * @return array array of lines that matched + */ + public static function findLines(?string $input, ?string $text):array { + $results = []; + foreach (\explode("\n", $input) as $line) { + if (\strpos($line, $text) !== false) { + $results[] = $line; + } + } + return $results; + } + + /** + * Delete a system config setting + * + * @param string|null $key + * @param string|null $xRequestId + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return string[] associated array with "code", "stdOut", "stdErr" + * @throws GuzzleException if parameters have not been provided yet or the testing app is not enabled + */ + public static function deleteSystemConfig( + ?string $key, + ?string $xRequestId = '', + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null + ):array { + $args = []; + $args[] = 'config:system:delete'; + $args[] = $key; + + $args[] = '--no-ansi'; + if ($baseUrl === null) { + $baseUrl = self::$baseUrl; + } + return SetupHelper::runOcc( + $args, + $xRequestId, + $adminUsername, + $adminPassword, + $baseUrl, + $ocPath + ); + } +} diff --git a/tests/TestHelpers/SharingHelper.php b/tests/TestHelpers/SharingHelper.php new file mode 100644 index 000000000..9fcc9154e --- /dev/null +++ b/tests/TestHelpers/SharingHelper.php @@ -0,0 +1,261 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ +namespace TestHelpers; + +use Exception; +use InvalidArgumentException; +use Psr\Http\Message\ResponseInterface; +use SimpleXMLElement; + +/** + * manage Shares via OCS API + * + * @author Artur Neumann + * + */ +class SharingHelper { + public const PERMISSION_TYPES = [ + 'read' => 1, + 'update' => 2, + 'create' => 4, + 'delete' => 8, + 'share' => 16, + ]; + + public const SHARE_TYPES = [ + 'user' => 0, + 'group' => 1, + 'public_link' => 3, + 'federated' => 6, + ]; + + public const SHARE_STATES = [ + 'accepted' => 0, + 'pending' => 1, + 'rejected' => 2, + 'declined' => 2, // declined is a synonym for rejected + ]; + + /** + * + * @param string $baseUrl baseURL of the ownCloud installation without /ocs. + * @param string $user user that creates the share. + * @param string $password password of the user that creates the share. + * @param string $path The path to the file or folder which should be shared. + * @param string|int $shareType The type of the share. This can be one of: + * 0 = user, 1 = group, 3 = public (link), + * 6 = federated (cloud share). + * Pass either the number or the keyword. + * @param string $xRequestId + * @param string|null $shareWith The user or group id with which the file should + * be shared. + * @param boolean|null $publicUpload Whether to allow public upload to a public + * shared folder. + * @param string|null $sharePassword The password to protect the public link + * share with. + * @param string|int|string[]|int[]|null $permissions The permissions to set on the share. + * 1 = read; 2 = update; 4 = create; + * 8 = delete; 16 = share + * (default: 31, for public shares: 1) + * Pass either the (total) number or array of numbers, + * or any of the above keywords or array of keywords. + * @param string|null $linkName A (human-readable) name for the share, + * which can be up to 64 characters in length. + * @param string|null $expireDate An expire date for public link shares. + * This argument expects a date string + * in the format 'YYYY-MM-DD'. + * @param int $ocsApiVersion + * @param int $sharingApiVersion + * @param string $sharingApp + * + * @return ResponseInterface + * @throws InvalidArgumentException + */ + public static function createShare( + string $baseUrl, + string $user, + string $password, + string $path, + $shareType, + string $xRequestId = '', + ?string $shareWith = null, + ?bool $publicUpload = false, + string $sharePassword = null, + $permissions = null, + ?string $linkName = null, + ?string $expireDate = null, + int $ocsApiVersion = 1, + int $sharingApiVersion = 1, + string $sharingApp = 'files_sharing' + ): ResponseInterface { + $fd = []; + foreach ([$path, $baseUrl, $user, $password] as $variableToCheck) { + if (!\is_string($variableToCheck)) { + throw new InvalidArgumentException( + "mandatory argument missing or wrong type ($variableToCheck => " + . \gettype($variableToCheck) . ")" + ); + } + } + + if ($permissions !== null) { + $fd['permissions'] = self::getPermissionSum($permissions); + } + + if (!\in_array($ocsApiVersion, [1, 2], true)) { + throw new InvalidArgumentException( + "invalid ocsApiVersion ($ocsApiVersion)" + ); + } + if (!\in_array($sharingApiVersion, [1, 2], true)) { + throw new InvalidArgumentException( + "invalid sharingApiVersion ($sharingApiVersion)" + ); + } + + $fullUrl = $baseUrl; + if (\substr($fullUrl, -1) !== '/') { + $fullUrl .= '/'; + } + $fullUrl .= "ocs/v{$ocsApiVersion}.php/apps/{$sharingApp}/api/v{$sharingApiVersion}/shares"; + + $fd['path'] = $path; + $fd['shareType'] = self::getShareType($shareType); + + if ($shareWith !== null) { + $fd['shareWith'] = $shareWith; + } + if ($publicUpload !== null) { + $fd['publicUpload'] = (bool) $publicUpload; + } + if ($sharePassword !== null) { + $fd['password'] = $sharePassword; + } + if ($linkName !== null) { + $fd['name'] = $linkName; + } + if ($expireDate !== null) { + $fd['expireDate'] = $expireDate; + } + $headers = ['OCS-APIREQUEST' => 'true']; + + return HttpRequestHelper::post( + $fullUrl, + $xRequestId, + $user, + $password, + $headers, + $fd + ); + } + + /** + * calculates the permission sum (int) from given permissions + * permissions can be passed in as int, string or array of int or string + * 'read' => 1 + * 'update' => 2 + * 'create' => 4 + * 'delete' => 8 + * 'share' => 16 + * + * @param string[]|string|int|int[] $permissions + * + * @return int + * @throws InvalidArgumentException + * + */ + public static function getPermissionSum($permissions):int { + if (\is_numeric($permissions)) { + // Allow any permission number so that test scenarios can + // specifically test invalid permission values + return (int) $permissions; + } + if (!\is_array($permissions)) { + $permissions = [$permissions]; + } + $permissionSum = 0; + foreach ($permissions as $permission) { + if (\array_key_exists($permission, self::PERMISSION_TYPES)) { + $permissionSum += self::PERMISSION_TYPES[$permission]; + } elseif (\in_array($permission, self::PERMISSION_TYPES, true)) { + $permissionSum += (int) $permission; + } else { + throw new InvalidArgumentException( + "invalid permission type ($permission)" + ); + } + } + if ($permissionSum < 1 || $permissionSum > 31) { + throw new InvalidArgumentException( + "invalid permission total ($permissionSum)" + ); + } + return $permissionSum; + } + + /** + * returns the share type number corresponding to the share type keyword + * + * @param string|int $shareType a keyword from SHARE_TYPES or a share type number + * + * @return int + * @throws InvalidArgumentException + * + */ + public static function getShareType($shareType):int { + if (\array_key_exists($shareType, self::SHARE_TYPES)) { + return self::SHARE_TYPES[$shareType]; + } else { + if (\ctype_digit($shareType)) { + $shareType = (int) $shareType; + } + $key = \array_search($shareType, self::SHARE_TYPES, true); + if ($key !== false) { + return self::SHARE_TYPES[$key]; + } + throw new InvalidArgumentException( + "invalid share type ($shareType)" + ); + } + } + + /** + * + * @param SimpleXMLElement $responseXmlObject + * @param string $errorMessage + * + * @return string + * @throws Exception + * + */ + public static function getLastShareIdFromResponse( + SimpleXMLElement $responseXmlObject, + string $errorMessage = "cannot find share id in response" + ):string { + $xmlPart = $responseXmlObject->xpath("//data/element[last()]/id"); + + if (!\is_array($xmlPart) || (\count($xmlPart) === 0)) { + throw new Exception($errorMessage); + } + return $xmlPart[0]->__toString(); + } +} diff --git a/tests/TestHelpers/SpaceNotFoundException.php b/tests/TestHelpers/SpaceNotFoundException.php new file mode 100644 index 000000000..4a576e391 --- /dev/null +++ b/tests/TestHelpers/SpaceNotFoundException.php @@ -0,0 +1,14 @@ + + * + */ +namespace TestHelpers; +use Exception; + +/** + * Class SpaceNotFoundException + * Exception when space id for a user is not found + */ +class SpaceNotFoundException extends Exception { +} diff --git a/tests/TestHelpers/TranslationHelper.php b/tests/TestHelpers/TranslationHelper.php new file mode 100644 index 000000000..bc262b473 --- /dev/null +++ b/tests/TestHelpers/TranslationHelper.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) 2021 Talank Baral talank@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +namespace TestHelpers; + +/** + * Class TranslationHelper + * + * Helper functions that are needed to run tests on different languages + * + * @package TestHelpers + */ +class TranslationHelper { + /** + * @param string|null $language + * + * @return string|null + */ + public static function getLanguage(?string $language): ?string { + if (!isset($language)) { + if (\getenv('OC_LANGUAGE') !== false) { + $language = \getenv('OC_LANGUAGE'); + } + } + return $language; + } +} diff --git a/tests/TestHelpers/UploadHelper.php b/tests/TestHelpers/UploadHelper.php new file mode 100644 index 000000000..1ff63cfa5 --- /dev/null +++ b/tests/TestHelpers/UploadHelper.php @@ -0,0 +1,318 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ +namespace TestHelpers; + +use Psr\Http\Message\ResponseInterface; + +/** + * Helper for Uploads + * + * @author Artur Neumann + * + */ +class UploadHelper extends \PHPUnit\Framework\Assert { + /** + * + * @param string|null $baseUrl URL of owncloud + * e.g. http://localhost:8080 + * should include the subfolder + * if owncloud runs in a subfolder + * e.g. http://localhost:8080/owncloud-core + * @param string|null $user + * @param string|null $password + * @param string|null $source + * @param string|null $destination + * @param string|null $xRequestId + * @param array|null $headers + * @param int|null $davPathVersionToUse (1|2) + * @param int|null $chunkingVersion (1|2|null) + * if set to null chunking will not be used + * @param int|null $noOfChunks how many chunks do we want to upload + * + * @return ResponseInterface + */ + public static function upload( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $source, + ?string $destination, + ?string $xRequestId = '', + ?array $headers = [], + ?int $davPathVersionToUse = 1, + ?int $chunkingVersion = null, + ?int $noOfChunks = 1 + ): ResponseInterface { + //simple upload with no chunking + if ($chunkingVersion === null) { + $data = \file_get_contents($source); + return WebDavHelper::makeDavRequest( + $baseUrl, + $user, + $password, + "PUT", + $destination, + $headers, + $xRequestId, + $data, + $davPathVersionToUse + ); + } else { + //prepare chunking + $chunks = self::chunkFile($source, $noOfChunks); + $chunkingId = 'chunking-' . (string)\rand(1000, 9999); + $v2ChunksDestination = '/uploads/' . $user . '/' . $chunkingId; + } + + //prepare chunking version specific stuff + if ($chunkingVersion === 1) { + $headers['OC-Chunked'] = '1'; + } elseif ($chunkingVersion === 2) { + $result = WebDavHelper::makeDavRequest( + $baseUrl, + $user, + $password, + 'MKCOL', + $v2ChunksDestination, + $headers, + $xRequestId, + null, + $davPathVersionToUse, + "uploads" + ); + if ($result->getStatusCode() >= 400) { + return $result; + } + } + + //upload chunks + foreach ($chunks as $index => $chunk) { + if ($chunkingVersion === 1) { + $filename = $destination . "-" . $chunkingId . "-" . + \count($chunks) . '-' . ( string ) $index; + $davRequestType = "files"; + } elseif ($chunkingVersion === 2) { + $filename = $v2ChunksDestination . '/' . (string)($index); + $davRequestType = "uploads"; + } + $result = WebDavHelper::makeDavRequest( + $baseUrl, + $user, + $password, + "PUT", + $filename, + $headers, + $xRequestId, + $chunk, + $davPathVersionToUse, + $davRequestType + ); + if ($result->getStatusCode() >= 400) { + return $result; + } + } + //finish upload for new chunking + if ($chunkingVersion === 2) { + $source = $v2ChunksDestination . '/.file'; + $headers['Destination'] = $baseUrl . "/" . + WebDavHelper::getDavPath($user, $davPathVersionToUse) . + $destination; + $result = WebDavHelper::makeDavRequest( + $baseUrl, + $user, + $password, + 'MOVE', + $source, + $headers, + $xRequestId, + null, + $davPathVersionToUse, + "uploads" + ); + if ($result->getStatusCode() >= 400) { + return $result; + } + } + return $result; + } + + /** + * Upload the same file multiple times with different mechanisms. + * + * @param string|null $baseUrl URL of owncloud + * @param string|null $user user who uploads + * @param string|null $password + * @param string|null $source source file path + * @param string|null $destination destination path on the server + * @param string|null $xRequestId + * @param bool $overwriteMode when false creates separate files to test uploading brand new files, + * when true it just overwrites the same file over and over again with the same name + * @param string|null $exceptChunkingType empty string or "old" or "new" + * + * @return array of ResponseInterface + */ + public static function uploadWithAllMechanisms( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $source, + ?string $destination, + ?string $xRequestId = '', + ?bool $overwriteMode = false, + ?string $exceptChunkingType = '' + ):array { + $responses = []; + foreach ([1, 2] as $davPathVersion) { + if ($davPathVersion === 1) { + $davHuman = 'old'; + } else { + $davHuman = 'new'; + } + + switch ($exceptChunkingType) { + case 'old': + $exceptChunkingVersion = 1; + break; + case 'new': + $exceptChunkingVersion = 2; + break; + default: + $exceptChunkingVersion = -1; + break; + } + + foreach ([null, 1, 2] as $chunkingVersion) { + if ($chunkingVersion === $exceptChunkingVersion) { + continue; + } + $valid = WebDavHelper::isValidDavChunkingCombination( + $davPathVersion, + $chunkingVersion + ); + if ($valid === false) { + continue; + } + $finalDestination = $destination; + if (!$overwriteMode && $chunkingVersion !== null) { + $finalDestination .= "-{$davHuman}dav-{$davHuman}chunking"; + } elseif (!$overwriteMode && $chunkingVersion === null) { + $finalDestination .= "-{$davHuman}dav-regular"; + } + $responses[] = self::upload( + $baseUrl, + $user, + $password, + $source, + $finalDestination, + $xRequestId, + [], + $davPathVersion, + $chunkingVersion, + 2 + ); + } + } + return $responses; + } + + /** + * cut the file in multiple chunks + * returns an array of chunks with the content of the file + * + * @param string|null $file + * @param int|null $noOfChunks + * + * @return array $string + */ + public static function chunkFile(?string $file, ?int $noOfChunks = 1):array { + $size = \filesize($file); + $chunkSize = \ceil($size / $noOfChunks); + $chunks = []; + $fp = \fopen($file, 'r'); + while (!\feof($fp) && \ftell($fp) < $size) { + $chunks[] = \fread($fp, (int)$chunkSize); + } + \fclose($fp); + if (\count($chunks) === 0) { + // chunk an empty file + $chunks[] = ''; + } + return $chunks; + } + + /** + * creates a File with a specific size + * + * @param string|null $name full path of the file to create + * @param int|null $size + * + * @return void + */ + public static function createFileSpecificSize(?string $name, ?int $size):void { + if (\file_exists($name)) { + \unlink($name); + } + $file = \fopen($name, 'w'); + \fseek($file, \max($size - 1, 0), SEEK_CUR); + if ($size) { + \fwrite($file, 'a'); // write a dummy char at SIZE position + } + \fclose($file); + self::assertEquals( + 1, + \file_exists($name) + ); + self::assertEquals( + $size, + \filesize($name) + ); + } + + /** + * creates a File with a specific text content + * + * @param string|null $name full path of the file to create + * @param string|null $text + * + * @return void + */ + public static function createFileWithText(?string $name, ?string $text):void { + $file = \fopen($name, 'w'); + \fwrite($file, $text); + \fclose($file); + self::assertEquals( + 1, + \file_exists($name) + ); + } + + /** + * get the path of a file from FilesForUpload directory + * + * @param string|null $name name of the file to upload + * + * @return string + */ + public static function getUploadFilesDir(?string $name):string { + return \getenv("FILES_FOR_UPLOAD") . $name; + } +} diff --git a/tests/TestHelpers/UserHelper.php b/tests/TestHelpers/UserHelper.php new file mode 100644 index 000000000..8dda97fdc --- /dev/null +++ b/tests/TestHelpers/UserHelper.php @@ -0,0 +1,375 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ +namespace TestHelpers; + +use Exception; +use GuzzleHttp\Exception\ClientException; +use Psr\Http\Message\ResponseInterface; + +/** + * Helper to administrate users (and groups) through the provisioning API + * + * @author Artur Neumann + * + */ +class UserHelper { + /** + * + * @param string|null $baseUrl + * @param string $user + * @param string $key + * @param string $value + * @param string $adminUser + * @param string $adminPassword + * @param string $xRequestId + * @param int|null $ocsApiVersion + * + * @return ResponseInterface + */ + public static function editUser( + ?string $baseUrl, + string $user, + string $key, + string $value, + string $adminUser, + string $adminPassword, + string $xRequestId = '', + ?int $ocsApiVersion = 2 + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "PUT", + "/cloud/users/" . $user, + $xRequestId, + ["key" => $key, "value" => $value], + $ocsApiVersion + ); + } + + /** + * Send batch requests to edit the user. + * This will send multiple requests in parallel to the server which will be faster in comparison to waiting for each request to complete. + * + * @param string|null $baseUrl + * @param array|null $editData ['user' => '', 'key' => '', 'value' => ''] + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param int|null $ocsApiVersion + * + * @return array + * @throws Exception + */ + public static function editUserBatch( + ?string $baseUrl, + ?array $editData, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?int $ocsApiVersion = 2 + ):array { + $requests = []; + $client = HttpRequestHelper::createClient( + $adminUser, + $adminPassword + ); + + foreach ($editData as $data) { + $path = "/cloud/users/" . $data['user']; + $body = ["key" => $data['key'], 'value' => $data["value"]]; + // Create the OCS API requests and push them to an array. + \array_push( + $requests, + OcsApiHelper::createOcsRequest( + $baseUrl, + 'PUT', + $path, + $xRequestId, + $body + ) + ); + } + // Send the array of requests at once in parallel. + $results = HttpRequestHelper::sendBatchRequest($requests, $client); + + foreach ($results as $e) { + if ($e instanceof ClientException) { + $httpStatusCode = $e->getResponse()->getStatusCode(); + $reasonPhrase = $e->getResponse()->getReasonPhrase(); + throw new Exception( + "Unexpected failure when editing a user: HTTP status $httpStatusCode HTTP reason $reasonPhrase" + ); + } + } + return $results; + } + + /** + * + * @param string|null $baseUrl + * @param string|null $userName + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param int|null $ocsApiVersion + * + * @return ResponseInterface + */ + public static function getUser( + ?string $baseUrl, + ?string $userName, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?int $ocsApiVersion = 2 + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "GET", + "/cloud/users/" . $userName, + $xRequestId, + [], + $ocsApiVersion + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $userName + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param int|null $ocsApiVersion + * + * @return ResponseInterface + */ + public static function deleteUser( + ?string $baseUrl, + ?string $userName, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?int $ocsApiVersion = 2 + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "DELETE", + "/cloud/users/" . $userName, + $xRequestId, + [], + $ocsApiVersion + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $group + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * + * @return ResponseInterface + */ + public static function createGroup( + ?string $baseUrl, + ?string $group, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '' + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "POST", + "/cloud/groups", + $xRequestId, + ['groupid' => $group] + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $group + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param int|null $ocsApiVersion + * + * @return ResponseInterface + */ + public static function deleteGroup( + ?string $baseUrl, + ?string $group, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?int $ocsApiVersion = 2 + ):ResponseInterface { + $group = \rawurlencode($group); + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "DELETE", + "/cloud/groups/" . $group, + $xRequestId, + [], + $ocsApiVersion + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $group + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param int|null $ocsApiVersion (1|2) + * + * @return ResponseInterface + */ + public static function addUserToGroup( + ?string $baseUrl, + ?string $user, + ?string $group, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?int $ocsApiVersion = 2 + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "POST", + "/cloud/users/" . $user . "/groups", + $xRequestId, + ['groupid' => $group], + $ocsApiVersion + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $group + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param int|null $ocsApiVersion (1|2) + * + * @return ResponseInterface + */ + public static function removeUserFromGroup( + ?string $baseUrl, + ?string $user, + ?string $group, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId, + ?int $ocsApiVersion = 2 + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "DELETE", + "/cloud/users/" . $user . "/groups", + $xRequestId, + ['groupid' => $group], + $ocsApiVersion + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param string|null $search + * + * @return ResponseInterface + */ + public static function getGroups( + ?string $baseUrl, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?string $search ="" + ):ResponseInterface { + return OcsApiHelper::sendRequest( + $baseUrl, + $adminUser, + $adminPassword, + "GET", + "/cloud/groups?search=" . $search, + $xRequestId + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $adminUser + * @param string|null $adminPassword + * @param string|null $xRequestId + * @param string|null $search + * + * @return string[] + * @throws Exception + */ + public static function getGroupsAsArray( + ?string $baseUrl, + ?string $adminUser, + ?string $adminPassword, + ?string $xRequestId = '', + ?string $search = "" + ):array { + $result = self::getGroups( + $baseUrl, + $adminUser, + $adminPassword, + $xRequestId, + $search + ); + $groups = HttpRequestHelper::getResponseXml($result, __METHOD__)->xpath(".//groups")[0]; + $return = []; + foreach ($groups as $group) { + $return[] = $group->__toString(); + } + return $return; + } +} diff --git a/tests/TestHelpers/WebDavHelper.php b/tests/TestHelpers/WebDavHelper.php new file mode 100644 index 000000000..e8d0c9905 --- /dev/null +++ b/tests/TestHelpers/WebDavHelper.php @@ -0,0 +1,898 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ +namespace TestHelpers; + +use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use InvalidArgumentException; +use PHPUnit\Framework\Assert; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use DateTime; +use TestHelpers\SpaceNotFoundException; + +/** + * Helper to make WebDav Requests + * + * @author Artur Neumann + * + */ +class WebDavHelper { + public const DAV_VERSION_OLD = 1; + public const DAV_VERSION_NEW = 2; + public const DAV_VERSION_SPACES = 3; + public static $SPACE_ID_FROM_OCIS = ''; + + /** + * @var array of users with their different spaces ids + */ + public static $spacesIdRef = []; + + /** + * clear space id reference for user + * + * @param string|null $user + * + * @return void + * @throws Exception + */ + public static function removeSpaceIdReferenceForUser( + ?string $user + ):void { + if (\array_key_exists($user, self::$spacesIdRef)) { + unset(self::$spacesIdRef[$user]); + } + } + + /** + * returns the id of a file + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $password + * @param string|null $path + * @param string|null $xRequestId + * @param int|null $davPathVersionToUse + * + * @return string + * @throws Exception + */ + public static function getFileIdForPath( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $path, + ?string $xRequestId = '', + ?int $davPathVersionToUse = self::DAV_VERSION_NEW + ): string { + $body + = ' + + + + + '; + $response = self::makeDavRequest( + $baseUrl, + $user, + $password, + "PROPFIND", + $path, + null, + $xRequestId, + $body, + $davPathVersionToUse + ); + \preg_match( + '/\([^\<]*)\<\/oc:fileid\>/', + $response->getBody()->getContents(), + $matches + ); + + if (!isset($matches[1])) { + throw new Exception("could not find fileId of $path"); + } + + return $matches[1]; + } + + /** + * returns body for propfind + * + * @param array|null $properties + * + * @return string + * @throws Exception + */ + public static function getBodyForPropfind(?array $properties): string { + $propertyBody = ""; + $extraNamespaces = ""; + foreach ($properties as $namespaceString => $property) { + if (\is_int($namespaceString)) { + //default namespace prefix if the property has no array key + //also used if no prefix is given in the property value + $namespacePrefix = "d"; + } else { + //calculate the namespace prefix and namespace from the array key + $matches = []; + \preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches); + $nameSpace = $matches[2]; + $namespacePrefix = $matches[1]; + $extraNamespaces .= " xmlns:$namespacePrefix=\"$nameSpace\" "; + } + //if a namespace prefix is given in the property value use that + if (\strpos($property, ":") !== false) { + $propertyParts = \explode(":", $property); + $namespacePrefix = $propertyParts[0]; + $property = $propertyParts[1]; + } + $propertyBody .= "<$namespacePrefix:$property/>"; + } + $body = " + + $propertyBody + "; + return $body; + } + + /** + * sends a PROPFIND request + * with these registered namespaces: + * | prefix | namespace | + * | d | DAV: | + * | oc | http://owncloud.org/ns | + * | ocs | http://open-collaboration-services.org/ns | + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $password + * @param string|null $path + * @param string[] $properties + * string can contain namespace prefix, + * if no prefix is given 'd:' is used as prefix + * if associated array is used then the key will be used as namespace + * @param string|null $xRequestId + * @param string|null $folderDepth + * @param string|null $type + * @param int|null $davPathVersionToUse + * @param string|null $doDavRequestAsUser + * + * @return ResponseInterface + */ + public static function propfind( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $path, + ?array $properties, + ?string $xRequestId = '', + ?string $folderDepth = '0', + ?string $type = "files", + ?int $davPathVersionToUse = self::DAV_VERSION_NEW, + ?string $doDavRequestAsUser = null + ):ResponseInterface { + $body = self::getBodyForPropfind($properties); + $folderDepth = (string) $folderDepth; + if ($folderDepth !== '0' && $folderDepth !== '1' && $folderDepth !== 'infinity') { + throw new InvalidArgumentException('Invalid depth value ' . $folderDepth); + } + $headers = ['Depth' => $folderDepth]; + return self::makeDavRequest( + $baseUrl, + $user, + $password, + "PROPFIND", + $path, + $headers, + $xRequestId, + $body, + $davPathVersionToUse, + $type, + null, + null, + false, + null, + null, + [], + $doDavRequestAsUser + ); + } + + /** + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $password + * @param string|null $path + * @param string|null $propertyName + * @param string|null $propertyValue + * @param string|null $xRequestId + * @param string|null $namespaceString string containing prefix and namespace + * e.g "x1='http://whatever.org/ns'" + * @param int|null $davPathVersionToUse + * @param string|null $type + * + * @return ResponseInterface + */ + public static function proppatch( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $path, + ?string $propertyName, + ?string $propertyValue, + ?string $xRequestId = '', + ?string $namespaceString = "oc='http://owncloud.org/ns'", + ?int $davPathVersionToUse = self::DAV_VERSION_NEW, + ?string $type="files" + ):ResponseInterface { + $matches = []; + \preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches); + $namespace = $matches[2]; + $namespacePrefix = $matches[1]; + $propertyBody = "<$namespacePrefix:$propertyName" . + " xmlns:$namespacePrefix=\"$namespace\">" . + "$propertyValue" . + ""; + $body = " + + + $propertyBody + + "; + return self::makeDavRequest( + $baseUrl, + $user, + $password, + "PROPPATCH", + $path, + [], + $xRequestId, + $body, + $davPathVersionToUse, + $type + ); + } + + /** + * gets namespace-prefix, namespace url and propName from provided namespaceString or property + * or otherwise use default + * + * @param string|null $namespaceString + * @param string|null $property + * + * @return array + */ + public static function getPropertyWithNamespaceInfo(?string $namespaceString = "", ?string $property = ""):array { + $namespace = ""; + $namespacePrefix = ""; + if (\is_int($namespaceString)) { + //default namespace prefix if the property has no array key + //also used if no prefix is given in the property value + $namespacePrefix = "d"; + $namespace = "DAV:"; + } elseif ($namespaceString) { + //calculate the namespace prefix and namespace from the array key + $matches = []; + \preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches); + $namespacePrefix = $matches[1]; + $namespace = $matches[2]; + } + //if a namespace prefix is given in the property value use that + if ($property && \strpos($property, ":")) { + $propertyParts = \explode(":", $property); + $namespacePrefix = $propertyParts[0]; + $property = $propertyParts[1]; + } + return [$namespacePrefix, $namespace, $property]; + } + + /** + * sends HTTP request PROPPATCH method with multiple properties + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $password + * @param string $path + * @param array|null $propertiesArray + * @param string|null $xRequestId + * @param int|null $davPathVersion + * @param string|null $namespaceString + * @param string|null $type + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function proppatchWithMultipleProps( + ?string $baseUrl, + ?string $user, + ?string $password, + string $path, + ?array $propertiesArray, + ?string $xRequestId = '', + ?int $davPathVersion = null, + ?string $namespaceString = "oc='http://owncloud.org/ns'", + ?string $type="files" + ):ResponseInterface { + $propertyBody = ""; + foreach ($propertiesArray as $propertyArray) { + $property = $propertyArray["propertyName"]; + $value = $propertyArray["propertyValue"]; + [$namespacePrefix, $namespace, $property] = self::getPropertyWithNamespaceInfo( + $namespaceString, + $property + ); + $propertyBody .= "\n\t<$namespacePrefix:$property>" . + "$value" . + ""; + } + $body = " + + + $propertyBody + + + "; + return self::makeDavRequest( + $baseUrl, + $user, + $password, + "PROPPATCH", + $path, + [], + $xRequestId, + $body, + $davPathVersion, + $type + ); + } + + /** + * returns the response to listing a folder (collection) + * + * @param string|null $baseUrl + * @param string|null $user + * @param string|null $password + * @param string|null $path + * @param string|null $folderDepth + * @param string|null $xRequestId + * @param string[] $properties + * @param string|null $type + * @param int|null $davPathVersionToUse + * + * @return ResponseInterface + */ + public static function listFolder( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $path, + ?string $folderDepth, + ?string $xRequestId = '', + ?array $properties = null, + ?string $type = "files", + ?int $davPathVersionToUse = self::DAV_VERSION_NEW + ):ResponseInterface { + if (!$properties) { + $properties = [ + 'getetag', 'resourcetype' + ]; + } + return self::propfind( + $baseUrl, + $user, + $password, + $path, + $properties, + $xRequestId, + $folderDepth, + $type, + $davPathVersionToUse + ); + } + + /** + * Generates UUIDv4 + * Example: 123e4567-e89b-12d3-a456-426614174000 + * + * @return string + * @throws Exception + */ + public static function generateUUIDv4():string { + // generate 16 bytes (128 bits) of random data or use the data passed into the function. + $data = random_bytes(16); + \assert(\strlen($data) == 16); + + $data[6] = \chr(\ord($data[6]) & 0x0f | 0x40); // set version to 0100 + $data[8] = \chr(\ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 + + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + + /** + * fetches personal space id for provided user + * + * @param string $baseUrl + * @param string $user + * @param string $password + * @param string $xRequestId + * + * @return string + * @throws GuzzleException + * @throws Exception + */ + public static function getPersonalSpaceIdForUser(string $baseUrl, string $user, string $password, string $xRequestId):string { + if (\array_key_exists($user, self::$spacesIdRef) && \array_key_exists("personal", self::$spacesIdRef[$user])) { + return self::$spacesIdRef[$user]["personal"]; + } + $trimmedBaseUrl = \trim($baseUrl, "/"); + $drivesPath = '/graph/v1.0/me/drives'; + $fullUrl = $trimmedBaseUrl . $drivesPath; + $response = HttpRequestHelper::get( + $fullUrl, + $xRequestId, + $user, + $password + ); + $bodyContents = $response->getBody()->getContents(); + $json = \json_decode($bodyContents); + $personalSpaceId = ''; + if ($json === null) { + // the graph endpoint did not give a useful answer + // try getting the information from the webdav endpoint + $fullUrl = $trimmedBaseUrl . '/remote.php/webdav'; + $response = HttpRequestHelper::sendRequest( + $fullUrl, + $xRequestId, + 'PROPFIND', + $user, + $password + ); + // we expect to get a multipart XML response with status 207 + $status = $response->getStatusCode(); + if ($status === 401) { + throw new SpaceNotFoundException(__METHOD__ . " Personal space not found for user " . $user); + } elseif ($status !== 207) { + throw new Exception( + __METHOD__ . " webdav propfind for user $user failed with status $status - so the personal space id cannot be discovered" + ); + } + $responseXmlObject = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $xmlPart = $responseXmlObject->xpath("/d:multistatus/d:response[1]/d:propstat/d:prop/oc:id"); + if ($xmlPart === false) { + throw new Exception( + __METHOD__ . " oc:id not found in webdav propfind for user $user - so the personal space id cannot be discovered" + ); + } + $ocIdRawString = $xmlPart[0]->__toString(); + $separator = "!"; + if (\strpos($ocIdRawString, $separator) !== false) { + // The string is not base64-encoded, because the exclamation mark is not in the base64 alphabet. + // We expect to have a string with 2 parts separated by the exclamation mark. + // This is the format introduced in 2022-02 + // oc:id should be something like: + // "7464caf6-1799-103c-9046-c7b74deb5f63!7464caf6-1799-103c-9046-c7b74deb5f63" + // There is no encoding to decode. + $decodedId = $ocIdRawString; + } else { + // fall-back to assuming that the oc:id is base64-encoded + // That is the format used before and up to 2022-02 + // This can be removed after both the edge and master branches of cs3org/reva are using the new format. + // oc:id should be some base64 encoded string like: + // "NzQ2NGNhZjYtMTc5OS0xMDNjLTkwNDYtYzdiNzRkZWI1ZjYzOjc0NjRjYWY2LTE3OTktMTAzYy05MDQ2LWM3Yjc0ZGViNWY2Mw==" + // That should decode to something like: + // "7464caf6-1799-103c-9046-c7b74deb5f63:7464caf6-1799-103c-9046-c7b74deb5f63" + $decodedId = base64_decode($ocIdRawString); + $separator = ":"; + } + $ocIdParts = \explode($separator, $decodedId); + if (\count($ocIdParts) !== 2) { + throw new Exception( + __METHOD__ . " the oc:id $decodedId for user $user does not have 2 parts separated by '$separator', so the personal space id cannot be discovered" + ); + } + $personalSpaceId = $ocIdParts[0]; + } else { + foreach ($json->value as $spaces) { + if ($spaces->driveType === "personal") { + $personalSpaceId = $spaces->id; + break; + } + } + } + if ($personalSpaceId) { + // If env var LOG_PERSONAL_SPACE_ID is defined, then output the details of the personal space id. + // This is a useful debugging tool to have confidence that the personal space id is found correctly. + if (\getenv('LOG_PERSONAL_SPACE_ID') !== false) { + echo __METHOD__ . " personal space id of user $user is $personalSpaceId\n"; + } + self::$spacesIdRef[$user] = []; + self::$spacesIdRef[$user]["personal"] = $personalSpaceId; + return $personalSpaceId; + } else { + throw new SpaceNotFoundException(__METHOD__ . " Personal space not found for user " . $user); + } + } + + /** + * First checks if a user exist to return its space ID + * In case of any exception, it returns a fake space ID + * + * @param string $baseUrl + * @param string $user + * @param string $password + * @param string $xRequestId + * + * @return string + * @throws Exception + */ + public static function getPersonalSpaceIdForUserOrFakeIfNotFound(string $baseUrl, string $user, string $password, string $xRequestId):string { + try { + $spaceId = self::getPersonalSpaceIdForUser( + $baseUrl, + $user, + $password, + $xRequestId, + ); + } catch (SpaceNotFoundException $e) { + // if the fetch fails, and the user is not found, then a fake space id is prepared + // this is useful for testing when the personal space is of a non-existing user + $fakeSpaceId = self::generateUUIDv4(); + self::$spacesIdRef[$user]["personal"] = $fakeSpaceId; + $spaceId = $fakeSpaceId; + } + return $spaceId; + } + + /** + * sends a DAV request + * + * @param string|null $baseUrl + * URL of owncloud e.g. http://localhost:8080 + * should include the subfolder if owncloud runs in a subfolder + * e.g. http://localhost:8080/owncloud-core + * @param string|null $user + * @param string|null $password or token when bearer auth is used + * @param string|null $method PUT, GET, DELETE, etc. + * @param string|null $path + * @param array|null $headers + * @param string|null $xRequestId + * @param string|null|resource|StreamInterface $body + * @param int|null $davPathVersionToUse (1|2|3) + * @param string|null $type of request + * @param string|null $sourceIpAddress to initiate the request from + * @param string|null $authType basic|bearer + * @param bool $stream Set to true to stream a response rather + * than download it all up-front. + * @param int|null $timeout + * @param Client|null $client + * @param array|null $urlParameter to concatenate with path + * @param string|null $doDavRequestAsUser run the DAV as this user, if null its same as $user + * + * @return ResponseInterface + * @throws GuzzleException + * @throws Exception + */ + public static function makeDavRequest( + ?string $baseUrl, + ?string $user, + ?string $password, + ?string $method, + ?string $path, + ?array $headers, + ?string $xRequestId = '', + $body = null, + ?int $davPathVersionToUse = self::DAV_VERSION_OLD, + ?string $type = "files", + ?string $sourceIpAddress = null, + ?string $authType = "basic", + ?bool $stream = false, + ?int $timeout = 0, + ?Client $client = null, + ?array $urlParameter = [], + ?string $doDavRequestAsUser = null + ):ResponseInterface { + $baseUrl = self::sanitizeUrl($baseUrl, true); + + // We need to manipulate and use path as a string. + // So ensure that it is a string to avoid any type-conversion errors. + if ($path === null) { + $path = ""; + } + + // get space id if testing with spaces dav + if (self::$SPACE_ID_FROM_OCIS === '' && $davPathVersionToUse === self::DAV_VERSION_SPACES) { + $spaceId = self::getPersonalSpaceIdForUserOrFakeIfNotFound( + $baseUrl, + $doDavRequestAsUser ?? $user, + $password, + $xRequestId + ); + } else { + $spaceId = self::$SPACE_ID_FROM_OCIS; + } + + $davPath = self::getDavPath($doDavRequestAsUser ?? $user, $davPathVersionToUse, $type, $spaceId); + + //replace %, # and ? and in the path, Guzzle will not encode them + $urlSpecialChar = [['%', '#', '?'], ['%25', '%23', '%3F']]; + $path = \str_replace($urlSpecialChar[0], $urlSpecialChar[1], $path); + + if (!empty($urlParameter)) { + $urlParameter = \http_build_query($urlParameter, '', '&'); + $path .= '?' . $urlParameter; + } + $fullUrl = self::sanitizeUrl($baseUrl . $davPath . $path); + + if ($authType === 'bearer') { + $headers['Authorization'] = 'Bearer ' . $password; + $user = null; + $password = null; + } + if ($type === "public-files-new") { + if ($password === null || $password === "") { + $user = null; + } else { + $user = "public"; + } + } + $config = null; + if ($sourceIpAddress !== null) { + $config = [ 'curl' => [ CURLOPT_INTERFACE => $sourceIpAddress ]]; + } + + if ($headers !== null) { + foreach ($headers as $key => $value) { + //? and # need to be encoded in the Destination URL + if ($key === "Destination") { + $headers[$key] = \str_replace( + $urlSpecialChar[0], + $urlSpecialChar[1], + $value + ); + break; + } + } + } + + //Clear the space ID from ocis after each request + self::$SPACE_ID_FROM_OCIS = ''; + return HttpRequestHelper::sendRequest( + $fullUrl, + $xRequestId, + $method, + $user, + $password, + $headers, + $body, + $config, + null, + $stream, + $timeout, + $client + ); + } + + /** + * get the dav path + * + * @param string|null $user + * @param int|null $davPathVersionToUse (1|2) + * @param string|null $type + * @param string|null $spaceId + * + * @return string + */ + public static function getDavPath( + ?string $user, + ?int $davPathVersionToUse = null, + ?string $type = "files", + ?string $spaceId = null + ):string { + $newTrashbinDavPath = "remote.php/dav/trash-bin/$user/"; + if ($type === "public-files" || $type === "public-files-old") { + return "public.php/webdav/"; + } + if ($type === "public-files-new") { + return "remote.php/dav/public-files/$user/"; + } + if ($type === "archive") { + return "remote.php/dav/archive/$user/files"; + } + if ($type === "customgroups") { + return "remote.php/dav/"; + } + if ($davPathVersionToUse === self::DAV_VERSION_SPACES) { + if (($spaceId === null) || (\strlen($spaceId) === 0)) { + throw new InvalidArgumentException( + __METHOD__ . " A spaceId must be passed when using DAV path version 3 (spaces)" + ); + } + if ($type === "trash-bin") { + return "/remote.php/dav/spaces/trash-bin/" . $spaceId . '/'; + } + return "dav/spaces/" . $spaceId . '/'; + } else { + if ($davPathVersionToUse === self::DAV_VERSION_OLD) { + if ($type === "trash-bin") { + // Since there is no trash bin endpoint for old dav version, new dav version's endpoint is used here. + return $newTrashbinDavPath; + } + return "remote.php/webdav/"; + } elseif ($davPathVersionToUse === self::DAV_VERSION_NEW) { + if ($type === "files") { + $path = 'remote.php/dav/files/'; + return $path . $user . '/'; + } elseif ($type === "trash-bin") { + return $newTrashbinDavPath; + } else { + return "remote.php/dav"; + } + } else { + throw new InvalidArgumentException( + "DAV path version $davPathVersionToUse is unknown" + ); + } + } + } + + /** + * make sure there are no double slash in the URL + * + * @param string|null $url + * @param bool|null $trailingSlash forces a trailing slash + * + * @return string + */ + public static function sanitizeUrl(?string $url, ?bool $trailingSlash = false):string { + if ($trailingSlash === true) { + $url = $url . "/"; + } else { + $url = \rtrim($url, "/"); + } + $url = \preg_replace("/([^:]\/)\/+/", '$1', $url); + return $url; + } + + /** + * decides if the proposed dav version and chunking version are + * a valid combination. + * If no chunkingVersion is specified, then any dav version is valid. + * If a chunkingVersion is specified, then it has to match the dav version. + * Note: in future the dav and chunking versions might or might not + * move together and/or be supported together. So a more complex + * matrix could be needed here. + * + * @param string|int $davPathVersion + * @param string|int|null $chunkingVersion + * + * @return boolean is this a valid combination + */ + public static function isValidDavChunkingCombination( + $davPathVersion, + $chunkingVersion + ): bool { + if ($davPathVersion === self::DAV_VERSION_SPACES) { + // allow only old chunking version when using the spaces dav + return $chunkingVersion === 1; + } + return ( + ($chunkingVersion === 'no' || $chunkingVersion === null) || + ($davPathVersion === $chunkingVersion) + ); + } + + /** + * get Mtime of File in a public link share + * + * @param string|null $baseUrl + * @param string|null $fileName + * @param string|null $token + * @param string|null $xRequestId + * @param int|null $davVersionToUse + * + * @return string + * @throws Exception + */ + public static function getMtimeOfFileinPublicLinkShare( + ?string $baseUrl, + ?string $fileName, + ?string $token, + ?string $xRequestId = '', + ?int $davVersionToUse = self::DAV_VERSION_NEW + ):string { + $response = self::propfind( + $baseUrl, + null, + null, + "/public-files/{$token}/{$fileName}", + ['d:getlastmodified'], + $xRequestId, + '1', + null, + $davVersionToUse + ); + $responseXmlObject = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $xmlPart = $responseXmlObject->xpath("//d:getlastmodified"); + + return $xmlPart[0]->__toString(); + } + + /** + * get Mtime of a resource + * + * @param string|null $user + * @param string|null $password + * @param string|null $baseUrl + * @param string|null $resource + * @param string|null $xRequestId + * @param int|null $davPathVersionToUse + * + * @return string + * @throws Exception + */ + public static function getMtimeOfResource( + ?string $user, + ?string $password, + ?string $baseUrl, + ?string $resource, + ?string $xRequestId = '', + ?int $davPathVersionToUse = self::DAV_VERSION_NEW + ):string { + $response = self::propfind( + $baseUrl, + $user, + $password, + $resource, + ["getlastmodified"], + $xRequestId, + "0", + "files", + $davPathVersionToUse + ); + $responseXmlObject = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $xmlpart = $responseXmlObject->xpath("//d:getlastmodified"); + Assert::assertArrayHasKey( + 0, + $xmlpart, + __METHOD__ . " XML part does not have key 0. Expected a value at index 0 of 'xmlPart' but, found: " . (string) json_encode($xmlpart) + ); + $mtime = new DateTime($xmlpart[0]->__toString()); + return $mtime->format('U'); + } +} diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 6527404c1..4c83ec685 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -73,7 +73,7 @@ default: - WebDavPropertiesContext: - TUSContext: - SpacesTUSContext: - + apiContract: paths: - '%paths.base%/../features/apiContract' @@ -152,4 +152,6 @@ default: - TrashbinContext: extensions: + rdx\behatvars\BehatVariablesExtension: ~ + Cjm\Behat\StepThroughExtension: ~ diff --git a/tests/acceptance/features/bootstrap/AppConfigurationContext.php b/tests/acceptance/features/bootstrap/AppConfigurationContext.php new file mode 100644 index 000000000..f44177d8c --- /dev/null +++ b/tests/acceptance/features/bootstrap/AppConfigurationContext.php @@ -0,0 +1,591 @@ + + * @author Sergio Bertolin + * @author Phillip Davis + * @copyright Copyright (c) 2018, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use GuzzleHttp\Exception\GuzzleException; +use PHPUnit\Framework\Assert; +use TestHelpers\AppConfigHelper; +use TestHelpers\OcsApiHelper; +use Behat\Gherkin\Node\TableNode; +use Behat\Behat\Context\Context; + +/** + * AppConfiguration trait + */ +class AppConfigurationContext implements Context { + /** + * @var FeatureContext + */ + private $featureContext; + + /** + * @When /^the administrator sets parameter "([^"]*)" of app "([^"]*)" to ((?:'[^']*')|(?:"[^"]*"))$/ + * + * @param string $parameter + * @param string $app + * @param string $value + * + * @return void + * @throws Exception + */ + public function adminSetsServerParameterToUsingAPI( + string $parameter, + string $app, + string $value + ):void { + // The capturing group of the regex always includes the quotes at each + // end of the captured string, so trim them. + $value = \trim($value, $value[0]); + $this->modifyAppConfig($app, $parameter, $value); + } + + /** + * @Given /^parameter "([^"]*)" of app "([^"]*)" has been set to ((?:'[^']*')|(?:"[^"]*"))$/ + * + * @param string $parameter + * @param string $app + * @param string $value + * + * @return void + * @throws Exception + */ + public function serverParameterHasBeenSetTo(string $parameter, string $app, string $value):void { + // The capturing group of the regex always includes the quotes at each + // end of the captured string, so trim them. + if (\TestHelpers\OcisHelper::isTestingOnOcisOrReva()) { + return; + } + $value = \trim($value, $value[0]); + $this->modifyAppConfig($app, $parameter, $value); + $this->featureContext->clearStatusCodeArrays(); + } + + /** + * @Then the capabilities setting of :capabilitiesApp path :capabilitiesPath should be :expectedValue + * @Given the capabilities setting of :capabilitiesApp path :capabilitiesPath has been confirmed to be :expectedValue + * + * @param string $capabilitiesApp the "app" name in the capabilities response + * @param string $capabilitiesPath the path to the element + * @param string $expectedValue + * + * @return void + * @throws Exception + */ + public function theCapabilitiesSettingOfAppParameterShouldBe( + string $capabilitiesApp, + string $capabilitiesPath, + string $expectedValue + ):void { + $this->theAdministratorGetsCapabilitiesCheckResponse(); + $actualValue = $this->getAppParameter($capabilitiesApp, $capabilitiesPath); + + Assert::assertEquals( + $expectedValue, + $actualValue, + __METHOD__ + . " $capabilitiesApp path $capabilitiesPath should be $expectedValue but is $actualValue" + ); + } + + /** + * @param string $capabilitiesApp the "app" name in the capabilities response + * @param string $capabilitiesPath the path to the element + * + * @return string + * @throws Exception + */ + public function getAppParameter(string $capabilitiesApp, string $capabilitiesPath):string { + return $this->getParameterValueFromXml( + $this->getCapabilitiesXml(__METHOD__), + $capabilitiesApp, + $capabilitiesPath + ); + } + + /** + * @When user :username retrieves the capabilities using the capabilities API + * + * @param string $username + * + * @return void + * @throws GuzzleException + * @throws JsonException + */ + public function userGetsCapabilities(string $username):void { + $user = $this->featureContext->getActualUsername($username); + $password = $this->featureContext->getPasswordForUser($user); + $this->featureContext->setResponse( + OcsApiHelper::sendRequest( + $this->featureContext->getBaseUrl(), + $user, + $password, + 'GET', + '/cloud/capabilities', + $this->featureContext->getStepLineRef(), + [], + $this->featureContext->getOcsApiVersion() + ) + ); + } + + /** + * @Given user :username has retrieved the capabilities + * + * @param string $username + * + * @return void + * @throws Exception + */ + public function userGetsCapabilitiesCheckResponse(string $username):void { + $this->userGetsCapabilities($username); + $statusCode = $this->featureContext->getResponse()->getStatusCode(); + if ($statusCode !== 200) { + throw new \Exception( + __METHOD__ + . " user $username returned unexpected status $statusCode" + ); + } + } + + /** + * @When the user retrieves the capabilities using the capabilities API + * + * @return void + */ + public function theUserGetsCapabilities():void { + $this->userGetsCapabilities($this->featureContext->getCurrentUser()); + } + + /** + * @Given the user has retrieved the capabilities + * + * @return void + * @throws Exception + */ + public function theUserGetsCapabilitiesCheckResponse():void { + $this->userGetsCapabilitiesCheckResponse($this->featureContext->getCurrentUser()); + } + + /** + * @return string + * @throws Exception + */ + public function getAdminUsernameForCapabilitiesCheck():string { + if (\TestHelpers\OcisHelper::isTestingOnReva()) { + // When testing on reva we don't have a user called "admin" to use + // to access the capabilities. So create an ordinary user on-the-fly + // with a default password. That user should be able to get a + // capabilities response that the test can process. + $adminUsername = "PseudoAdminForRevaTest"; + $createdUsers = $this->featureContext->getCreatedUsers(); + if (!\array_key_exists($adminUsername, $createdUsers)) { + $this->featureContext->createUser($adminUsername); + } + } else { + $adminUsername = $this->featureContext->getAdminUsername(); + } + return $adminUsername; + } + + /** + * @When the administrator retrieves the capabilities using the capabilities API + * + * @return void + */ + public function theAdministratorGetsCapabilities():void { + $this->userGetsCapabilities($this->getAdminUsernameForCapabilitiesCheck()); + } + + /** + * @Given the administrator has retrieved the capabilities + * + * @return void + * @throws Exception + */ + public function theAdministratorGetsCapabilitiesCheckResponse():void { + $this->userGetsCapabilitiesCheckResponse($this->getAdminUsernameForCapabilitiesCheck()); + } + + /** + * @param string $exceptionText text to put at the front of exception messages + * + * @return SimpleXMLElement latest retrieved capabilities in XML format + * @throws Exception + */ + public function getCapabilitiesXml(string $exceptionText = ''): SimpleXMLElement { + if ($exceptionText === '') { + $exceptionText = __METHOD__; + } + return $this->featureContext->getResponseXml(null, $exceptionText)->data->capabilities; + } + + /** + * @param string $exceptionText text to put at the front of exception messages + * + * @return SimpleXMLElement latest retrieved version data in XML format + * @throws Exception + */ + public function getVersionXml(string $exceptionText = ''): SimpleXMLElement { + if ($exceptionText === '') { + $exceptionText = __METHOD__; + } + return $this->featureContext->getResponseXml(null, $exceptionText)->data->version; + } + + /** + * @param SimpleXMLElement $xml of the capabilities + * @param string $capabilitiesApp the "app" name in the capabilities response + * @param string $capabilitiesPath the path to the element + * + * @return string + */ + public function getParameterValueFromXml( + SimpleXMLElement $xml, + string $capabilitiesApp, + string $capabilitiesPath + ):string { + $path_to_element = \explode('@@@', $capabilitiesPath); + $answeredValue = $xml->{$capabilitiesApp}; + foreach ($path_to_element as $element) { + $nameIndexParts = \explode('[', $element); + if (isset($nameIndexParts[1])) { + // This part of the path should be something like "some_element[1]" + // Separately extract the name and the index + $name = $nameIndexParts[0]; + $index = (int) \explode(']', $nameIndexParts[1])[0]; + // and use those to construct the reference into the next XML level + $answeredValue = $answeredValue->{$name}[$index]; + } else { + if ($element !== "") { + $answeredValue = $answeredValue->{$element}; + } + } + } + + return (string) $answeredValue; + } + + /** + * @param SimpleXMLElement $xml of the capabilities + * @param string $capabilitiesApp the "app" name in the capabilities response + * @param string $capabilitiesPath the path to the element + * + * @return boolean + */ + public function parameterValueExistsInXml( + SimpleXMLElement $xml, + string $capabilitiesApp, + string $capabilitiesPath + ):bool { + $path_to_element = \explode('@@@', $capabilitiesPath); + $answeredValue = $xml->{$capabilitiesApp}; + + foreach ($path_to_element as $element) { + $nameIndexParts = \explode('[', $element); + if (isset($nameIndexParts[1])) { + // This part of the path should be something like "some_element[1]" + // Separately extract the name and the index + $name = $nameIndexParts[0]; + $index = (int) \explode(']', $nameIndexParts[1])[0]; + // and use those to construct the reference into the next XML level + if (isset($answeredValue->{$name}[$index])) { + $answeredValue = $answeredValue->{$name}[$index]; + } else { + // The path ends at this level + return false; + } + } else { + if (isset($answeredValue->{$element})) { + $answeredValue = $answeredValue->{$element}; + } else { + // The path ends at this level + return false; + } + } + } + + return true; + } + + /** + * @param string $app + * @param string $parameter + * @param string $value + * + * @return void + * @throws Exception + */ + public function modifyAppConfig(string $app, string $parameter, string $value):void { + AppConfigHelper::modifyAppConfig( + $this->featureContext->getBaseUrl(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $app, + $parameter, + $value, + $this->featureContext->getStepLineRef(), + $this->featureContext->getOcsApiVersion() + ); + } + + /** + * @param array $appParameterValues + * + * @return void + * @throws Exception + */ + public function modifyAppConfigs(array $appParameterValues):void { + AppConfigHelper::modifyAppConfigs( + $this->featureContext->getBaseUrl(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $appParameterValues, + $this->featureContext->getStepLineRef(), + $this->featureContext->getOcsApiVersion() + ); + } + + /** + * @When the administrator adds url :url as trusted server using the testing API + * + * @param string $url + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorAddsUrlAsTrustedServerUsingTheTestingApi(string $url):void { + $adminUser = $this->featureContext->getAdminUsername(); + $response = OcsApiHelper::sendRequest( + $this->featureContext->getBaseUrl(), + $adminUser, + $this->featureContext->getAdminPassword(), + 'POST', + "/apps/testing/api/v1/trustedservers", + $this->featureContext->getStepLineRef(), + ['url' => $this->featureContext->substituteInLineCodes($url)] + ); + $this->featureContext->setResponse($response); + $this->featureContext->pushToLastStatusCodesArrays(); + } + + /** + * Return text that contains the details of the URL, including any differences due to inline codes + * + * @param string $url + * + * @return string + */ + private function getUrlStringForMessage(string $url):string { + $text = $url; + $expectedUrl = $this->featureContext->substituteInLineCodes($url); + if ($expectedUrl !== $url) { + $text .= " ($expectedUrl)"; + } + return $text; + } + + /** + * @param string $url + * + * @return string + */ + private function getNotTrustedServerMessage(string $url):string { + return + "URL " + . $this->getUrlStringForMessage($url) + . " is not a trusted server but should be"; + } + + /** + * @Then url :url should be a trusted server + * + * @param string $url + * + * @return void + * @throws Exception + */ + public function urlShouldBeATrustedServer(string $url):void { + $trustedServers = $this->featureContext->getTrustedServers(); + foreach ($trustedServers as $server => $id) { + if ($server === $this->featureContext->substituteInLineCodes($url)) { + return; + } + } + Assert::fail($this->getNotTrustedServerMessage($url)); + } + + /** + * @Then the trusted server list should include these urls: + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theTrustedServerListShouldIncludeTheseUrls(TableNode $table):void { + $trustedServers = $this->featureContext->getTrustedServers(); + $expected = $table->getColumnsHash(); + + foreach ($expected as $server) { + $found = false; + foreach ($trustedServers as $url => $id) { + if ($url === $this->featureContext->substituteInLineCodes($server['url'])) { + $found = true; + break; + } + } + if (!$found) { + Assert::fail($this->getNotTrustedServerMessage($server['url'])); + } + } + } + + /** + * @Given the administrator has added url :url as trusted server + * + * @param string $url + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function theAdministratorHasAddedUrlAsTrustedServer(string $url):void { + $this->theAdministratorAddsUrlAsTrustedServerUsingTheTestingApi($url); + $status = $this->featureContext->getResponse()->getStatusCode(); + if ($status !== 201) { + throw new \Exception( + __METHOD__ . + " Could not add trusted server " . $this->getUrlStringForMessage($url) + . ". The request failed with status $status" + ); + } + } + + /** + * @When the administrator deletes url :url from trusted servers using the testing API + * + * @param string $url + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorDeletesUrlFromTrustedServersUsingTheTestingApi(string $url):void { + $adminUser = $this->featureContext->getAdminUsername(); + $response = OcsApiHelper::sendRequest( + $this->featureContext->getBaseUrl(), + $adminUser, + $this->featureContext->getAdminPassword(), + 'DELETE', + "/apps/testing/api/v1/trustedservers", + $this->featureContext->getStepLineRef(), + ['url' => $this->featureContext->substituteInLineCodes($url)] + ); + $this->featureContext->setResponse($response); + } + + /** + * @Then url :url should not be a trusted server + * + * @param string $url + * + * @return void + * @throws Exception + */ + public function urlShouldNotBeATrustedServer(string $url):void { + $trustedServers = $this->featureContext->getTrustedServers(); + foreach ($trustedServers as $server => $id) { + if ($server === $this->featureContext->substituteInLineCodes($url)) { + Assert::fail( + "URL " . $this->getUrlStringForMessage($url) + . " is a trusted server but is not expected to be" + ); + } + } + } + + /** + * @When the administrator deletes all trusted servers using the testing API + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorDeletesAllTrustedServersUsingTheTestingApi():void { + $adminUser = $this->featureContext->getAdminUsername(); + $response = OcsApiHelper::sendRequest( + $this->featureContext->getBaseUrl(), + $adminUser, + $this->featureContext->getAdminPassword(), + 'DELETE', + "/apps/testing/api/v1/trustedservers/all", + $this->featureContext->getStepLineRef() + ); + $this->featureContext->setResponse($response); + } + + /** + * @Given the trusted server list is cleared + * + * @return void + * @throws Exception + */ + public function theTrustedServerListIsCleared():void { + $this->theAdministratorDeletesAllTrustedServersUsingTheTestingApi(); + $statusCode = $this->featureContext->getResponse()->getStatusCode(); + if ($statusCode !== 204) { + $contents = $this->featureContext->getResponse()->getBody()->getContents(); + throw new \Exception( + __METHOD__ + . " Failed to clear all trusted servers" . $contents + ); + } + } + + /** + * @Then the trusted server list should be empty + * + * @return void + * @throws Exception + */ + public function theTrustedServerListShouldBeEmpty():void { + $trustedServers = $this->featureContext->getTrustedServers(); + Assert::assertEmpty( + $trustedServers, + __METHOD__ . " Trusted server list is not empty" + ); + } + + /** + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function setUpScenario(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/AuthContext.php b/tests/acceptance/features/bootstrap/AuthContext.php new file mode 100644 index 000000000..24c57b657 --- /dev/null +++ b/tests/acceptance/features/bootstrap/AuthContext.php @@ -0,0 +1,1158 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @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 + * + */ + +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use TestHelpers\HttpRequestHelper; +use Behat\Gherkin\Node\TableNode; +use Behat\Behat\Context\Context; +use TestHelpers\SetupHelper; + +/** + * Authentication functions + */ +class AuthContext implements Context { + /** + * @var string + */ + private $clientToken; + + /** + * @var string + */ + private $appToken; + + /** + * @var array + */ + private $appTokens; + + /** + * @var boolean + */ + private $tokenAuthHasBeenSet = false; + + /** + * @var FeatureContext + */ + private $featureContext; + + /** + * @var string 'true' or 'false' or '' + */ + private $tokenAuthHasBeenSetTo = ''; + + /** + * @return string + */ + public function getTokenAuthHasBeenSetTo():string { + return $this->tokenAuthHasBeenSetTo; + } + + /** + * get the client token that was last generated + * app acceptance tests that have their own step code may need to use this + * + * @return string client token + */ + public function getClientToken():string { + return $this->clientToken; + } + + /** + * get the app token that was last generated + * app acceptance tests that have their own step code may need to use this + * + * @return string app token + */ + public function getAppToken():string { + return $this->appToken; + } + + /** + * get the app token that was last generated + * app acceptance tests that have their own step code may need to use this + * + * @return array app tokens + */ + public function getAppTokens():array { + return $this->appTokens; + } + + /** + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function setUpScenario(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + + // Reset ResponseXml + $this->featureContext->setResponseXml([]); + + // Initialize SetupHelper class + SetupHelper::init( + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + } + + /** + * @When a user requests :url with :method and no authentication + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userRequestsURLWith(string $url, string $method):void { + $this->sendRequest($url, $method); + } + + /** + * @When user :user requests :url with :method and no authentication + * + * @param string $user + * @param string $url + * @param string $method + * + * @return void + * @throws JsonException + */ + public function userRequestsURLWithNoAuth(string $user, string $url, string $method):void { + $userRenamed = $this->featureContext->getActualUsername($user); + $url = $this->featureContext->substituteInLineCodes($url, $userRenamed); + $this->sendRequest($url, $method); + } + + /** + * @Given a user has requested :url with :method and no authentication + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userHasRequestedURLWith(string $url, string $method):void { + $this->sendRequest($url, $method); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * Verifies status code + * + * @param string $ocsCode + * @param string $httpCode + * @param string $endPoint + * + * @return void + * @throws Exception + */ + public function verifyStatusCode(string $ocsCode, string $httpCode, string $endPoint):void { + if ($ocsCode !== null) { + $this->featureContext->ocsContext->theOCSStatusCodeShouldBe( + $ocsCode, + $message = "Got unexpected OCS code while sending request to endpoint " . $endPoint + ); + } + $this->featureContext->theHTTPStatusCodeShouldBe( + $httpCode, + $message = "Got unexpected HTTP code while sending request to endpoint " . $endPoint + ); + } + + /** + * @When a user requests these endpoints with :method with body :body and no authentication about user :user + * + * @param string $method + * @param ?string $body + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithBodyAndNoAuthThenStatusCodeAboutUser(string $method, ?string $body, ?string $ofUser, TableNode $table):void { + $ofUser = \strtolower($this->featureContext->getActualUsername($ofUser)); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastOCSStatusCodesArray(); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $ofUser + ); + $this->sendRequest($row['endpoint'], $method, null, false, $body); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When a user requests these endpoints with :method with no authentication about user :user + * + * @param string $method + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithoutBodyAndNoAuth(string $method, string $ofUser, TableNode $table):void { + $this->userRequestsEndpointsWithBodyAndNoAuthThenStatusCodeAboutUser( + $method, + null, + $ofUser, + $table + ); + } + + /** + * @When a user requests these endpoints with :method and no authentication + * + * @param string $method + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithNoAuthentication(string $method, TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastOCSStatusCodesArray(); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + foreach ($table->getHash() as $row) { + $this->sendRequest($row['endpoint'], $method); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When the user :user requests these endpoints with :method with basic auth + * + * @param string $user + * @param string $method + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithBasicAuth(string $user, string $method, TableNode $table):void { + $user = $this->featureContext->getActualUsername($user); + $this->userRequestsEndpointsWithPassword($user, $method, null, $table); + } + + /** + * @When the user :user requests these endpoints with :method using basic auth and generated app password about user :ofUser + * + * @param string $user + * @param string $method + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithBasicAuthAndGeneratedPassword(string $user, string $method, string $ofUser, TableNode $table):void { + $this->requestEndpointsWithBasicAuthAndGeneratedPassword($user, $method, $ofUser, $table); + } + + /** + * @When /^the user "([^"]*)" requests these endpoints with "([^"]*)" to (?:get|set) property "([^"]*)" using basic auth and generated app password about user "([^"]*)"$/ + * + * @param string $user + * @param string $method + * @param string $property + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithBasicAuthAndGeneratedPasswordWithProperty( + string $user, + string $method, + string $property, + string $ofUser, + TableNode $table + ):void { + $this->requestEndpointsWithBasicAuthAndGeneratedPassword( + $user, + $method, + $ofUser, + $table, + null, + $property + ); + } + + /** + * @When /^the user "([^"]*)" requests these endpoints with "([^"]*)" to (?:get|set) property "([^"]*)" with password "([^"]*)" about user "([^"]*)"$/ + * + * @param string $user + * @param string $method + * @param string $property + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithPasswordWithProperty( + string $user, + string $method, + string $property, + string $ofUser, + TableNode $table + ):void { + $this->userRequestsEndpointsWithPassword( + $user, + $method, + $ofUser, + $table, + $property + ); + } + + /** + * @When the user :user requests these endpoints with :method with body :body using basic auth and generated app password about user :ofUser + * + * @param string $user + * @param string $method + * @param string $body + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithBasicAuthAndGeneratedPasswordWithBody( + string $user, + string $method, + string $body, + string $ofUser, + TableNode $table + ):void { + $header = []; + if ($method === 'MOVE' || $method === 'COPY') { + $header['Destination'] = '/path/to/destination'; + } + + $this->requestEndpointsWithBasicAuthAndGeneratedPassword( + $user, + $method, + $ofUser, + $table, + $body, + null, + $header, + ); + } + + /** + * @param string $user requesting user + * @param string $method http method + * @param string $ofUser resource owner + * @param TableNode $table endpoints table + * @param string|null $body body for request + * @param string|null $property property to get + * @param Array|null $header request header + * + * @return void + * @throws Exception + */ + public function requestEndpointsWithBasicAuthAndGeneratedPassword( + string $user, + string $method, + string $ofUser, + TableNode $table, + ?string $body = null, + ?string $property = null, + ?array $header = null + ):void { + $user = $this->featureContext->getActualUsername($user); + $ofUser = \strtolower($this->featureContext->getActualUsername($ofUser)); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastOCSStatusCodesArray(); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + if ($body === null && $property !== null) { + $body = $this->featureContext->getBodyForOCSRequest($method, $property); + } + + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $ofUser + ); + $this->userRequestsURLWithUsingBasicAuth($user, $row['endpoint'], $method, $this->appToken, $body, $header); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When user :user requests these endpoints with :method using password :password + * + * @param string $user + * @param string $method + * @param string|null $password + * @param TableNode $table + * @param string|null $property + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsWithPassword( + string $user, + string $method, + ?string $password, + TableNode $table, + ?string $property = null + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->featureContext->emptyLastOCSStatusCodesArray(); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + foreach ($table->getHash() as $row) { + $body = null; + if ($property !== null) { + $body = $this->featureContext->getBodyForOCSRequest($method, $property); + } + $this->userRequestsURLWithUsingBasicAuth($user, $row['endpoint'], $method, $password, $body); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When the administrator requests these endpoint with :method + * + * @param string $method + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function adminRequestsEndpoint(string $method, TableNode $table):void { + $this->adminRequestsEndpointsWithBodyWithPassword($method, null, null, null, $table); + } + + /** + * @When the administrator requests these endpoints with :method with body :body using password :password about user :ofUser + * + * @param string|null $method + * @param string|null $body + * @param string|null $password + * @param string|null $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function adminRequestsEndpointsWithBodyWithPassword( + ?string $method, + ?string $body, + ?string $password, + ?string $ofUser, + TableNode $table + ):void { + $ofUser = $this->featureContext->getActualUsername($ofUser); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $ofUser + ); + $this->userRequestsURLWithUsingBasicAuth( + $this->featureContext->getAdminUsername(), + $row['endpoint'], + $method, + $password, + $body + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When the administrator requests these endpoints with :method using password :password about user :ofUser + * + * @param string $method + * @param string $password + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function adminRequestsEndpointsWithPassword( + string $method, + string $password, + string $ofUser, + TableNode $table + ):void { + $this->adminRequestsEndpointsWithBodyWithPassword($method, null, $password, $ofUser, $table); + } + + /** + * @When user :user requests these endpoints with :method using basic token auth + * + * @param string $user + * @param string $method + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function whenUserWithNewClientTokenRequestsForEndpointUsingBasicTokenAuth(string $user, string $method, TableNode $table):void { + $user = $this->featureContext->getActualUsername($user); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $this->userRequestsURLWithUsingBasicTokenAuth($user, $row['endpoint'], $method); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When the user requests these endpoints with :method using a new browser session + * + * @param string $method + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsTheseEndpointsUsingNewBrowserSession(string $method, TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $this->userRequestsURLWithBrowserSession($row['endpoint'], $method); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When the user requests these endpoints with :method using the generated app password about user :user + * + * @param string $method + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsUsingTheGeneratedAppPasswordThenStatusCodeAboutUser(string $method, string $user, TableNode $table):void { + $user = \strtolower($this->featureContext->getActualUsername($user)); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $user + ); + $this->userRequestsURLWithUsingAppPassword($row['endpoint'], $method); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When the user requests these endpoints with :method using the generated app password + * + * @param string $method + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsEndpointsUsingTheGeneratedAppPassword(string $method, TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $this->userRequestsURLWithUsingAppPassword($row['endpoint'], $method); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @param string $url + * @param string $method + * @param string|null $authHeader + * @param bool $useCookies + * @param string|null $body + * @param array|null $headers + * + * @return void + */ + public function sendRequest( + string $url, + string $method, + ?string $authHeader = null, + bool $useCookies = false, + ?string $body = null, + ?array $headers = [] + ):void { + // reset responseXml + $this->featureContext->setResponseXml([]); + + $fullUrl = $this->featureContext->getBaseUrl() . $url; + + $cookies = null; + if ($useCookies) { + $cookies = $this->featureContext->getCookieJar(); + } + + if ($authHeader) { + $headers['Authorization'] = $authHeader; + } + $headers['OCS-APIREQUEST'] = 'true'; + if (isset($this->requestToken)) { + $headers['requesttoken'] = $this->featureContext->getRequestToken(); + } + $this->featureContext->setResponse( + HttpRequestHelper::sendRequest( + $fullUrl, + $this->featureContext->getStepLineRef(), + $method, + null, + null, + $headers, + $body, + null, + $cookies + ) + ); + } + + /** + * Use the private API to generate an app password + * + * @param string $name + * + * @return void + */ + public function userGeneratesNewAppPasswordNamed(string $name):void { + $url = $this->featureContext->getBaseUrl() . '/index.php/settings/personal/authtokens'; + $body = ['name' => $name]; + $headers = [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'OCS-APIREQUEST' => 'true', + 'requesttoken' => $this->featureContext->getRequestToken(), + 'X-Requested-With' => 'XMLHttpRequest' + ]; + $this->featureContext->setResponse( + HttpRequestHelper::post( + $url, + $this->featureContext->getStepLineRef(), + null, + null, + $headers, + $body, + null, + $this->featureContext->getCookieJar() + ) + ); + $token = \json_decode($this->featureContext->getResponse()->getBody()->getContents()); + $this->appToken = $token->token; + $this->appTokens[$token->deviceToken->name] + = ["id" => $token->deviceToken->id, "token" => $token->token]; + } + + /** + * Use the private API to generate an app password + * + * @param string $name + * + * @return void + */ + public function userDeletesAppPasswordNamed(string $name):void { + $url = $this->featureContext->getBaseUrl() . '/index.php/settings/personal/authtokens/' . $this->appTokens[$name]["id"]; + $headers = [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'OCS-APIREQUEST' => 'true', + 'requesttoken' => $this->featureContext->getRequestToken(), + 'X-Requested-With' => 'XMLHttpRequest' + ]; + $this->featureContext->setResponse( + HttpRequestHelper::delete( + $url, + $this->featureContext->getStepLineRef(), + null, + null, + $headers, + null, + null, + $this->featureContext->getCookieJar() + ) + ); + } + + /** + * @Given the user has generated a new app password named :name + * + * @param string $name + * + * @return void + */ + public function aNewAppPasswordHasBeenGenerated(string $name):void { + $this->userGeneratesNewAppPasswordNamed($name); + $this->featureContext->theHTTPStatusCodeShouldBe(200); + } + + /** + * @Given the user has deleted the app password named :name + * + * @param string $name + * + * @return void + */ + public function aNewAppPasswordHasBeenDeleted(string $name):void { + $this->userDeletesAppPasswordNamed($name); + $this->featureContext->theHTTPStatusCodeShouldBe(200); + } + + /** + * @When user :user generates a new client token using the token API + * @Given a new client token for :user has been generated + * + * @param string $user + * + * @return void + */ + public function aNewClientTokenHasBeenGenerated(string $user):void { + $user = $this->featureContext->getActualUsername($user); + $body = \json_encode( + [ + 'user' => $this->featureContext->getActualUsername($user), + 'password' => $this->featureContext->getPasswordForUser($user), + ] + ); + $headers = ['Content-Type' => 'application/json']; + $url = $this->featureContext->getBaseUrl() . '/token/generate'; + $this->featureContext->setResponse( + HttpRequestHelper::post( + $url, + $this->featureContext->getStepLineRef(), + null, + null, + $headers, + $body + ) + ); + $this->featureContext->theHTTPStatusCodeShouldBe("200"); + $this->clientToken + = \json_decode($this->featureContext->getResponse()->getBody()->getContents())->token; + } + + /** + * @When the administrator generates a new client token using the token API + * @Given a new client token for the administrator has been generated + * + * @return void + */ + public function aNewClientTokenForTheAdministratorHasBeenGenerated():void { + $admin = $this->featureContext->getAdminUsername(); + $this->aNewClientTokenHasBeenGenerated($admin); + } + + /** + * @When user :user requests :url with :method using basic auth + * + * @param string $user + * @param string $url + * @param string $method + * @param string|null $password + * @param string|null $body + * @param array|null $header + * + * @return void + */ + public function userRequestsURLWithUsingBasicAuth( + string $user, + string $url, + string $method, + ?string $password = null, + ?string $body = null, + ?array $header = null + ):void { + $userRenamed = $this->featureContext->getActualUsername($user); + $url = $this->featureContext->substituteInLineCodes( + $url, + $userRenamed + ); + if ($password === null) { + $authString = "$userRenamed:" . $this->featureContext->getPasswordForUser($user); + } else { + $authString = $userRenamed . ":" . $this->featureContext->getActualPassword($password); + } + $this->sendRequest( + $url, + $method, + 'basic ' . \base64_encode($authString), + false, + $body, + $header + ); + } + + /** + * @When user :user requests :url with :method using basic auth and with headers + * + * @param string $user + * @param string $url + * @param string $method + * @param TableNode $headersTable + * + * @return void + * @throws Exception + */ + public function userRequestsURLWithUsingBasicAuthAndDepthHeader(string $user, string $url, string $method, TableNode $headersTable):void { + $user = $this->featureContext->getActualUsername($user); + $authString = "$user:" . $this->featureContext->getPasswordForUser($user); + $url = $this->featureContext->substituteInLineCodes( + $url, + $user + ); + $this->featureContext->verifyTableNodeColumns( + $headersTable, + ['header', 'value'] + ); + $headers = []; + foreach ($headersTable as $row) { + $headers[$row['header']] = $row ['value']; + } + $this->sendRequest( + $url, + $method, + 'basic ' . \base64_encode($authString), + false, + null, + $headers + ); + } + + /** + * @Given user :user has requested :url with :method using basic auth + * + * @param string $user + * @param string $url + * @param string $method + * @param string|null $password + * @param string|null $body + * + * @return void + */ + public function userHasRequestedURLWithUsingBasicAuth( + string $user, + string $url, + string $method, + ?string $password = null, + ?string $body = null + ):void { + $this->userRequestsURLWithUsingBasicAuth( + $user, + $url, + $method, + $password, + $body + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the administrator requests :url with :method using basic auth + * + * @param string $url + * @param string $method + * @param string|null $password + * + * @return void + */ + public function administratorRequestsURLWithUsingBasicAuth(string $url, string $method, ?string $password = null):void { + $this->userRequestsURLWithUsingBasicAuth( + $this->featureContext->getAdminUsername(), + $url, + $method, + $password + ); + } + + /** + * @When user :user requests :url with :method using basic token auth + * + * @param string $user + * @param string $url + * @param string $method + * + * @return void + */ + public function userRequestsURLWithUsingBasicTokenAuth(string $user, string $url, string $method):void { + $user = $this->featureContext->getActualUsername($user); + $this->sendRequest( + $url, + $method, + 'basic ' . \base64_encode("$user:" . $this->clientToken) + ); + } + + /** + * @Given user :user has requested :url with :method using basic token auth + * + * @param string $user + * @param string $url + * @param string $method + * + * @return void + */ + public function userHasRequestedURLWithUsingBasicTokenAuth(string $user, string $url, string $method):void { + $this->userRequestsURLWithUsingBasicTokenAuth($user, $url, $method); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the user requests :url with :method using the generated client token + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userRequestsURLWithUsingAClientToken(string $url, string $method):void { + $this->sendRequest($url, $method, 'token ' . $this->clientToken); + } + + /** + * @Given the user has requested :url with :method using the generated client token + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userHasRequestedURLWithUsingAClientToken(string $url, string $method):void { + $this->userRequestsURLWithUsingAClientToken($url, $method); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the user requests :url with :method using the generated app password + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userRequestsURLWithUsingAppPassword(string $url, string $method):void { + $this->sendRequest($url, $method, 'token ' . $this->appToken); + } + + /** + * @When the user requests :url with :method using app password named :tokenName + * + * @param string $url + * @param string $method + * @param string $tokenName + * + * @return void + */ + public function theUserRequestsWithUsingAppPasswordNamed(string $url, string $method, string $tokenName):void { + $this->sendRequest($url, $method, 'token ' . $this->appTokens[$tokenName]['token']); + } + + /** + * @Given the user has requested :url with :method using the generated app password + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userHasRequestedURLWithUsingAppPassword(string $url, string $method):void { + $this->userRequestsURLWithUsingAppPassword($url, $method); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the user requests :url with :method using the browser session + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userRequestsURLWithBrowserSession(string $url, string $method):void { + $this->sendRequest($url, $method, null, true); + } + + /** + * @Given the user has requested :url with :method using the browser session + * + * @param string $url + * @param string $method + * + * @return void + */ + public function userHasRequestedURLWithBrowserSession(string $url, string $method):void { + $this->userRequestsURLWithBrowserSession($url, $method); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Given a new browser session for :user has been started + * + * @param string $user + * + * @return void + */ + public function aNewBrowserSessionForHasBeenStarted(string $user):void { + $user = $this->featureContext->getActualUsername($user); + $loginUrl = $this->featureContext->getBaseUrl() . '/index.php/login'; + // Request a new session and extract CSRF token + $this->featureContext->setResponse( + HttpRequestHelper::get( + $loginUrl, + $this->featureContext->getStepLineRef(), + null, + null, + null, + null, + null, + $this->featureContext->getCookieJar() + ) + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + $this->featureContext->extractRequestTokenFromResponse($this->featureContext->getResponse()); + + // Login and extract new token + $body = [ + 'user' => $this->featureContext->getActualUsername($user), + 'password' => $this->featureContext->getPasswordForUser($user), + 'requesttoken' => $this->featureContext->getRequestToken() + ]; + $this->featureContext->setResponse( + HttpRequestHelper::post( + $loginUrl, + $this->featureContext->getStepLineRef(), + null, + null, + null, + $body, + null, + $this->featureContext->getCookieJar() + ) + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + $this->featureContext->extractRequestTokenFromResponse($this->featureContext->getResponse()); + } + + /** + * @Given a new browser session for the administrator has been started + * + * @return void + */ + public function aNewBrowserSessionForTheAdministratorHasBeenStarted():void { + $admin = $this->featureContext->getAdminUsername(); + $this->aNewBrowserSessionForHasBeenStarted($admin); + } + + /** + * @When /^the administrator (enforces|does not enforce)\s?token auth$/ + * @Given /^token auth has (not|)\s?been enforced$/ + * + * @param string $hasOrNot + * + * @return void + * @throws Exception + */ + public function tokenAuthHasBeenEnforced(string $hasOrNot):void { + $enforce = (($hasOrNot !== "not") && ($hasOrNot !== "does not enforce")); + if ($enforce) { + $value = 'true'; + } else { + $value = 'false'; + } + $occStatus = SetupHelper::setSystemConfig( + 'token_auth_enforced', + $value, + $this->featureContext->getStepLineRef(), + 'boolean' + ); + if ($occStatus['code'] !== "0") { + throw new \Exception("setSystemConfig token_auth_enforced returned error code " . $occStatus['code']); + } + + // Remember that we set this value, so it can be removed after the scenario + $this->tokenAuthHasBeenSet = true; + $this->tokenAuthHasBeenSetTo = $value; + } + + /** + * + * @return string + */ + public function generateAuthTokenForAdmin():string { + $this->aNewBrowserSessionForHasBeenStarted($this->featureContext->getAdminUsername()); + $this->userGeneratesNewAppPasswordNamed('acceptance-test ' . \microtime()); + return $this->appToken; + } + + /** + * delete token_auth_enforced if it was set in the scenario + * + * @AfterScenario + * + * @return void + * @throws Exception + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteTokenAuthEnforcedAfterScenario():void { + if ($this->tokenAuthHasBeenSet) { + if ($this->tokenAuthHasBeenSetTo === 'true') { + // Because token auth is enforced, we have to use a token + // (app password) as the password to send to the testing app + // so it will authenticate us. + $appTokenForOccCommand = $this->generateAuthTokenForAdmin(); + } else { + $appTokenForOccCommand = null; + } + SetupHelper::deleteSystemConfig( + 'token_auth_enforced', + $this->featureContext->getStepLineRef(), + null, + $appTokenForOccCommand, + null, + null + ); + $this->tokenAuthHasBeenSet = false; + $this->tokenAuthHasBeenSetTo = ''; + } + } +} diff --git a/tests/acceptance/features/bootstrap/CapabilitiesContext.php b/tests/acceptance/features/bootstrap/CapabilitiesContext.php new file mode 100644 index 000000000..bfdb36995 --- /dev/null +++ b/tests/acceptance/features/bootstrap/CapabilitiesContext.php @@ -0,0 +1,223 @@ + + * @author Sergio Bertolin + * @author Phillip Davis + * @copyright Copyright (c) 2018, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +require_once 'bootstrap.php'; + +/** + * Capabilities context. + */ +class CapabilitiesContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * @Then the capabilities should contain + * + * @param TableNode|null $formData + * + * @return void + * @throws Exception + */ + public function checkCapabilitiesResponse(TableNode $formData):void { + $capabilitiesXML = $this->featureContext->appConfigurationContext->getCapabilitiesXml(__METHOD__); + $assertedSomething = false; + + $this->featureContext->verifyTableNodeColumns($formData, ['value', 'path_to_element', 'capability']); + + foreach ($formData->getHash() as $row) { + $row['value'] = $this->featureContext->substituteInLineCodes($row['value']); + Assert::assertEquals( + $row['value'] === "EMPTY" ? '' : $row['value'], + $this->featureContext->appConfigurationContext->getParameterValueFromXml( + $capabilitiesXML, + $row['capability'], + $row['path_to_element'] + ), + "Failed field {$row['capability']} {$row['path_to_element']}" + ); + $assertedSomething = true; + } + + Assert::assertTrue( + $assertedSomething, + 'there was nothing in the table of expected capabilities' + ); + } + + /** + * @Then the version data in the response should contain + * + * @param TableNode|null $formData + * + * @return void + * @throws Exception + */ + public function checkVersionResponse(TableNode $formData):void { + $versionXML = $this->featureContext->appConfigurationContext->getVersionXml(__METHOD__); + $assertedSomething = false; + + $this->featureContext->verifyTableNodeColumns($formData, ['name', 'value']); + + foreach ($formData->getHash() as $row) { + $row['value'] = $this->featureContext->substituteInLineCodes($row['value']); + $actualValue = $versionXML->{$row['name']}; + + Assert::assertEquals( + $row['value'] === "EMPTY" ? '' : $row['value'], + $actualValue, + "Failed field {$row['name']}" + ); + $assertedSomething = true; + } + + Assert::assertTrue( + $assertedSomething, + 'there was nothing in the table of expected version data' + ); + } + + /** + * @Then the major-minor-micro version data in the response should match the version string + * + * @return void + * @throws Exception + */ + public function checkVersionMajorMinorMicroResponse():void { + $versionXML = $this->featureContext->appConfigurationContext->getVersionXml(__METHOD__); + $versionString = (string) $versionXML->string; + // We expect that versionString will be in a format like "10.9.2 beta" or "10.9.2-alpha" or "10.9.2" + $result = \preg_match('/^[0-9]+\.[0-9]+\.[0-9]+/', $versionString, $matches); + Assert::assertSame( + 1, + $result, + __METHOD__ . " version string '$versionString' does not start with a semver version" + ); + // semVerParts should have an array with the 3 semver components of the version, e.g. "1", "9" and "2". + $semVerParts = \explode('.', $matches[0]); + $expectedMajor = $semVerParts[0]; + $expectedMinor = $semVerParts[1]; + $expectedMicro = $semVerParts[2]; + $actualMajor = (string) $versionXML->major; + $actualMinor = (string) $versionXML->minor; + $actualMicro = (string) $versionXML->micro; + Assert::assertSame( + $expectedMajor, + $actualMajor, + __METHOD__ . "'major' data item does not match with major version in string '$versionString'" + ); + Assert::assertSame( + $expectedMinor, + $actualMinor, + __METHOD__ . "'minor' data item does not match with minor version in string '$versionString'" + ); + Assert::assertSame( + $expectedMicro, + $actualMicro, + __METHOD__ . "'micro' data item does not match with micro (patch) version in string '$versionString'" + ); + } + + /** + * @Then the :pathToElement capability of files sharing app should be :value + * + * @param string $pathToElement + * @param string $value + * + * @return void + * @throws Exception + */ + public function theCapabilityOfFilesSharingAppShouldBe( + string $pathToElement, + string $value + ):void { + $this->featureContext->appConfigurationContext->userGetsCapabilitiesCheckResponse( + $this->featureContext->getCurrentUser() + ); + $capabilitiesXML = $this->featureContext->appConfigurationContext->getCapabilitiesXml(__METHOD__); + $actualValue = $this->featureContext->appConfigurationContext->getParameterValueFromXml( + $capabilitiesXML, + "files_sharing", + $pathToElement + ); + Assert::assertEquals( + $value === "EMPTY" ? '' : $value, + $actualValue, + "Expected {$pathToElement} capability of files sharing app to be {$value}, but got {$actualValue}" + ); + } + + /** + * @Then the capabilities should not contain + * + * @param TableNode|null $formData + * + * @return void + */ + public function theCapabilitiesShouldNotContain(TableNode $formData):void { + $capabilitiesXML = $this->featureContext->appConfigurationContext->getCapabilitiesXml(__METHOD__); + $assertedSomething = false; + + foreach ($formData->getHash() as $row) { + Assert::assertFalse( + $this->featureContext->appConfigurationContext->parameterValueExistsInXml( + $capabilitiesXML, + $row['capability'], + $row['path_to_element'] + ), + "Capability {$row['capability']} {$row['path_to_element']} exists but it should not exist" + ); + $assertedSomething = true; + } + + Assert::assertTrue( + $assertedSomething, + 'there was nothing in the table of not expected capabilities' + ); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/ChecksumContext.php b/tests/acceptance/features/bootstrap/ChecksumContext.php new file mode 100644 index 000000000..cc7d52827 --- /dev/null +++ b/tests/acceptance/features/bootstrap/ChecksumContext.php @@ -0,0 +1,464 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @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 + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use PHPUnit\Framework\Assert; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * Checksum functions + */ +class ChecksumContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * @When user :user uploads file :source to :destination with checksum :checksum using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * @param string $checksum + * + * @return void + */ + public function userUploadsFileToWithChecksumUsingTheAPI( + string $user, + string $source, + string $destination, + string $checksum + ):void { + $file = \file_get_contents( + $this->featureContext->acceptanceTestsDirLocation() . $source + ); + $response = $this->featureContext->makeDavRequest( + $user, + 'PUT', + $destination, + ['OC-Checksum' => $checksum], + $file, + "files" + ); + $this->featureContext->setResponse($response); + } + + /** + * @Given user :user has uploaded file :source to :destination with checksum :checksum + * + * @param string $user + * @param string $source + * @param string $destination + * @param string $checksum + * + * @return void + */ + public function userHasUploadedFileToWithChecksumUsingTheAPI( + string $user, + string $source, + string $destination, + string $checksum + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->userUploadsFileToWithChecksumUsingTheAPI( + $user, + $source, + $destination, + $checksum + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When user :user uploads file with content :content and checksum :checksum to :destination using the WebDAV API + * + * @param string $user + * @param string $content + * @param string $checksum + * @param string $destination + * + * @return void + */ + public function userUploadsFileWithContentAndChecksumToUsingTheAPI( + string $user, + string $content, + string $checksum, + string $destination + ):void { + $response = $this->featureContext->makeDavRequest( + $user, + 'PUT', + $destination, + ['OC-Checksum' => $checksum], + $content, + "files" + ); + $this->featureContext->setResponse($response); + } + + /** + * @Given user :user has uploaded file with content :content and checksum :checksum to :destination + * + * @param string $user + * @param string $content + * @param string $checksum + * @param string $destination + * + * @return void + */ + public function userHasUploadedFileWithContentAndChecksumToUsingTheAPI( + string $user, + string $content, + string $checksum, + string $destination + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->userUploadsFileWithContentAndChecksumToUsingTheAPI( + $user, + $content, + $checksum, + $destination + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When user :user requests the checksum of :path via propfind + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userRequestsTheChecksumOfViaPropfind(string $user, string $path):void { + $user = $this->featureContext->getActualUsername($user); + $body = ' + + + + + '; + $password = $this->featureContext->getPasswordForUser($user); + $response = WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + $user, + $password, + 'PROPFIND', + $path, + null, + $this->featureContext->getStepLineRef(), + $body, + $this->featureContext->getDavPathVersion() + ); + $this->featureContext->setResponse($response); + } + + /** + * @Then the webdav checksum should match :expectedChecksum + * + * @param string $expectedChecksum + * + * @return void + * @throws Exception + */ + public function theWebdavChecksumShouldMatch(string $expectedChecksum):void { + $service = new Sabre\Xml\Service(); + $bodyContents = $this->featureContext->getResponse()->getBody()->getContents(); + $parsed = $service->parse($bodyContents); + + /* + * Fetch the checksum array + * The checksums are way down in the array: + * $checksums = $parsed[0]['value'][1]['value'][0]['value'][0]; + * And inside is the actual checksum string: + * $checksums['value'][0]['value'] + * The Asserts below check the existence of the expected key at every level + * of the nested array. This helps to see what happened if a test fails + * because the response structure is not as expected. + */ + + Assert::assertIsArray( + $parsed, + __METHOD__ . " could not parse response as XML. Expected parsed XML to be an array but found " . $bodyContents + ); + Assert::assertArrayHasKey( + 0, + $parsed, + __METHOD__ . " parsed XML does not have key 0" + ); + $parsed0 = $parsed[0]; + Assert::assertArrayHasKey( + 'value', + $parsed0, + __METHOD__ . " parsed XML parsed0 does not have key value" + ); + $parsed0Value = $parsed0['value']; + Assert::assertArrayHasKey( + 1, + $parsed0Value, + __METHOD__ . " parsed XML parsed0Value does not have key 1" + ); + $parsed0Value1 = $parsed0Value[1]; + Assert::assertArrayHasKey( + 'value', + $parsed0Value1, + __METHOD__ . " parsed XML parsed0Value1 does not have key value after key 1" + ); + $parsed0Value1Value = $parsed0Value1['value']; + Assert::assertArrayHasKey( + 0, + $parsed0Value1Value, + __METHOD__ . " parsed XML parsed0Value1Value does not have key 0" + ); + $parsed0Value1Value0 = $parsed0Value1Value[0]; + Assert::assertArrayHasKey( + 'value', + $parsed0Value1Value0, + __METHOD__ . " parsed XML parsed0Value1Value0 does not have key value" + ); + $parsed0Value1Value0Value = $parsed0Value1Value0['value']; + Assert::assertArrayHasKey( + 0, + $parsed0Value1Value0Value, + __METHOD__ . " parsed XML parsed0Value1Value0Value does not have key 0" + ); + $checksums = $parsed0Value1Value0Value[0]; + Assert::assertArrayHasKey( + 'value', + $checksums, + __METHOD__ . " parsed XML checksums does not have key value" + ); + $checksumsValue = $checksums['value']; + Assert::assertArrayHasKey( + 0, + $checksumsValue, + __METHOD__ . " parsed XML checksumsValue does not have key 0" + ); + $checksumsValue0 = $checksumsValue[0]; + Assert::assertArrayHasKey( + 'value', + $checksumsValue0, + __METHOD__ . " parsed XML checksumsValue0 does not have key value" + ); + $actualChecksum = $checksumsValue0['value']; + Assert::assertEquals( + $expectedChecksum, + $actualChecksum, + "Expected: webDav checksum should be {$expectedChecksum} but got {$actualChecksum}" + ); + } + + /** + * @Then as user :user the webdav checksum of :path via propfind should match :expectedChecksum + * + * @param string $user + * @param string $path + * @param string $expectedChecksum + * + * @return void + * @throws Exception + */ + public function theWebdavChecksumOfViaPropfindShouldMatch(string $user, string $path, string $expectedChecksum):void { + $user = $this->featureContext->getActualUsername($user); + $this->userRequestsTheChecksumOfViaPropfind($user, $path); + $this->theWebdavChecksumShouldMatch($expectedChecksum); + } + + /** + * @Then the header checksum should match :expectedChecksum + * + * @param string $expectedChecksum + * + * @return void + * @throws Exception + */ + public function theHeaderChecksumShouldMatch(string $expectedChecksum):void { + $headerChecksums + = $this->featureContext->getResponse()->getHeader('OC-Checksum'); + + Assert::assertIsArray( + $headerChecksums, + __METHOD__ . " getHeader('OC-Checksum') did not return an array" + ); + + Assert::assertNotEmpty( + $headerChecksums, + __METHOD__ . " getHeader('OC-Checksum') returned an empty array. No checksum header was found." + ); + + $checksumCount = \count($headerChecksums); + + Assert::assertTrue( + $checksumCount === 1, + __METHOD__ . " Expected 1 checksum in the header but found $checksumCount checksums" + ); + + $headerChecksum + = $headerChecksums[0]; + Assert::assertEquals( + $expectedChecksum, + $headerChecksum, + "Expected: header checksum should match {$expectedChecksum} but got {$headerChecksum}" + ); + } + + /** + * @Then the header checksum when user :arg1 downloads file :arg2 using the WebDAV API should match :arg3 + * + * @param string $user + * @param string $fileName + * @param string $expectedChecksum + * + * @return void + * @throws Exception + */ + public function theHeaderChecksumWhenUserDownloadsFileUsingTheWebdavApiShouldMatch(string $user, string $fileName, string $expectedChecksum):void { + $this->featureContext->userDownloadsFileUsingTheAPI($user, $fileName); + $this->theHeaderChecksumShouldMatch($expectedChecksum); + } + + /** + * @Then the webdav checksum should be empty + * + * @return void + * @throws Exception + */ + public function theWebdavChecksumShouldBeEmpty():void { + $service = new Sabre\Xml\Service(); + $parsed = $service->parse( + $this->featureContext->getResponse()->getBody()->getContents() + ); + + /* + * Fetch the checksum array + * Maybe we want to do this a bit cleaner ;) + */ + $status = $parsed[0]['value'][1]['value'][1]['value']; + $expectedStatus = 'HTTP/1.1 404 Not Found'; + Assert::assertEquals( + $expectedStatus, + $status, + "Expected status to be {$expectedStatus} but got {$status}" + ); + } + + /** + * @Then the OC-Checksum header should not be there + * + * @return void + * @throws Exception + */ + public function theOcChecksumHeaderShouldNotBeThere():void { + $isHeader = $this->featureContext->getResponse()->hasHeader('OC-Checksum'); + Assert::assertFalse( + $isHeader, + "Expected no checksum header but got " + . $this->featureContext->getResponse()->getHeader('OC-Checksum') + ); + } + + /** + * @When user :user uploads chunk file :num of :total with :data to :destination with checksum :expectedChecksum using the WebDAV API + * + * @param string $user + * @param int $num + * @param int $total + * @param string $data + * @param string $destination + * @param string $expectedChecksum + * + * @return void + */ + public function userUploadsChunkFileOfWithToWithChecksum( + string $user, + int $num, + int $total, + string $data, + string $destination, + string $expectedChecksum + ):void { + $user = $this->featureContext->getActualUsername($user); + $num -= 1; + $file = "$destination-chunking-42-$total-$num"; + $response = $this->featureContext->makeDavRequest( + $user, + 'PUT', + $file, + ['OC-Checksum' => $expectedChecksum, 'OC-Chunked' => '1'], + $data, + "files" + ); + $this->featureContext->setResponse($response); + } + + /** + * @Given user :user has uploaded chunk file :num of :total with :data to :destination with checksum :expectedChecksum + * + * @param string $user + * @param int $num + * @param int $total + * @param string $data + * @param string $destination + * @param string $expectedChecksum + * + * @return void + */ + public function userHasUploadedChunkFileOfWithToWithChecksum( + string $user, + int $num, + int $total, + string $data, + string $destination, + string $expectedChecksum + ):void { + $this->userUploadsChunkFileOfWithToWithChecksum( + $user, + $num, + $total, + $data, + $destination, + $expectedChecksum + ); + $this->featureContext->theHTTPStatusCodeShouldBeOr(201, 206); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/FavoritesContext.php b/tests/acceptance/features/bootstrap/FavoritesContext.php new file mode 100644 index 000000000..1164dc26a --- /dev/null +++ b/tests/acceptance/features/bootstrap/FavoritesContext.php @@ -0,0 +1,361 @@ + + * @copyright Copyright (c) 2018 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use Psr\Http\Message\ResponseInterface; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * context containing favorites related API steps + */ +class FavoritesContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * + * @var WebDavPropertiesContext + */ + private $webDavPropertiesContext; + + /** + * @param string$user + * @param string $path + * + * @return void + */ + public function userFavoritesElement(string $user, string $path):void { + $response = $this->changeFavStateOfAnElement( + $user, + $path, + 1 + ); + $this->featureContext->setResponse($response); + } + + /** + * @When user :user favorites element :path using the WebDAV API + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userFavoritesElementUsingWebDavApi(string $user, string $path):void { + $this->userFavoritesElement($user, $path); + } + + /** + * @Given user :user has favorited element :path + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userHasFavoritedElementUsingWebDavApi(string $user, string $path):void { + $this->userFavoritesElement($user, $path); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the user favorites element :path using the WebDAV API + * + * @param string $path + * + * @return void + */ + public function theUserFavoritesElement(string $path):void { + $this->userFavoritesElement( + $this->featureContext->getCurrentUser(), + $path + ); + } + + /** + * @Given the user has favorited element :path + * + * @param string $path + * + * @return void + */ + public function theUserHasFavoritedElement(string $path):void { + $this->userFavoritesElement( + $this->featureContext->getCurrentUser(), + $path + ); + $this->featureContext->theHTTPStatusCodeShouldBe( + 207, + "Expected response status code to be 207 (Multi-status), but not found! " + ); + } + + /** + * @param $user + * @param $path + * + * @return void + */ + public function userUnfavoritesElement(string $user, string $path):void { + $response = $this->changeFavStateOfAnElement( + $user, + $path, + 0 + ); + $this->featureContext->setResponse($response); + } + + /** + * @When user :user unfavorites element :path using the WebDAV API + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userUnfavoritesElementUsingWebDavApi(string $user, string $path):void { + $this->userUnfavoritesElement($user, $path); + } + + /** + * @Given user :user has unfavorited element :path + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userHasUnfavoritedElementUsingWebDavApi(string $user, string $path):void { + $this->userUnfavoritesElement($user, $path); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^user "([^"]*)" should (not|)\s?have favorited the following elements$/ + * + * @param string $user + * @param string $shouldOrNot (not|) + * @param TableNode $expectedElements + * + * @return void + */ + public function checkFavoritedElements( + string $user, + string $shouldOrNot, + TableNode $expectedElements + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->userListsFavorites($user, null); + $this->featureContext->propfindResultShouldContainEntries( + $shouldOrNot, + $expectedElements, + $user + ); + } + + /** + * @When /^user "([^"]*)" lists the favorites and limits the result to ([\d*]) elements using the WebDAV API$/ + * + * @param string $user + * @param int|null $limit + * + * @return void + */ + public function userListsFavorites(string $user, ?int $limit = null):void { + $renamedUser = $this->featureContext->getActualUsername($user); + $baseUrl = $this->featureContext->getBaseUrl(); + $password = $this->featureContext->getPasswordForUser($user); + $body + = "\n" . + " \n" . + " \n" . + " 1\n"; + + if ($limit !== null) { + $body .= " \n" . + " $limit\n" . + " \n"; + } + + $body .= " "; + $response = WebDavHelper::makeDavRequest( + $baseUrl, + $renamedUser, + $password, + "REPORT", + "/", + null, + $this->featureContext->getStepLineRef(), + $body, + $this->featureContext->getDavPathVersion() + ); + $this->featureContext->setResponse($response); + } + + /** + * @param string $path + * + * @return void + */ + public function theUserUnfavoritesElement(string $path):void { + $this->userUnfavoritesElement( + $this->featureContext->getCurrentUser(), + $path + ); + } + + /** + * @When the user unfavorites element :path using the WebDAV API + * + * @param string $path + * + * @return void + */ + public function theUserUnfavoritesElementUsingWebDavApi(string $path):void { + $this->theUserUnfavoritesElement($path); + } + + /** + * @Given the user has unfavorited element :path + * + * @param string $path + * + * @return void + */ + public function theUserHasUnfavoritedElementUsingWebDavApi(string $path):void { + $this->theUserUnfavoritesElement($path); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should be favorited$/ + * + * @param string $user + * @param string $path + * @param integer $expectedValue 0|1 + * + * @return void + */ + public function asUserFileOrFolderShouldBeFavorited(string $user, string $path, int $expectedValue = 1):void { + $property = "oc:favorite"; + $this->webDavPropertiesContext->asUserFolderShouldContainAPropertyWithValue( + $user, + $path, + $property, + (string)$expectedValue + ); + } + + /** + * @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should not be favorited$/ + * + * @param string $user + * @param string $path + * + * @return void + */ + public function asUserFileShouldNotBeFavorited(string $user, string $path):void { + $this->asUserFileOrFolderShouldBeFavorited($user, $path, 0); + } + + /** + * @Then /^as the user (?:file|folder|entry) "([^"]*)" should be favorited$/ + * + * @param string $path + * @param integer $expectedValue 0|1 + * + * @return void + */ + public function asTheUserFileOrFolderShouldBeFavorited(string $path, int $expectedValue = 1):void { + $this->asUserFileOrFolderShouldBeFavorited( + $this->featureContext->getCurrentUser(), + $path, + $expectedValue + ); + } + + /** + * @Then /^as the user (?:file|folder|entry) "([^"]*)" should not be favorited$/ + * + * @param string $path + * + * @return void + */ + public function asTheUserFileOrFolderShouldNotBeFavorited(string $path):void { + $this->asTheUserFileOrFolderShouldBeFavorited($path, 0); + } + + /** + * Set the elements of a proppatch + * + * @param string $user + * @param string $path + * @param int|null $favOrUnfav 1 = favorite, 0 = unfavorite + * + * @return ResponseInterface + */ + public function changeFavStateOfAnElement( + string $user, + string $path, + ?int $favOrUnfav + ):ResponseInterface { + $renamedUser = $this->featureContext->getActualUsername($user); + return WebDavHelper::proppatch( + $this->featureContext->getBaseUrl(), + $renamedUser, + $this->featureContext->getPasswordForUser($user), + $path, + 'favorite', + (string)$favOrUnfav, + $this->featureContext->getStepLineRef(), + "oc='http://owncloud.org/ns'", + $this->featureContext->getDavPathVersion() + ); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + $this->webDavPropertiesContext = $environment->getContext( + 'WebDavPropertiesContext' + ); + } +} diff --git a/tests/acceptance/features/bootstrap/FeatureContext.php b/tests/acceptance/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..b98986319 --- /dev/null +++ b/tests/acceptance/features/bootstrap/FeatureContext.php @@ -0,0 +1,4488 @@ + + * @author Phillip Davis + * @copyright Copyright (c) 2018, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Hook\Scope\BeforeStepScope; +use GuzzleHttp\Exception\GuzzleException; +use rdx\behatvars\BehatVariablesContext; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; +use Behat\Testwork\Hook\Scope\BeforeSuiteScope; +use GuzzleHttp\Cookie\CookieJar; +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\Assert; +use TestHelpers\AppConfigHelper; +use TestHelpers\OcsApiHelper; +use TestHelpers\SetupHelper; +use TestHelpers\HttpRequestHelper; +use TestHelpers\UploadHelper; +use TestHelpers\OcisHelper; +use Laminas\Ldap\Ldap; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * Features context. + */ +class FeatureContext extends BehatVariablesContext { + use Provisioning; + use Sharing; + use WebDav; + + /** + * @var int Unix timestamp seconds + */ + private $scenarioStartTime; + + /** + * @var string + */ + private $adminUsername = ''; + + /** + * @var string + */ + private $adminPassword = ''; + + /** + * @var string + */ + private $adminDisplayName = ''; + + /** + * @var string + */ + private $adminEmailAddress = ''; + + /** + * @var string + */ + private $originalAdminPassword = ''; + + /** + * An array of values of replacement values of user attributes. + * These are only referenced when creating a user. After that, the + * run-time values are maintained and referenced in the $createdUsers array. + * + * Key is the username, value is an array of user attributes + * + * @var array|null + */ + private $userReplacements = null; + + /** + * @var string + */ + private $regularUserPassword = ''; + + /** + * @var string + */ + private $alt1UserPassword = ''; + + /** + * @var string + */ + private $alt2UserPassword = ''; + + /** + * @var string + */ + private $alt3UserPassword = ''; + + /** + * @var string + */ + private $alt4UserPassword = ''; + + /** + * The password to use in tests that create a sub-admin user + * + * @var string + */ + private $subAdminPassword = ''; + + /** + * The password to use in tests that create another admin user + * + * @var string + */ + private $alternateAdminPassword = ''; + + /** + * The password to use in tests that create public link shares + * + * @var string + */ + private $publicLinkSharePassword = ''; + + /** + * @var string + */ + private $ocPath = ''; + + /** + * @var string location of the root folder of ownCloud on the local server under test + */ + private $localServerRoot = null; + + /** + * @var string + */ + private $currentUser = ''; + + /** + * @var string + */ + private $currentServer = ''; + + /** + * The base URL of the current server under test, + * without any terminating slash + * e.g. http://localhost:8080 + * + * @var string + */ + private $baseUrl = ''; + + /** + * The base URL of the local server under test, + * without any terminating slash + * e.g. http://localhost:8080 + * + * @var string + */ + private $localBaseUrl = ''; + + /** + * The base URL of the remote (federated) server under test, + * without any terminating slash + * e.g. http://localhost:8180 + * + * @var string + */ + private $remoteBaseUrl = ''; + + /** + * The suite name, feature name and scenario line number. + * Example: apiComments/createComments.feature:24 + * + * @var string + */ + private $scenarioString = ''; + + /** + * A full unique reference to the step that is currently executing. + * Example: apiComments/createComments.feature:24-28 + * That is line 28, in the scenario at line 24, in the createComments feature + * in the apiComments suite. + * + * @var string + */ + private $stepLineRef = ''; + + /** + * @var bool|null + */ + private $sendStepLineRef = null; + + /** + * + * + * @var boolean true if TEST_SERVER_FED_URL is defined + */ + private $federatedServerExists = false; + + /** + * @var int + */ + private $ocsApiVersion = 1; + + /** + * @var ResponseInterface + */ + private $response = null; + + /** + * @var string + */ + private $responseUser = ""; + + /** + * @var string + */ + private $responseBodyContent = null; + + /** + * @var array + */ + private $userResponseBodyContents = []; + + /** + * @var array + */ + public $emailRecipients = []; + + /** + * @var CookieJar + */ + private $cookieJar; + + /** + * @var string + */ + private $requestToken; + + /** + * @var array + */ + private $storageIds = []; + + /** + * @var array + */ + private $createdFiles = []; + + /** + * The local source IP address from which to initiate API actions. + * Defaults to system-selected address matching IP address family and scope. + * + * @var string|null + */ + private $sourceIpAddress = null; + + private $guzzleClientHeaders = []; + + /** + * + * @var OCSContext + */ + public $ocsContext; + + /** + * + * @var AuthContext + */ + public $authContext; + + /** + * + * @var GraphContext + */ + public $graphContext; + + /** + * + * @var AppConfigurationContext + */ + public $appConfigurationContext; + + /** + * @var array saved configuration of the system before test runs as reported + * by occ config:list + */ + private $savedConfigList = []; + + /** + * @var array + */ + private $initialTrustedServer; + + /** + * @var int return code of last command + */ + private $occLastCode; + /** + * @var string stdout of last command + */ + private $lastStdOut; + /** + * @var string stderr of last command + */ + private $lastStdErr; + /** + * The codes are stored as strings, even though they are numbers + * + * @var array last http status codes + */ + private $lastHttpStatusCodesArray = []; + /** + * @var array last ocs status codes + */ + private $lastOCSStatusCodesArray = []; + + /** + * @var bool + * + * this is set true for db conversion tests + */ + private $dbConversion = false; + + /** + * @param bool $value + * + * @return void + */ + public function setDbConversionState(bool $value): void { + $this->dbConversion = $value; + } + + /** + * @return bool + */ + public function isRunningForDbConversion(): bool { + return $this->dbConversion; + } + + /** + * @var string + */ + private $oCSelector; + + /** + * @param string $selector + * + * @return void + */ + public function setOCSelector(string $selector): void { + $this->oCSelector = $selector; + } + + /** + * @return string + */ + public function getOCSelector(): string { + return $this->oCSelector; + } + + /** + * @return void + */ + public function resetOccLastCode(): void { + $this->occLastCode = null; + } + + /** + * @param int $statusCode + * + * @return void + */ + public function setOccLastCode(?int $statusCode = null): void { + $this->occLastCode = $statusCode; + } + + /** + * @param string|null $httpStatusCode + * + * @return void + */ + public function pushToLastHttpStatusCodesArray(?string $httpStatusCode = null): void { + if ($httpStatusCode !== null) { + $this->lastHttpStatusCodesArray[] = $httpStatusCode; + } elseif ($this->getResponse()->getStatusCode() !== null) { + $this->lastHttpStatusCodesArray[] = (string)$this->getResponse()->getStatusCode(); + } + } + + /** + * @return void + */ + public function emptyLastHTTPStatusCodesArray(): void { + $this->lastHttpStatusCodesArray = []; + } + + /** + * @return void + */ + public function emptyLastOCSStatusCodesArray(): void { + $this->lastOCSStatusCodesArray = []; + } + + /** + * @return void + */ + public function clearStatusCodeArrays(): void { + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + } + + /** + * @param string $ocsStatusCode + * + * @return void + */ + public function pushToLastOcsCodesArray(string $ocsStatusCode): void { + array_push($this->lastOCSStatusCodesArray, $ocsStatusCode); + } + + /** + * Add HTTP and OCS status code of the last response to the respective status code array + * + * @return void + */ + public function pushToLastStatusCodesArrays(): void { + $this->pushToLastHttpStatusCodesArray( + (string)$this->getResponse()->getStatusCode() + ); + try { + $this->pushToLastOcsCodesArray( + $this->ocsContext->getOCSResponseStatusCode( + $this->getResponse() + ) + ); + } catch (Exception $exception) { + // if response couldn't be converted into xml then push "notset" to last ocs status codes array + $this->pushToLastOcsCodesArray("notset"); + } + } + + /** + * @param string $emailAddress + * + * @return void + */ + public function pushEmailRecipientAsMailBox(string $emailAddress): void { + $mailBox = explode("@", $emailAddress)[0]; + if (!\in_array($mailBox, $this->emailRecipients)) { + $this->emailRecipients[] = $mailBox; + } + } + + /* + * @var Ldap + */ + private $ldap; + /** + * @var string + */ + private $ldapBaseDN; + /** + * @var string + */ + private $ldapHost; + /** + * @var int + */ + private $ldapPort; + /** + * @var string + */ + private $ldapAdminUser; + /** + * @var string + */ + private $ldapAdminPassword = ""; + /** + * @var string + */ + private $ldapUsersOU; + /** + * @var string + */ + private $ldapGroupsOU; + /** + * @var string + */ + private $ldapGroupSchema; + /** + * @var bool + */ + private $skipImportLdif; + /** + * @var array + */ + private $toDeleteDNs = []; + private $ldapCreatedUsers = []; + private $ldapCreatedGroups = []; + private $toDeleteLdapConfigs = []; + private $oldLdapConfig = []; + + /** + * @return Ldap + */ + public function getLdap(): Ldap { + return $this->ldap; + } + + /** + * @param string $configId + * + * @return void + */ + public function setToDeleteLdapConfigs(string $configId): void { + $this->toDeleteLdapConfigs[] = $configId; + } + + /** + * @return array + */ + public function getToDeleteLdapConfigs(): array { + return $this->toDeleteLdapConfigs; + } + + /** + * @param string $setValue + * + * @return void + */ + public function setToDeleteDNs(string $setValue): void { + $this->toDeleteDNs[] = $setValue; + } + + /** + * @return string + */ + public function getLdapBaseDN(): string { + return $this->ldapBaseDN; + } + + /** + * @return string + */ + public function getLdapUsersOU(): string { + return $this->ldapUsersOU; + } + + /** + * @return string + */ + public function getLdapGroupsOU(): string { + return $this->ldapGroupsOU; + } + + /** + * @return array + */ + public function getOldLdapConfig(): array { + return $this->oldLdapConfig; + } + + /** + * @param string $configId + * @param string $configKey + * @param string $value + * + * @return void + */ + public function setOldLdapConfig(string $configId, string $configKey, string $value): void { + $this->oldLdapConfig[$configId][$configKey] = $value; + } + + /** + * @return string + */ + public function getLdapHost(): string { + return $this->ldapHost; + } + + /** + * @return string + */ + public function getLdapHostWithoutScheme(): string { + return $this->removeSchemeFromUrl($this->ldapHost); + } + + /** + * @return integer + */ + public function getLdapPort(): int { + return $this->ldapPort; + } + + /** + * @return bool + */ + public function isTestingWithLdap(): bool { + return (\getenv("TEST_WITH_LDAP") === "true"); + } + + /** + * @return bool + */ + public function sendScenarioLineReferencesInXRequestId(): ?bool { + if ($this->sendStepLineRef === null) { + $this->sendStepLineRef = (\getenv("SEND_SCENARIO_LINE_REFERENCES") === "true"); + } + return $this->sendStepLineRef; + } + + /** + * @return bool + */ + public function isTestingReplacingUsernames(): bool { + return (\getenv('REPLACE_USERNAMES') === "true"); + } + + /** + * @return array|null + */ + public function usersToBeReplaced(): ?array { + if (($this->userReplacements === null) && $this->isTestingReplacingUsernames()) { + $this->userReplacements = \json_decode( + \file_get_contents("./tests/acceptance/usernames.json"), + true + ); + // Loop through the user replacements, and make entries for the lower + // and upper case forms. This allows for steps that specifically + // want to test that usernames like "alice", "Alice" and "ALICE" all work. + // Such steps will make useful replacements for each form. + foreach ($this->userReplacements as $key => $value) { + $lowerKey = \strtolower($key); + if ($lowerKey !== $key) { + $this->userReplacements[$lowerKey] = $value; + $this->userReplacements[$lowerKey]['username'] = \strtolower( + $this->userReplacements[$lowerKey]['username'] + ); + } + $upperKey = \strtoupper($key); + if ($upperKey !== $key) { + $this->userReplacements[$upperKey] = $value; + $this->userReplacements[$upperKey]['username'] = \strtoupper( + $this->userReplacements[$upperKey]['username'] + ); + } + } + } + return $this->userReplacements; + } + + /** + * BasicStructure constructor. + * + * @param string $baseUrl + * @param string $adminUsername + * @param string $adminPassword + * @param string $regularUserPassword + * @param string $ocPath + * + */ + public function __construct( + string $baseUrl, + string $adminUsername, + string $adminPassword, + string $regularUserPassword, + string $ocPath + ) { + // Initialize your context here + $this->baseUrl = \rtrim($baseUrl, '/'); + $this->adminUsername = $adminUsername; + $this->adminPassword = $adminPassword; + $this->regularUserPassword = $regularUserPassword; + $this->localBaseUrl = $this->baseUrl; + $this->currentServer = 'LOCAL'; + $this->cookieJar = new CookieJar(); + $this->ocPath = $ocPath; + + // PARALLEL DEPLOYMENT: ownCloud selector + $this->oCSelector = "oc10"; + + // These passwords are referenced in tests and can be overridden by + // setting environment variables. + $this->alt1UserPassword = "1234"; + $this->alt2UserPassword = "AaBb2Cc3Dd4"; + $this->alt3UserPassword = "aVeryLongPassword42TheMeaningOfLife"; + $this->alt4UserPassword = "ThisIsThe4thAlternatePwd"; + $this->subAdminPassword = "IamAJuniorAdmin42"; + $this->alternateAdminPassword = "IHave99LotsOfPriv"; + $this->publicLinkSharePassword = "publicPwd1"; + + // in case of CI deployment we take the server url from the environment + $testServerUrl = \getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = \rtrim($testServerUrl, '/'); + $this->localBaseUrl = $this->baseUrl; + } + + // federated server url from the environment + $testRemoteServerUrl = \getenv('TEST_SERVER_FED_URL'); + if ($testRemoteServerUrl !== false) { + $this->remoteBaseUrl = \rtrim($testRemoteServerUrl, '/'); + $this->federatedServerExists = true; + } else { + $this->remoteBaseUrl = $this->localBaseUrl; + $this->federatedServerExists = false; + } + + // get the admin username from the environment (if defined) + $adminUsernameFromEnvironment = $this->getAdminUsernameFromEnvironment(); + if ($adminUsernameFromEnvironment !== false) { + $this->adminUsername = $adminUsernameFromEnvironment; + } + + // get the admin password from the environment (if defined) + $adminPasswordFromEnvironment = $this->getAdminPasswordFromEnvironment(); + if ($adminPasswordFromEnvironment !== false) { + $this->adminPassword = $adminPasswordFromEnvironment; + } + + // get the regular user password from the environment (if defined) + $regularUserPasswordFromEnvironment = $this->getRegularUserPasswordFromEnvironment(); + if ($regularUserPasswordFromEnvironment !== false) { + $this->regularUserPassword = $regularUserPasswordFromEnvironment; + } + + // get the alternate(1) user password from the environment (if defined) + $alt1UserPasswordFromEnvironment = $this->getAlt1UserPasswordFromEnvironment(); + if ($alt1UserPasswordFromEnvironment !== false) { + $this->alt1UserPassword = $alt1UserPasswordFromEnvironment; + } + + // get the alternate(2) user password from the environment (if defined) + $alt2UserPasswordFromEnvironment = $this->getAlt2UserPasswordFromEnvironment(); + if ($alt2UserPasswordFromEnvironment !== false) { + $this->alt2UserPassword = $alt2UserPasswordFromEnvironment; + } + + // get the alternate(3) user password from the environment (if defined) + $alt3UserPasswordFromEnvironment = $this->getAlt3UserPasswordFromEnvironment(); + if ($alt3UserPasswordFromEnvironment !== false) { + $this->alt3UserPassword = $alt3UserPasswordFromEnvironment; + } + + // get the alternate(4) user password from the environment (if defined) + $alt4UserPasswordFromEnvironment = $this->getAlt4UserPasswordFromEnvironment(); + if ($alt4UserPasswordFromEnvironment !== false) { + $this->alt4UserPassword = $alt4UserPasswordFromEnvironment; + } + + // get the sub-admin password from the environment (if defined) + $subAdminPasswordFromEnvironment = $this->getSubAdminPasswordFromEnvironment(); + if ($subAdminPasswordFromEnvironment !== false) { + $this->subAdminPassword = $subAdminPasswordFromEnvironment; + } + + // get the alternate admin password from the environment (if defined) + $alternateAdminPasswordFromEnvironment = $this->getAlternateAdminPasswordFromEnvironment(); + if ($alternateAdminPasswordFromEnvironment !== false) { + $this->alternateAdminPassword = $alternateAdminPasswordFromEnvironment; + } + + // get the public link share password from the environment (if defined) + $publicLinkSharePasswordFromEnvironment = $this->getPublicLinkSharePasswordFromEnvironment(); + if ($publicLinkSharePasswordFromEnvironment !== false) { + $this->publicLinkSharePassword = $publicLinkSharePasswordFromEnvironment; + } + $this->originalAdminPassword = $this->adminPassword; + } + + /** + * @param string $appTestCodeFullPath + * + * @return string the relative path from the core tests/acceptance dir + * to the equivalent dir in the app + */ + public function getPathFromCoreToAppAcceptanceTests( + string $appTestCodeFullPath + ): string { + // $appTestCodeFullPath is something like: + // '/somedir/anotherdir/core/apps/guests/tests/acceptance/features/bootstrap' + // and we want to know the 'apps/guests/tests/acceptance' part + + $path = \dirname($appTestCodeFullPath, 2); + $acceptanceDir = \basename($path); + $path = \dirname($path); + $testsDir = \basename($path); + $path = \dirname($path); + $appNameDir = \basename($path); + $path = \dirname($path); + // We specially are not sure about the name of the directory 'apps' + // Sometimes the app could be installed in some alternate apps directory + // like, for example, `apps-external`. So this really does need to be + // resolved here at run-time. + $appsDir = \basename($path); + // To get from core tests/acceptance we go up 2 levels then down through + // the above app dirs. + return "../../$appsDir/$appNameDir/$testsDir/$acceptanceDir"; + } + + /** + * Get the externally-defined admin username, if any + * + * @return string|false + */ + private static function getAdminUsernameFromEnvironment() { + return \getenv('ADMIN_USERNAME'); + } + + /** + * Get the externally-defined admin password, if any + * + * @return string|false + */ + private static function getAdminPasswordFromEnvironment() { + return \getenv('ADMIN_PASSWORD'); + } + + /** + * Get the externally-defined regular user password, if any + * + * @return string|false + */ + private static function getRegularUserPasswordFromEnvironment() { + return \getenv('REGULAR_USER_PASSWORD'); + } + + /** + * Get the externally-defined alternate(1) user password, if any + * + * @return string|false + */ + private static function getAlt1UserPasswordFromEnvironment() { + return \getenv('ALT1_USER_PASSWORD'); + } + + /** + * Get the externally-defined alternate(2) user password, if any + * + * @return string|false + */ + private static function getAlt2UserPasswordFromEnvironment() { + return \getenv('ALT2_USER_PASSWORD'); + } + + /** + * Get the externally-defined alternate(3) user password, if any + * + * @return string|false + */ + private static function getAlt3UserPasswordFromEnvironment() { + return \getenv('ALT3_USER_PASSWORD'); + } + + /** + * Get the externally-defined alternate(4) user password, if any + * + * @return string|false + */ + private static function getAlt4UserPasswordFromEnvironment() { + return \getenv('ALT4_USER_PASSWORD'); + } + + /** + * Get the externally-defined sub-admin password, if any + * + * @return string|false + */ + private static function getSubAdminPasswordFromEnvironment() { + return \getenv('SUB_ADMIN_PASSWORD'); + } + + /** + * Get the externally-defined alternate admin password, if any + * + * @return string|false + */ + private static function getAlternateAdminPasswordFromEnvironment() { + return \getenv('ALTERNATE_ADMIN_PASSWORD'); + } + + /** + * Get the externally-defined public link share password, if any + * + * @return string|false + */ + private static function getPublicLinkSharePasswordFromEnvironment() { + return \getenv('PUBLIC_LINK_SHARE_PASSWORD'); + } + + /** + * removes the scheme "http(s)://" (if any) from the front of a URL + * note: only needs to handle http or https + * + * @param string $url + * + * @return string + */ + public function removeSchemeFromUrl(string $url): string { + return \preg_replace( + "(^https?://)", + "", + $url + ); + } + + /** + * @return string + */ + public function getOcPath(): string { + return $this->ocPath; + } + + /** + * @return CookieJar + */ + public function getCookieJar(): CookieJar { + return $this->cookieJar; + } + + /** + * @return string + */ + public function getRequestToken(): string { + return $this->requestToken; + } + + /** + * returns the base URL (which is without a slash at the end) + * + * @return string + */ + public function getBaseUrl(): string { + return $this->baseUrl; + } + + /** + * returns the path of the base URL + * e.g. owncloud-core/10 if the baseUrl is http://localhost/owncloud-core/10 + * the path is without a slash at the end and without a slash at the beginning + * + * @return string + */ + public function getBasePath(): string { + $parsedUrl = \parse_url($this->getBaseUrl(), PHP_URL_PATH); + // If the server-under-test is at the "top" of the domain then parse_url returns null. + // For example, testing a server at http://localhost:8080 or http://example.com + if ($parsedUrl === null) { + $parsedUrl = ''; + } + return \ltrim($parsedUrl, "/"); + } + + /** + * returns the OCS path + * the path is without a slash at the end and without a slash at the beginning + * + * @param string $ocsApiVersion + * + * @return string + */ + public function getOCSPath(string $ocsApiVersion): string { + return \ltrim($this->getBasePath() . "/ocs/v$ocsApiVersion.php", "/"); + } + + /** + * returns the complete DAV path including the base path e.g. owncloud-core/remote.php/dav + * + * @return string + */ + public function getDAVPathIncludingBasePath(): string { + return \ltrim($this->getBasePath() . "/" . $this->getDavPath(), "/"); + } + + /** + * returns the base URL but without "http(s)://" in front of it + * + * @return string + */ + public function getBaseUrlWithoutScheme(): string { + return $this->removeSchemeFromUrl($this->getBaseUrl()); + } + + /** + * returns the local base URL (which is without a slash at the end) + * + * @return string + */ + public function getLocalBaseUrl(): string { + return $this->localBaseUrl; + } + + /** + * returns the local base URL but without "http(s)://" in front of it + * + * @return string + */ + public function getLocalBaseUrlWithoutScheme(): string { + return $this->removeSchemeFromUrl($this->getLocalBaseUrl()); + } + + /** + * returns the remote base URL (which is without a slash at the end) + * + * @return string + */ + public function getRemoteBaseUrl(): string { + return $this->remoteBaseUrl; + } + + /** + * returns the remote base URL but without "http(s)://" in front of it + * + * @return string + */ + public function getRemoteBaseUrlWithoutScheme(): string { + return $this->removeSchemeFromUrl($this->getRemoteBaseUrl()); + } + + /** + * returns the reference to the current line being executed. + * + * @return string + */ + public function getStepLineRef(): string { + if (!$this->sendStepLineRef) { + return ''; + } + + // If we are in BeforeScenario and possibly before any particular step + // is being executed, then stepLineRef might be empty. In that case + // return just the string for the scenario. + if ($this->stepLineRef === '') { + return $this->scenarioString; + } + return $this->stepLineRef; + } + + /** + * get the exit status of the last occ command + * app acceptance tests that have their own step code may need to process this + * + * @return int exit status code of the last occ command + */ + public function getExitStatusCodeOfOccCommand(): ?int { + return $this->occLastCode; + } + + /** + * get the normal output of the last occ command + * app acceptance tests that have their own step code may need to process this + * + * @return string normal output of the last occ command + */ + public function getStdOutOfOccCommand(): string { + return $this->lastStdOut; + } + + /** + * set the normal output of the last occ command + * + * @param string $stdOut + * + * @return void + */ + public function setStdOutOfOccCommand(string $stdOut): void { + $this->lastStdOut = $stdOut; + } + + /** + * get the error output of the last occ command + * app acceptance tests that have their own step code may need to process this + * + * @return string error output of the last occ command + */ + public function getStdErrOfOccCommand(): string { + return $this->lastStdErr; + } + + /** + * returns the base URL without any sub-path e.g. http://localhost:8080 + * of the base URL http://localhost:8080/owncloud + * + * @return string + */ + public function getBaseUrlWithoutPath(): string { + $parts = \parse_url($this->getBaseUrl()); + $url = $parts ["scheme"] . "://" . $parts["host"]; + if (isset($parts["port"])) { + $url = "$url:" . $parts["port"]; + } + return $url; + } + + /** + * @return int + */ + public function getOcsApiVersion(): int { + return $this->ocsApiVersion; + } + + /** + * @return string|null + */ + public function getSourceIpAddress(): ?string { + return $this->sourceIpAddress; + } + + /** + * @return array|null + */ + public function getStorageIds(): ?array { + return $this->storageIds; + } + + /** + * @param string $storageName + * + * @return integer + * @throws Exception + */ + public function getStorageId(string $storageName): int { + $storageIds = $this->getStorageIds(); + $storageId = \array_search($storageName, $storageIds); + Assert::assertNotFalse( + $storageId, + "Could not find storageId with storage name $storageName" + ); + return $storageId; + } + + /** + * @param string $storageName + * @param integer $storageId + * + * @return void + */ + public function addStorageId(string $storageName, int $storageId): void { + $this->storageIds[$storageId] = $storageName; + } + + /** + * @param integer $storageId + * + * @return void + */ + public function popStorageId(int $storageId): void { + unset($this->storageIds[$storageId]); + } + + /** + * @param string $sourceIpAddress + * + * @return void + */ + public function setSourceIpAddress(string $sourceIpAddress): void { + $this->sourceIpAddress = $sourceIpAddress; + } + + /** + * @return array + */ + public function getGuzzleClientHeaders(): array { + return $this->guzzleClientHeaders; + } + + /** + * @param array $guzzleClientHeaders ['X-Foo' => 'Bar'] + * + * @return void + */ + public function setGuzzleClientHeaders(array $guzzleClientHeaders): void { + $this->guzzleClientHeaders = $guzzleClientHeaders; + } + + /** + * @param array $guzzleClientHeaders ['X-Foo' => 'Bar'] + * + * @return void + */ + public function addGuzzleClientHeaders(array $guzzleClientHeaders): void { + $this->guzzleClientHeaders = \array_merge( + $this->guzzleClientHeaders, + $guzzleClientHeaders + ); + } + + /** + * @Given /^using OCS API version "([^"]*)"$/ + * + * @param string $version + * + * @return void + */ + public function usingOcsApiVersion(string $version): void { + $this->ocsApiVersion = (int)$version; + } + + /** + * @Given /^as user "([^"]*)"$/ + * + * @param string $user + * + * @return void + */ + public function asUser(string $user): void { + $this->currentUser = $this->getActualUsername($user); + } + + /** + * @Given as the administrator + * + * @return void + */ + public function asTheAdministrator(): void { + $this->currentUser = $this->getAdminUsername(); + } + + /** + * @return string + */ + public function getCurrentUser(): string { + return $this->currentUser; + } + + /** + * @param string $user + * + * @return void + */ + public function setCurrentUser(string $user): void { + $this->currentUser = $user; + } + + /** + * returns $this->response + * some steps use that private var to store the response for other steps + * + * @return ResponseInterface + */ + public function getResponse(): ?ResponseInterface { + return $this->response; + } + + /** + * let this class remember a response that was received elsewhere + * so that steps in this class can be used to examine the response + * + * @param ResponseInterface|null $response + * @param string $username of the user that received the response + * + * @return void + */ + public function setResponse( + ?ResponseInterface $response, + string $username = "" + ): void { + $this->response = $response; + //after a new response reset the response xml + $this->responseXml = []; + //after a new response reset the response xml object + $this->responseXmlObject = null; + // remember the user that received the response + $this->responseUser = $username; + } + + /** + * @return string + */ + public function getCurrentServer(): string { + return $this->currentServer; + } + + /** + * @Given /^using server "(LOCAL|REMOTE)"$/ + * + * @param string|null $server + * + * @return string Previous used server + */ + public function usingServer(?string $server): string { + $previousServer = $this->currentServer; + if ($server === 'LOCAL') { + $this->baseUrl = $this->localBaseUrl; + $this->currentServer = 'LOCAL'; + } else { + $this->baseUrl = $this->remoteBaseUrl; + $this->currentServer = 'REMOTE'; + } + return $previousServer; + } + + /** + * + * @return boolean + */ + public function federatedServerExists(): bool { + return $this->federatedServerExists; + } + + /** + * disable CSRF + * + * @return string the previous setting of csrf.disabled + * @throws Exception + */ + public function disableCSRF(): string { + return $this->setCSRFDotDisabled('true'); + } + + /** + * enable CSRF + * + * @return string the previous setting of csrf.disabled + * @throws Exception + */ + public function enableCSRF(): string { + return $this->setCSRFDotDisabled('false'); + } + + /** + * set csrf.disabled + * + * @param string $setting "true", "false" or "" to delete the setting + * + * @return string the previous setting of csrf.disabled + * @throws Exception + */ + public function setCSRFDotDisabled(string $setting): string { + $oldCSRFSetting = SetupHelper::getSystemConfigValue( + 'csrf.disabled', + $this->getStepLineRef() + ); + + if ($setting === "") { + SetupHelper::deleteSystemConfig( + 'csrf.disabled', + $this->getStepLineRef() + ); + } elseif (($setting === 'true') || ($setting === 'false')) { + SetupHelper::setSystemConfig( + 'csrf.disabled', + $setting, + $this->getStepLineRef(), + 'boolean' + ); + } else { + throw new \http\Exception\InvalidArgumentException( + 'setting must be "true", "false" or ""' + ); + } + return \trim($oldCSRFSetting); + } + + /** + * Parses the response as XML + * + * @param ResponseInterface|null $response + * @param string|null $exceptionText text to put at the front of exception messages + * + * @return SimpleXMLElement + * @throws Exception + */ + public function getResponseXml(?ResponseInterface $response = null, ?string $exceptionText = ''): SimpleXMLElement { + if ($response === null) { + $response = $this->response; + } + + if ($exceptionText === '') { + $exceptionText = __METHOD__; + } + return HttpRequestHelper::getResponseXml($response, $exceptionText); + } + + /** + * Parses the xml answer to get the requested key and sub-key + * + * @param ResponseInterface $response + * @param string $key1 + * @param string $key2 + * + * @return string + * @throws Exception + */ + public function getXMLKey1Key2Value(ResponseInterface $response, string $key1, string $key2): string { + return (string)$this->getResponseXml($response, __METHOD__)->$key1->$key2; + } + + /** + * Parses the xml answer to get the requested key sequence + * + * @param ResponseInterface $response + * @param string $key1 + * @param string $key2 + * @param string $key3 + * + * @return string + * @throws Exception + */ + public function getXMLKey1Key2Key3Value( + ResponseInterface $response, + string $key1, + string $key2, + string $key3 + ): string { + return (string)$this->getResponseXml($response, __METHOD__)->$key1->$key2->$key3; + } + + /** + * Parses the xml answer to get the requested attribute value + * + * @param ResponseInterface $response + * @param string $key1 + * @param string $key2 + * @param string $key3 + * @param string $attribute + * + * @return string + * @throws Exception + */ + public function getXMLKey1Key2Key3AttributeValue( + ResponseInterface $response, + string $key1, + string $key2, + string $key3, + string $attribute + ): string { + return (string)$this->getResponseXml($response, __METHOD__)->$key1->$key2->$key3->attributes()->$attribute; + } + + /** + * This function is needed to use a vertical fashion in the gherkin tables. + * + * @param array $arrayOfArrays + * + * @return array + */ + public function simplifyArray(array $arrayOfArrays): array { + $a = \array_map( + function ($subArray) { + return $subArray[0]; + }, + $arrayOfArrays + ); + return $a; + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to URL "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * + * @return void + */ + public function userSendsHTTPMethodToUrl(string $user, string $verb, string $url): void { + $user = $this->getActualUsername($user); + $this->sendingToWithDirectUrl($user, $verb, $url, null); + } + + /** + * @Given /^user "([^"]*)" has sent HTTP method "([^"]*)" to URL "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * + * @return void + */ + public function userHasSentHTTPMethodToUrl(string $user, string $verb, string $url): void { + $this->userSendsHTTPMethodToUrl($user, $verb, $url); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to URL "([^"]*)" with password "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param string $password + * + * @return void + */ + public function userSendsHTTPMethodToUrlWithPassword(string $user, string $verb, string $url, string $password): void { + $this->sendingToWithDirectUrl($user, $verb, $url, null, $password); + } + + /** + * @Given /^user "([^"]*)" has sent HTTP method "([^"]*)" to URL "([^"]*)" with password "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param string $password + * + * @return void + */ + public function userHasSentHTTPMethodToUrlWithPassword(string $user, string $verb, string $url, string $password): void { + $this->userSendsHTTPMethodToUrlWithPassword($user, $verb, $url, $password); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string|null $user + * @param string|null $verb + * @param string|null $url + * @param TableNode|null $body + * @param string|null $password + * + * @return void + * @throws GuzzleException + */ + public function sendingToWithDirectUrl(?string $user, ?string $verb, ?string $url, ?TableNode $body, ?string $password = null): void { + $fullUrl = $this->getBaseUrl() . $url; + + if ($password === null) { + $password = $this->getPasswordForUser($user); + } + + $headers = $this->guzzleClientHeaders; + + $config = null; + if ($this->sourceIpAddress !== null) { + $config = [ + 'curl' => [ + CURLOPT_INTERFACE => $this->sourceIpAddress + ] + ]; + } + + $cookies = null; + if (!empty($this->cookieJar->toArray())) { + $cookies = $this->cookieJar; + } + + $bodyRows = null; + if ($body instanceof TableNode) { + $bodyRows = $body->getRowsHash(); + } + + if (isset($this->requestToken)) { + $headers['requesttoken'] = $this->requestToken; + } + + $this->response = HttpRequestHelper::sendRequest( + $fullUrl, + $this->getStepLineRef(), + $verb, + $user, + $password, + $headers, + $bodyRows, + $config, + $cookies + ); + } + + /** + * @param string $url + * + * @return bool + */ + public function isAPublicLinkUrl(string $url): bool { + if (OcisHelper::isTestingOnReva()) { + $urlEnding = \ltrim($url, '/'); + } else { + if (\substr($url, 0, 4) !== "http") { + return false; + } + $urlEnding = \substr($url, \strlen($this->getBaseUrl() . '/')); + } + + if (OcisHelper::isTestingOnOcisOrReva()) { + $matchResult = \preg_match("%^(#/)?s/([a-zA-Z0-9]{15})$%", $urlEnding); + } else { + $matchResult = \preg_match("%^(index.php/)?s/([a-zA-Z0-9]{15})$%", $urlEnding); + } + + // preg_match returns (int) 1 for a match, we want to return a boolean. + if ($matchResult === 1) { + $isPublicLinkUrl = true; + } else { + $isPublicLinkUrl = false; + } + return $isPublicLinkUrl; + } + + /** + * Check that the status code in the saved response is the expected status + * code, or one of the expected status codes. + * + * @param int|int[]|string|string[] $expectedStatusCode + * @param string|null $message + * + * @return void + */ + public function theHTTPStatusCodeShouldBe($expectedStatusCode, ?string $message = ""): void { + $actualStatusCode = $this->response->getStatusCode(); + if (\is_array($expectedStatusCode)) { + if ($message === "") { + $message = "HTTP status code $actualStatusCode is not one of the expected values " . \implode(" or ", $expectedStatusCode); + } + + Assert::assertContainsEquals( + $actualStatusCode, + $expectedStatusCode, + $message + ); + } else { + if ($message === "") { + $message = "HTTP status code $actualStatusCode is not the expected value $expectedStatusCode"; + } + + Assert::assertEquals( + $expectedStatusCode, + $actualStatusCode, + $message + ); + } + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @Then /^the HTTP status code should be "([^"]*)"$/ + * + * @param int|string $statusCode + * + * @return void + */ + public function thenTheHTTPStatusCodeShouldBe($statusCode): void { + $this->theHTTPStatusCodeShouldBe($statusCode); + } + + /** + * @Then /^the HTTP status code should be "([^"]*)" or "([^"]*)"$/ + * + * @param int|string $statusCode1 + * @param int|string $statusCode2 + * + * @return void + */ + public function theHTTPStatusCodeShouldBeOr($statusCode1, $statusCode2): void { + $this->theHTTPStatusCodeShouldBe( + [$statusCode1, $statusCode2] + ); + } + + /** + * @Then /^the HTTP status code should be between "(\d+)" and "(\d+)"$/ + * + * @param int|string $minStatusCode + * @param int|string $maxStatusCode + * + * @return void + */ + public function theHTTPStatusCodeShouldBeBetween( + $minStatusCode, + $maxStatusCode + ): void { + $statusCode = $this->response->getStatusCode(); + $message = "The HTTP status code $statusCode is not between $minStatusCode and $maxStatusCode"; + Assert::assertGreaterThanOrEqual( + $minStatusCode, + $statusCode, + $message + ); + Assert::assertLessThanOrEqual( + $maxStatusCode, + $statusCode, + $message + ); + } + + /** + * @Then the HTTP status code should be success + * + * @return void + */ + public function theHTTPStatusCodeShouldBeSuccess(): void { + $this->theHTTPStatusCodeShouldBeBetween(200, 299); + } + + /** + * @Then the HTTP status code should be failure + * + * @return void + */ + public function theHTTPStatusCodeShouldBeFailure(): void { + $statusCode = $this->response->getStatusCode(); + $message = "The HTTP status code $statusCode is not greater than or equals to 400"; + Assert::assertGreaterThanOrEqual( + 400, + $statusCode, + $message + ); + } + + /** + * + * @return bool + */ + public function theHTTPStatusCodeWasSuccess(): bool { + $statusCode = $this->response->getStatusCode(); + return (($statusCode >= 200) && ($statusCode <= 299)); + } + + /** + * Check the text in an HTTP responseXml message + * + * @Then /^the HTTP response message should be "([^"]*)"$/ + * + * @param string $expectedMessage + * + * @return void + * @throws Exception + */ + public function theHttpResponseMessageShouldBe(string $expectedMessage): void { + $actualMessage = $this->responseXml['value'][1]['value']; + Assert::assertEquals( + $expectedMessage, + $actualMessage, + "Expected $expectedMessage HTTP response message but got $actualMessage" + ); + } + + /** + * Check the text in an HTTP reason phrase + * + * @Then /^the HTTP reason phrase should be "([^"]*)"$/ + * + * @param string $reasonPhrase + * + * @return void + */ + public function theHTTPReasonPhraseShouldBe(string $reasonPhrase): void { + Assert::assertEquals( + $reasonPhrase, + $this->getResponse()->getReasonPhrase(), + 'Unexpected HTTP reason phrase in response' + ); + } + + /** + * Check the text in an HTTP reason phrase + * Use this step form if the expected text contains double quotes, + * single quotes and other content that theHTTPReasonPhraseShouldBe() + * cannot handle. + * + * After the step, write the expected text in PyString form like: + * + * """ + * File "abc.txt" can't be shared due to reason "xyz" + * """ + * + * @Then /^the HTTP reason phrase should be:$/ + * + * @param PyStringNode $reasonPhrase + * + * @return void + */ + public function theHTTPReasonPhraseShouldBePyString( + PyStringNode $reasonPhrase + ): void { + Assert::assertEquals( + $reasonPhrase->getRaw(), + $this->getResponse()->getReasonPhrase(), + 'Unexpected HTTP reason phrase in response' + ); + } + + /** + * @Then /^the XML "([^"]*)" "([^"]*)" value should be "([^"]*)"$/ + * + * @param string $key1 + * @param string $key2 + * @param string $idText + * + * @return void + * @throws Exception + */ + public function theXMLKey1Key2ValueShouldBe(string $key1, string $key2, string $idText): void { + $actualValue = $this->getXMLKey1Key2Value($this->response, $key1, $key2); + Assert::assertEquals( + $idText, + $actualValue, + "Expected $idText but got " + . $actualValue + ); + } + + /** + * @Then /^the XML "([^"]*)" "([^"]*)" "([^"]*)" value should be "([^"]*)"$/ + * + * @param string $key1 + * @param string $key2 + * @param string $key3 + * @param string $idText + * + * @return void + * @throws Exception + */ + public function theXMLKey1Key2Key3ValueShouldBe( + string $key1, + string $key2, + string $key3, + string $idText + ) { + $actualValue = $this->getXMLKey1Key2Key3Value($this->response, $key1, $key2, $key3); + Assert::assertEquals( + $idText, + $actualValue, + "Expected $idText but got " + . $actualValue + ); + } + + /** + * @Then /^the XML "([^"]*)" "([^"]*)" "([^"]*)" "([^"]*)" attribute value should be a valid version string$/ + * + * @param string $key1 + * @param string $key2 + * @param string $key3 + * @param string $attribute + * + * @return void + * @throws Exception + */ + public function theXMLKey1Key2AttributeValueShouldBe( + string $key1, + string $key2, + string $key3, + string $attribute + ): void { + $value = $this->getXMLKey1Key2Key3AttributeValue( + $this->response, + $key1, + $key2, + $key3, + $attribute + ); + Assert::assertTrue( + \version_compare($value, '0.0.1') >= 0, + "attribute $attribute value $value is not a valid version string" + ); + } + + /** + * @param ResponseInterface $response + * + * @return void + */ + public function extractRequestTokenFromResponse(ResponseInterface $response): void { + $this->requestToken = \substr( + \preg_replace( + '/(.*)data-requesttoken="(.*)">(.*)/sm', + '\2', + $response->getBody()->getContents() + ), + 0, + 89 + ); + } + + /** + * @Given /^user "([^"]*)" has logged in to a web-style session$/ + * + * @param string $user + * + * @return void + * @throws GuzzleException + * @throws JsonException + */ + public function userHasLoggedInToAWebStyleSessionUsingTheAPI(string $user): void { + $user = $this->getActualUsername($user); + $loginUrl = $this->getBaseUrl() . '/login'; + // Request a new session and extract CSRF token + + $config = null; + if ($this->sourceIpAddress !== null) { + $config = [ + 'curl' => [ + CURLOPT_INTERFACE => $this->sourceIpAddress + ] + ]; + } + + $this->response = HttpRequestHelper::get( + $loginUrl, + $this->getStepLineRef(), + null, + null, + $this->guzzleClientHeaders, + null, + $config, + $this->cookieJar + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + $this->extractRequestTokenFromResponse($this->response); + + // Login and extract new token + $password = $this->getPasswordForUser($user); + $body = [ + 'user' => $user, + 'password' => $password, + 'requesttoken' => $this->requestToken + ]; + $this->response = HttpRequestHelper::post( + $loginUrl, + $this->getStepLineRef(), + null, + null, + $this->guzzleClientHeaders, + $body, + $config, + $this->cookieJar + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + $this->extractRequestTokenFromResponse($this->response); + } + + /** + * @When the client sends a :method to :url of user :user with requesttoken + * + * @param string $method + * @param string $url + * @param string $user + * + * @return void + * @throws GuzzleException + * @throws JsonException + */ + public function sendingAToWithRequesttoken( + string $method, + string $url, + string $user + ): void { + $headers = $this->guzzleClientHeaders; + + $config = null; + if ($this->sourceIpAddress !== null) { + $config = [ + 'curl' => [ + CURLOPT_INTERFACE => $this->sourceIpAddress + ] + ]; + } + + $headers['requesttoken'] = $this->requestToken; + + $user = \strtolower($this->getActualUsername($user)); + $url = $this->getBaseUrl() . $url; + $url = $this->substituteInLineCodes($url, $user); + $this->response = HttpRequestHelper::sendRequest( + $url, + $this->getStepLineRef(), + $method, + null, + null, + $headers, + null, + $config, + $this->cookieJar + ); + } + + /** + * @Given the client has sent a :method to :url of user :user with requesttoken + * + * @param string $method + * @param string $url + * @param string $user + * + * @return void + */ + public function theClientHasSentAToWithRequesttoken( + string $method, + string $url, + string $user + ): void { + $this->sendingAToWithRequesttoken($method, $url, $user); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the client sends a :method to :url of user :user without requesttoken + * + * @param string $method + * @param string $url + * @param string|null $user + * + * @return void + * @throws JsonException + */ + public function sendingAToWithoutRequesttoken(string $method, string $url, ?string $user = null): void { + $config = null; + if ($this->sourceIpAddress !== null) { + $config = [ + 'curl' => [ + CURLOPT_INTERFACE => $this->sourceIpAddress + ] + ]; + } + + $user = \strtolower($this->getActualUsername($user)); + $url = $this->getBaseUrl() . $url; + $url = $this->substituteInLineCodes($url, $user); + $this->response = HttpRequestHelper::sendRequest( + $url, + $this->getStepLineRef(), + $method, + null, + null, + $this->guzzleClientHeaders, + null, + $config, + $this->cookieJar + ); + } + + /** + * @Given the client has sent a :method to :url without requesttoken + * + * @param string $method + * @param string $url + * + * @return void + */ + public function theClientHasSentAToWithoutRequesttoken(string $method, string $url): void { + $this->sendingAToWithoutRequesttoken($method, $url); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $path + * @param string $filename + * + * @return void + */ + public static function removeFile(string $path, string $filename): void { + if (\file_exists("$path$filename")) { + \unlink("$path$filename"); + } + } + + /** + * Creates a file locally in the file system of the test runner + * The file will be available to upload to the server + * + * @param string $name + * @param string $size + * @param string $endData + * + * @return void + */ + public function createLocalFileOfSpecificSize(string $name, string $size, string $endData = 'a'): void { + $folder = $this->workStorageDirLocation(); + if (!\is_dir($folder)) { + \mkDir($folder); + } + $file = \fopen($folder . $name, 'w'); + \fseek($file, $size - \strlen($endData), SEEK_CUR); + \fwrite($file, $endData); // write the end data to force the file size + \fclose($file); + } + + /** + * Make a directory under the server root on the ownCloud server + * + * @param string $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo' + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function mkDirOnServer(string $dirPathFromServerRoot): void { + SetupHelper::mkDirOnServer( + $dirPathFromServerRoot, + $this->getStepLineRef(), + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + } + + /** + * @param string $filePathFromServerRoot + * @param string $content + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function createFileOnServerWithContent( + string $filePathFromServerRoot, + string $content + ): void { + SetupHelper::createFileOnServer( + $filePathFromServerRoot, + $content, + $this->getStepLineRef(), + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + } + + /** + * @Given file :filename with text :text has been created in local storage on the server + * + * @param string $filename + * @param string $text + * + * @return void + * @throws Exception + */ + public function fileHasBeenCreatedInLocalStorageWithText(string $filename, string $text): void { + $this->createFileOnServerWithContent( + LOCAL_STORAGE_DIR_ON_REMOTE_SERVER . "/$filename", + $text + ); + } + + /** + * @Given file :filename has been deleted from local storage on the server + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function fileHasBeenDeletedInLocalStorage(string $filename): void { + SetupHelper::deleteFileOnServer( + LOCAL_STORAGE_DIR_ON_REMOTE_SERVER . "/$filename", + $this->getStepLineRef(), + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + } + + /** + * @param string $user + * + * @return boolean + */ + public function isAdminUsername(string $user): bool { + return ($user === $this->getAdminUsername()); + } + + /** + * @return string + */ + public function getAdminUsername(): string { + return $this->adminUsername; + } + + /** + * @return string + */ + public function getAdminPassword(): string { + return $this->adminPassword; + } + + /** + * @param string $password + * + * @return void + */ + public function rememberNewAdminPassword(string $password): void { + $this->adminPassword = $password; + } + + /** + * @param string|null $userName + * + * @return string + */ + public function getPasswordForUser(?string $userName): string { + $userNameNormalized = $this->normalizeUsername($userName); + $username = $this->getActualUsername($userNameNormalized); + if ($username === $this->getAdminUsername()) { + return $this->getAdminPassword(); + } elseif (\array_key_exists($username, $this->createdUsers)) { + return (string)$this->createdUsers[$username]['password']; + } elseif (\array_key_exists($username, $this->createdRemoteUsers)) { + return (string)$this->createdRemoteUsers[$username]['password']; + } + + // The user has not been created yet, see if there is a replacement + // defined for the user. + $usernameReplacements = $this->usersToBeReplaced(); + if (isset($usernameReplacements)) { + if (isset($usernameReplacements[$userNameNormalized])) { + return $usernameReplacements[$userNameNormalized]['password']; + } + } + + // Fall back to the default password used for the well-known users. + if ($username === 'regularuser') { + return $this->regularUserPassword; + } elseif ($username === 'alice') { + return $this->regularUserPassword; + } elseif ($username === 'brian') { + return $this->alt1UserPassword; + } elseif ($username === 'carol') { + return $this->alt2UserPassword; + } elseif ($username === 'david') { + return $this->alt3UserPassword; + } elseif ($username === 'emily') { + return $this->alt4UserPassword; + } elseif ($username === 'usergrp') { + return $this->regularUserPassword; + } elseif ($username === 'sharee1') { + return $this->regularUserPassword; + } + + // The user has not been created yet and is not one of the pre-known + // users. So let the caller have the default password. + return (string)$this->getActualPassword($this->regularUserPassword); + } + + /** + * Get the display name of the user. + * + * For users that have already been created, return their display name. + * For special known usernames, return the display name that is also used by LDAP tests. + * For other users, return null. They will not be assigned any particular + * display name by this function. + * + * @param string $userName + * + * @return string|null + */ + public function getDisplayNameForUser(string $userName): ?string { + $userNameNormalized = $this->normalizeUsername($userName); + $username = $this->getActualUsername($userNameNormalized); + if (\array_key_exists($username, $this->createdUsers)) { + if (isset($this->createdUsers[$username]['displayname'])) { + return (string)$this->createdUsers[$username]['displayname']; + } + return $userName; + } + if (\array_key_exists($username, $this->createdRemoteUsers)) { + if (isset($this->createdRemoteUsers[$username]['displayname'])) { + return (string)$this->createdRemoteUsers[$username]['displayname']; + } + return $userName; + } + + // The user has not been created yet, see if there is a replacement + // defined for the user. + $usernameReplacements = $this->usersToBeReplaced(); + if (isset($usernameReplacements)) { + if (isset($usernameReplacements[$userNameNormalized])) { + return $usernameReplacements[$userNameNormalized]['displayname']; + } elseif (isset($usernameReplacements[$userName])) { + return $usernameReplacements[$userName]['displayname']; + } + } + + // Fall back to the default display name used for the well-known users. + if ($username === 'regularuser') { + return 'Regular User'; + } elseif ($username === 'alice') { + return 'Alice Hansen'; + } elseif ($username === 'brian') { + return 'Brian Murphy'; + } elseif ($username === 'carol') { + return 'Carol King'; + } elseif ($username === 'david') { + return 'David Lopez'; + } elseif ($username === 'emily') { + return 'Emily Wagner'; + } elseif ($username === 'usergrp') { + return 'User Grp'; + } elseif ($username === 'sharee1') { + return 'Sharee One'; + } elseif ($username === 'sharee2') { + return 'Sharee Two'; + } elseif (\in_array($username, ["grp1", "***redacted***"])) { + return $username; + } + return null; + } + + /** + * Get the email address of the user. + * + * For users that have already been created, return their email address. + * For special known usernames, return the email address that is also used by LDAP tests. + * For other users, return null. They will not be assigned any particular + * email address by this function. + * + * @param string $userName + * + * @return string|null + */ + public function getEmailAddressForUser(string $userName): ?string { + $userNameNormalized = $this->normalizeUsername($userName); + $username = $this->getActualUsername($userNameNormalized); + if (\array_key_exists($username, $this->createdUsers)) { + return (string)$this->createdUsers[$username]['email']; + } + if (\array_key_exists($username, $this->createdRemoteUsers)) { + return (string)$this->createdRemoteUsers[$username]['email']; + } + + // The user has not been created yet, see if there is a replacement + // defined for the user. + $usernameReplacements = $this->usersToBeReplaced(); + if (isset($usernameReplacements)) { + if (isset($usernameReplacements[$userNameNormalized])) { + return $usernameReplacements[$userNameNormalized]['email']; + } elseif (isset($usernameReplacements[$userName])) { + return $usernameReplacements[$userName]['email']; + } + } + + // Fall back to the default display name used for the well-known users. + if ($username === 'regularuser') { + return 'regularuser@example.org'; + } elseif ($username === 'alice') { + return 'alice@example.org'; + } elseif ($username === 'brian') { + return 'brian@example.org'; + } elseif ($username === 'carol') { + return 'carol@example.org'; + } elseif ($username === 'david') { + return 'david@example.org'; + } elseif ($username === 'emily') { + return 'emily@example.org'; + } elseif ($username === 'usergrp') { + return 'usergrp@example.org'; + } elseif ($username === 'sharee1') { + return 'sharee1@example.org'; + } else { + return null; + } + } + + // TODO do similar for other usernames for e.g. %regularuser% or %test-user-1% + + /** + * @param string|null $functionalUsername + * + * @return string|null + * @throws JsonException + */ + public function getActualUsername(?string $functionalUsername): ?string { + if ($functionalUsername === null) { + return null; + } + $usernames = $this->usersToBeReplaced(); + if (isset($usernames)) { + if (isset($usernames[$functionalUsername])) { + return $usernames[$functionalUsername]['username']; + } + $normalizedUsername = $this->normalizeUsername($functionalUsername); + if (isset($usernames[$normalizedUsername])) { + return $usernames[$normalizedUsername]['username']; + } + } + if ($functionalUsername === "%admin%") { + return $this->getAdminUsername(); + } + return $functionalUsername; + } + + /** + * @param string|null $functionalPassword + * + * @return string|null + */ + public function getActualPassword(?string $functionalPassword): ?string { + if ($functionalPassword === "%regular%") { + return $this->regularUserPassword; + } elseif ($functionalPassword === "%alt1%") { + return $this->alt1UserPassword; + } elseif ($functionalPassword === "%alt2%") { + return $this->alt2UserPassword; + } elseif ($functionalPassword === "%alt3%") { + return $this->alt3UserPassword; + } elseif ($functionalPassword === "%alt4%") { + return $this->alt4UserPassword; + } elseif ($functionalPassword === "%subadmin%") { + return $this->subAdminPassword; + } elseif ($functionalPassword === "%admin%") { + return $this->getAdminPassword(); + } elseif ($functionalPassword === "%altadmin%") { + return $this->alternateAdminPassword; + } elseif ($functionalPassword === "%public%") { + return $this->publicLinkSharePassword; + } elseif ($functionalPassword === "%remove%") { + return ""; + } else { + return $functionalPassword; + } + } + + /** + * @param string $userName + * + * @return array + */ + public function getAuthOptionForUser(string $userName): array { + return [$userName, $this->getPasswordForUser($userName)]; + } + + /** + * @return array + */ + public function getAuthOptionForAdmin(): array { + return $this->getAuthOptionForUser($this->getAdminUsername()); + } + + /** + * @When the administrator requests status.php + * + * @return void + */ + public function theAdministratorRequestsStatusPhp(): void { + $this->response = $this->getStatusPhp(); + } + + /** + * @When the administrator creates file :path with content :content in local storage using the testing API + * + * @param string $path + * @param string $content + * + * @return void + */ + public function theAdministratorCreatesFileUsingTheTestingApi(string $path, string $content): void { + $this->theAdministratorCreatesFileWithContentInLocalStorageUsingTheTestingApi( + $path, + $content, + 'local_storage' + ); + } + + /** + * @Given the administrator has created file :path with content :content in local storage using the testing API + * + * @param string $path + * @param string $content + * + * @return void + */ + public function theAdministratorHasCreatedFileUsingTheTestingApi(string $path, string $content): void { + $this->theAdministratorHasCreatedFileWithContentInLocalStorageUsingTheTestingApi( + $path, + $content, + 'local_storage' + ); + } + + /** + * @When the administrator creates file :path with content :content in local storage :mountPoint using the testing API + * + * @param string $path + * @param string $content + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdministratorCreatesFileWithContentInLocalStorageUsingTheTestingApi( + string $path, + string $content, + string $mountPoint + ): void { + $response = $this->copyContentToFileInTemporaryStorageOnSystemUnderTest( + "$mountPoint/$path", + $content + ); + $this->setResponse($response); + } + + /** + * @Given the administrator has created a file :path in temporary storage with the last exported content using the testing API + * + * @param string $path + * + * @return void + * @throws Exception + */ + public function theAdministratorHasCreatedAFileInTemporaryStorageWithLastExportedContent( + string $path + ): void { + $commandOutput = $this->getStdOutOfOccCommand(); + $this->copyContentToFileInTemporaryStorageOnSystemUnderTest($path, $commandOutput); + $this->theFileWithContentShouldExistInTheServerRoot( + TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/$path", + $commandOutput + ); + } + + /** + * @Given the administrator has created file :path with content :content in local storage :mountPoint + * + * @param string $path + * @param string $content + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdministratorHasCreatedFileWithContentInLocalStorageUsingTheTestingApi( + string $path, + string $content, + string $mountPoint + ): void { + $this->theAdministratorCreatesFileWithContentInLocalStorageUsingTheTestingApi( + $path, + $content, + $mountPoint + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * Copy a file from the test-runner to the temporary storage directory on + * the system-under-test. This uses the testing app to push the file into + * the backend of the server, where it can be seen by occ commands done in + * the server-under-test. + * + * @Given the administrator has copied file :localPath to :destination in temporary storage on the system under test + * + * @param string $localPath relative to the core "root" folder + * @param string $destination + * + * @return void + * @throws Exception + */ + public function theAdministratorHasCopiedFileToTemporaryStorageOnTheSystemUnderTest( + string $localPath, + string $destination + ): void { + // FeatureContext is in tests/acceptance/features/bootstrap so go up 4 + // levels to the test-runner root + $testRunnerRoot = \dirname(__DIR__, 4); + // The local path is specified down from the root - e.g. tests/data/file.txt + $content = \file_get_contents("$testRunnerRoot/$localPath"); + Assert::assertNotFalse( + $content, + "Local file $localPath cannot be read" + ); + $this->copyContentToFileInTemporaryStorageOnSystemUnderTest($destination, $content); + $this->theFileWithContentShouldExistInTheServerRoot(TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/$destination", $content); + } + + /** + * @param string $destination + * @param string $content + * + * @return ResponseInterface + * @throws Exception + */ + public function copyContentToFileInTemporaryStorageOnSystemUnderTest( + string $destination, + string $content + ): ResponseInterface { + $this->mkDirOnServer(TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER); + + return OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + 'POST', + "/apps/testing/api/v1/file", + $this->getStepLineRef(), + [ + 'file' => TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/$destination", + 'content' => $content + ], + $this->getOcsApiVersion() + ); + } + + /** + * @When the administrator deletes file :path in local storage using the testing API + * + * @param string $path + * + * @return void + */ + public function theAdministratorDeletesFileInLocalStorageUsingTheTestingApi(string $path): void { + $user = $this->getAdminUsername(); + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getAdminPassword(), + 'DELETE', + "/apps/testing/api/v1/file", + $this->getStepLineRef(), + ['file' => LOCAL_STORAGE_DIR_ON_REMOTE_SERVER . "/$path"], + $this->getOcsApiVersion() + ); + $this->setResponse($response); + } + + /** + * @Given a file with the size of :size bytes and the name :name has been created locally + * + * @param int $size if not int given it will be cast to int + * @param string $name + * + * @return void + * @throws InvalidArgumentException + */ + public function aFileWithSizeAndNameHasBeenCreatedLocally(int $size, string $name): void { + $fullPath = UploadHelper::getUploadFilesDir($name); + if (\file_exists($fullPath)) { + throw new InvalidArgumentException( + __METHOD__ . " could not create '$fullPath' file exists" + ); + } + UploadHelper::createFileSpecificSize($fullPath, $size); + $this->createdFiles[] = $fullPath; + } + + /** + * + * @return ResponseInterface + */ + public function getStatusPhp(): ResponseInterface { + $fullUrl = $this->getBaseUrl() . "/status.php"; + + $config = null; + if ($this->sourceIpAddress !== null) { + $config = [ + 'curl' => [ + CURLOPT_INTERFACE => $this->sourceIpAddress + ] + ]; + } + + return HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->guzzleClientHeaders, + null, + $config + ); + } + + /** + * @Then the json responded should match with + * + * @param PyStringNode $jsonExpected + * + * @return void + */ + public function jsonRespondedShouldMatch(PyStringNode $jsonExpected): void { + $jsonExpectedEncoded = \json_encode($jsonExpected->getRaw()); + $jsonRespondedEncoded = \json_encode((string)$this->response->getBody()); + Assert::assertEquals( + $jsonExpectedEncoded, + $jsonRespondedEncoded, + "The json responded: $jsonRespondedEncoded does not match with json expected: $jsonExpectedEncoded" + ); + } + + /** + * @Then the status.php response should include + * + * @param PyStringNode $jsonExpected + * + * @return void + * @throws Exception + */ + public function statusPhpRespondedShouldMatch(PyStringNode $jsonExpected): void { + $jsonExpectedDecoded = \json_decode($jsonExpected->getRaw(), true); + $jsonRespondedDecoded = $this->getJsonDecodedResponse(); + + $this->appConfigurationContext->theAdministratorGetsCapabilitiesCheckResponse(); + $edition = $this->appConfigurationContext->getParameterValueFromXml( + $this->appConfigurationContext->getCapabilitiesXml(__METHOD__), + 'core', + 'status@@@edition' + ); + + if (!\strlen($edition)) { + Assert::fail( + "Cannot get edition from core capabilities" + ); + } + + $product = $this->appConfigurationContext->getParameterValueFromXml( + $this->appConfigurationContext->getCapabilitiesXml(__METHOD__), + 'core', + 'status@@@product' + ); + if (!\strlen($product)) { + Assert::fail( + "Cannot get product from core capabilities" + ); + } + + $productName = $this->appConfigurationContext->getParameterValueFromXml( + $this->appConfigurationContext->getCapabilitiesXml(__METHOD__), + 'core', + 'status@@@productname' + ); + + if (!\strlen($productName)) { + Assert::fail( + "Cannot get productname from core capabilities" + ); + } + + $jsonExpectedDecoded['edition'] = $edition; + $jsonExpectedDecoded['product'] = $product; + $jsonExpectedDecoded['productname'] = $productName; + + if (OcisHelper::isTestingOnOc10()) { + // On oC10 get the expected version values by parsing the output of "occ status" + $runOccStatus = $this->runOcc(['status']); + if ($runOccStatus === 0) { + $output = \explode("- ", $this->lastStdOut); + $version = \explode(": ", $output[3]); + Assert::assertEquals( + "version", + $version[0], + "Expected 'version' but got $version[0]" + ); + $versionString = \explode(": ", $output[4]); + Assert::assertEquals( + "versionstring", + $versionString[0], + "Expected 'versionstring' but got $versionString[0]" + ); + $jsonExpectedDecoded['version'] = \trim($version[1]); + $jsonExpectedDecoded['versionstring'] = \trim($versionString[1]); + } else { + Assert::fail( + "Cannot get version variables from occ - status $runOccStatus" + ); + } + } else { + // We are on oCIS or reva or some other implementation. We cannot do "occ status". + // So get the expected version values by looking in the capabilities response. + $version = $this->appConfigurationContext->getParameterValueFromXml( + $this->appConfigurationContext->getCapabilitiesXml(__METHOD__), + 'core', + 'status@@@version' + ); + + if (!\strlen($version)) { + Assert::fail( + "Cannot get version from core capabilities" + ); + } + + $versionString = $this->appConfigurationContext->getParameterValueFromXml( + $this->appConfigurationContext->getCapabilitiesXml(__METHOD__), + 'core', + 'status@@@versionstring' + ); + + if (!\strlen($versionString)) { + Assert::fail( + "Cannot get versionstring from core capabilities" + ); + } + + $jsonExpectedDecoded['version'] = $version; + $jsonExpectedDecoded['versionstring'] = $versionString; + } + $errorMessage = ""; + $errorFound = false; + foreach ($jsonExpectedDecoded as $key => $expectedValue) { + if (\array_key_exists($key, $jsonRespondedDecoded)) { + $actualValue = $jsonRespondedDecoded[$key]; + if ($actualValue !== $expectedValue) { + $errorMessage .= "$key expected value was $expectedValue but actual value was $actualValue\n"; + $errorFound = true; + } + } else { + $errorMessage .= "$key was not found in the status response\n"; + $errorFound = true; + } + } + Assert::assertFalse($errorFound, $errorMessage); + // We have checked that the status.php response has data that matches up with + // data found in the capabilities response and/or the "occ status" command output. + // But the output might be reported wrongly in all of these in the same way. + // So check that the values also seem "reasonable". + $version = $jsonExpectedDecoded['version']; + $versionString = $jsonExpectedDecoded['versionstring']; + Assert::assertMatchesRegularExpression( + "/^\d+\.\d+\.\d+\.\d+$/", + $version, + "version should be in a form like 10.9.8.1 but is $version" + ); + if (\preg_match("/^(\d+\.\d+\.\d+)\.\d+(-[0-9A-Za-z-]+)?(\+[0-9A-Za-z-]+)?$/", $version, $matches)) { + // We should have matched something like 10.9.8 - the first 3 numbers in the version. + // Ignore pre-releases and meta information + Assert::assertArrayHasKey( + 1, + $matches, + "version $version could not match the pattern Major.Minor.Patch" + ); + $majorMinorPatchVersion = $matches[1]; + } else { + Assert::fail("version '$version' does not start in a form like 10.9.8"); + } + Assert::assertStringStartsWith( + $majorMinorPatchVersion, + $versionString, + "versionstring should start with $majorMinorPatchVersion but is $versionString" + ); + } + + /** + * send request to read a server file for core + * + * @param string $path + * + * @return void + */ + public function readFileInServerRootForCore(string $path): void { + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + 'GET', + "/apps/testing/api/v1/file?file=$path", + $this->getStepLineRef() + ); + $this->setResponse($response); + } + + /** + * read a server file for ocis + * + * @param string $path + * + * @return string + * @throws Exception + */ + public function readFileInServerRootForOCIS(string $path): string { + $pathToOcis = \getenv("PATH_TO_OCIS"); + $targetFile = \rtrim($pathToOcis, "/") . "/" . "services/web/assets" . "/" . ltrim($path, '/'); + if (!\file_exists($targetFile)) { + throw new Exception('Target File ' . $targetFile . ' could not be found'); + } + return \file_get_contents($targetFile); + } + + /** + * send request to list a server file + * + * @param string $path + * + * @return void + */ + public function listTrashbinFileInServerRoot(string $path): void { + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + 'GET', + "/apps/testing/api/v1/dir?dir=$path", + $this->getStepLineRef() + ); + $this->setResponse($response); + } + + /** + * move file in server root + * + * @param string $path + * @param string $target + * + * @return void + */ + public function moveFileInServerRoot(string $path, string $target): void { + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + "MOVE", + "/apps/testing/api/v1/file", + $this->getStepLineRef(), + [ + 'source' => $path, + 'target' => $target + ] + ); + + $this->setResponse($response); + } + + /** + * @When the local storage mount for :mount is renamed to :target + * + * @param string $mount + * @param string $target + * + * @return void + */ + public function theLocalStorageMountForIsRenamedTo(string $mount, string $target): void { + $mountPath = TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/" . ltrim($mount, '/'); + $targetPath = TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/" . ltrim($target, '/'); + + $this->moveFileInServerRoot($mountPath, $targetPath); + } + + /** + * @Then the file :path with content :content should exist in the server root + * + * @param string $path + * @param string $content + * + * @return void + * @throws Exception + */ + public function theFileWithContentShouldExistInTheServerRoot(string $path, string $content): void { + if (OcisHelper::isTestingOnOcis()) { + $fileContent = $this->readFileInServerRootForOCIS($path); + } else { + $this->readFileInServerRootForCore($path); + $this->theHTTPStatusCodeShouldBe(200, 'Failed to read the file $path'); + $fileContent = $this->getResponseXml(); + $fileContent = (string)$fileContent->data->element->contentUrlEncoded; + $fileContent = \urldecode($fileContent); + } + Assert::assertSame( + $content, + $fileContent, + "The content of the file does not match with '$content'" + ); + } + + /** + * @Then /^the content in the response should match with the content of file "([^"]*)" in the server root$/ + * + * @param string $path + * + * @return void + * @throws Exception + */ + public function theContentInTheRespShouldMatchWithFileInTheServerRoot(string $path): void { + $content = $this->getResponse()->getBody()->getContents(); + $this->theFileWithContentShouldExistInTheServerRoot($path, $content); + } + + /** + * @Then the file :path should not exist in the server root + * + * @param string $path + * + * @return void + */ + public function theFileShouldNotExistInTheServerRoot(string $path): void { + $this->readFileInServerRootForCore($path); + Assert::assertSame( + 404, + $this->getResponse()->getStatusCode(), + "The file '$path' exists in the server root but was not expected to exist" + ); + } + + /** + * @Then the body of the response should be empty + * + * @return void + */ + public function theResponseBodyShouldBeEmpty(): void { + Assert::assertEmpty( + $this->getResponse()->getBody()->getContents(), + "The response body was expected to be empty but got " + . $this->getResponse()->getBody()->getContents() + ); + } + + /** + * @param ResponseInterface|null $response + * + * @return array + */ + public function getJsonDecodedResponse(?ResponseInterface $response = null): array { + if ($response === null) { + $response = $this->getResponse(); + } + return \json_decode( + (string)$response->getBody(), + true + ); + } + + /** + * + * @return array + */ + public function getJsonDecodedStatusPhp(): array { + return $this->getJsonDecodedResponse( + $this->getStatusPhp() + ); + } + + /** + * @return string + */ + public function getEditionFromStatus(): string { + $decodedResponse = $this->getJsonDecodedStatusPhp(); + if (isset($decodedResponse['edition'])) { + return $decodedResponse['edition']; + } + return ''; + } + + /** + * @return string|null + */ + public function getProductNameFromStatus(): ?string { + $decodedResponse = $this->getJsonDecodedStatusPhp(); + if (isset($decodedResponse['productname'])) { + return $decodedResponse['productname']; + } + return ''; + } + + /** + * @return string|null + */ + public function getVersionFromStatus(): ?string { + $decodedResponse = $this->getJsonDecodedStatusPhp(); + if (isset($decodedResponse['version'])) { + return $decodedResponse['version']; + } + return ''; + } + + /** + * @return string|null + */ + public function getVersionStringFromStatus(): ?string { + $decodedResponse = $this->getJsonDecodedStatusPhp(); + if (isset($decodedResponse['versionstring'])) { + return $decodedResponse['versionstring']; + } + return ''; + } + + /** + * returns a string that can be used to check a url of comments with + * regular expression (without delimiter) + * + * @return string + */ + public function getCommentUrlRegExp(): string { + $basePath = \ltrim($this->getBasePath() . "/", "/"); + return "/{$basePath}remote.php/dav/comments/files/([0-9]+)"; + } + + /** + * substitutes codes like %base_url% with the value + * if the given value does not have anything to be substituted + * then it is returned unmodified + * + * @param string|null $value + * @param string|null $user + * @param array|null $functions associative array of functions and parameters to be + * called on every replacement string before the + * replacement + * function name has to be the key and the parameters an + * own array + * the replacement itself will be used as first parameter + * e.g. substituteInLineCodes($value, ['preg_quote' => ['/']]) + * @param array|null $additionalSubstitutions + * array of additional substitution configurations + * [ + * [ + * "code" => "%my_code%", + * "function" => [ + * $myClass, + * "myFunction" + * ], + * "parameter" => [] + * ], + * ] + * + * @return string + */ + public function substituteInLineCodes( + ?string $value, + ?string $user = null, + ?array $functions = [], + ?array $additionalSubstitutions = [] + ): ?string { + $substitutions = [ + [ + "code" => "%base_url%", + "function" => [ + $this, + "getBaseUrl" + ], + "parameter" => [] + ], + [ + "code" => "%base_url_without_scheme%", + "function" => [ + $this, + "getBaseUrlWithoutScheme" + ], + "parameter" => [] + ], + [ + "code" => "%remote_server%", + "function" => [ + $this, + "getRemoteBaseUrl" + ], + "parameter" => [] + ], + [ + "code" => "%remote_server_without_scheme%", + "function" => [ + $this, + "getRemoteBaseUrlWithoutScheme" + ], + "parameter" => [] + ], + [ + "code" => "%local_server%", + "function" => [ + $this, + "getLocalBaseUrl" + ], + "parameter" => [] + ], + [ + "code" => "%local_server_without_scheme%", + "function" => [ + $this, + "getLocalBaseUrlWithoutScheme" + ], + "parameter" => [] + ], + [ + "code" => "%base_path%", + "function" => [ + $this, + "getBasePath" + ], + "parameter" => [] + ], + [ + "code" => "%dav_path%", + "function" => [ + $this, + "getDAVPathIncludingBasePath" + ], + "parameter" => [] + ], + [ + "code" => "%ocs_path_v1%", + "function" => [ + $this, + "getOCSPath" + ], + "parameter" => ["1"] + ], + [ + "code" => "%ocs_path_v2%", + "function" => [ + $this, + "getOCSPath" + ], + "parameter" => ["2"] + ], + [ + "code" => "%productname%", + "function" => [ + $this, + "getProductNameFromStatus" + ], + "parameter" => [] + ], + [ + "code" => "%edition%", + "function" => [ + $this, + "getEditionFromStatus" + ], + "parameter" => [] + ], + [ + "code" => "%version%", + "function" => [ + $this, + "getVersionFromStatus" + ], + "parameter" => [] + ], + [ + "code" => "%versionstring%", + "function" => [ + $this, + "getVersionStringFromStatus" + ], + "parameter" => [] + ], + [ + "code" => "%a_comment_url%", + "function" => [ + $this, + "getCommentUrlRegExp" + ], + "parameter" => [] + ], + [ + "code" => "%last_share_id%", + "function" => [ + $this, + "getLastShareId" + ], + "parameter" => [] + ], + [ + "code" => "%last_public_share_token%", + "function" => [ + $this, + "getLastPublicShareToken" + ], + "parameter" => [] + ] + ]; + if ($user !== null) { + array_push( + $substitutions, + [ + "code" => "%username%", + "function" => [ + $this, + "getActualUsername" + ], + "parameter" => [$user] + ], + [ + "code" => "%displayname%", + "function" => [ + $this, + "getDisplayNameForUser" + ], + "parameter" => [$user] + ], + [ + "code" => "%password%", + "function" => [ + $this, + "getPasswordForUser" + ], + "parameter" => [$user] + ], + [ + "code" => "%emailaddress%", + "function" => [ + $this, + "getEmailAddressForUser" + ], + "parameter" => [$user] + ], + [ + "code" => "%spaceid%", + "function" => [ + $this, + "getPersonalSpaceIdForUser", + ], + "parameter" => [$user, true] + ] + ); + } + + if (!empty($additionalSubstitutions)) { + $substitutions = \array_merge($substitutions, $additionalSubstitutions); + } + + foreach ($substitutions as $substitution) { + if (strpos($value, $substitution['code']) === false) { + continue; + } + + $replacement = \call_user_func_array( + $substitution["function"], + $substitution["parameter"] + ); + foreach ($functions as $function => $parameters) { + $replacement = \call_user_func_array( + $function, + \array_merge([$replacement], $parameters) + ); + } + $value = \str_replace( + $substitution["code"], + $replacement, + $value + ); + } + return $value; + } + + /** + * returns personal space id for user if the test is using the spaces dav path + * or if alwaysDoIt is set to true, + * otherwise it returns null. + * + * @param string $user + * @param bool $alwaysDoIt default false. Set to true + * + * @return string|null + * @throws GuzzleException + */ + public function getPersonalSpaceIdForUser(string $user, bool $alwaysDoIt = false): ?string { + if ($alwaysDoIt || ($this->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES)) { + return WebDavHelper::getPersonalSpaceIdForUserOrFakeIfNotFound( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + $this->getStepLineRef() + ); + } + return null; + } + + /** + * @return string + */ + public function temporaryStorageSubfolderName(): string { + return "work_tmp"; + } + + /** + * @return string + */ + public function acceptanceTestsDirLocation(): string { + return \dirname(__FILE__) . "/../../"; + } + + /** + * @return string + */ + public function workStorageDirLocation(): string { + return $this->acceptanceTestsDirLocation() . $this->temporaryStorageSubfolderName() . "/"; + } + + /** + * Get the path of the ownCloud server root directory + * + * @return string + * @throws Exception + */ + public function getServerRoot(): string { + if ($this->localServerRoot === null) { + $this->localServerRoot = SetupHelper::getServerRoot( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef() + ); + } + return $this->localServerRoot; + } + + /** + * @Then the config key :key of app :appID should have value :value + * + * @param string $key + * @param string $appID + * @param string $value + * + * @return void + * @throws Exception + */ + public function theConfigKeyOfAppShouldHaveValue(string $key, string $appID, string $value): void { + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + 'GET', + "/apps/testing/api/v1/app/$appID/$key", + $this->getStepLineRef(), + [], + $this->getOcsApiVersion() + ); + $configkeyValue = (string)$this->getResponseXml($response, __METHOD__)->data[0]->element->value; + Assert::assertEquals( + $value, + $configkeyValue, + "The config key $key of app $appID was expected to have value $value but got $configkeyValue" + ); + } + + /** + * Parse list of config keys from the given XML response + * + * @param SimpleXMLElement $responseXml + * + * @return array + */ + public function parseConfigListFromResponseXml(SimpleXMLElement $responseXml): array { + $configkeyData = \json_decode(\json_encode($responseXml->data), true); + if (isset($configkeyData['element'])) { + $configkeyData = $configkeyData['element']; + } else { + // There are no keys for the app + return []; + } + if (isset($configkeyData[0])) { + $configkeyValues = $configkeyData; + } else { + // There is just 1 key for the app + $configkeyValues[0] = $configkeyData; + } + return $configkeyValues; + } + + /** + * Returns a list of config keys for the given app + * + * @param string $appID + * @param string $exceptionText text to put at the front of exception messages + * + * @return array + * @throws Exception + */ + public function getConfigKeyList(string $appID, string $exceptionText = ''): array { + if ($exceptionText === '') { + $exceptionText = __METHOD__; + } + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + 'GET', + "/apps/testing/api/v1/app/$appID", + $this->getStepLineRef(), + [], + $this->getOcsApiVersion() + ); + return $this->parseConfigListFromResponseXml( + $this->getResponseXml($response, $exceptionText) + ); + } + + /** + * Check if given config key is present for given app + * + * @param string $key + * @param string $appID + * + * @return bool + * @throws Exception + */ + public function checkConfigKeyInApp(string $key, string $appID): bool { + $configkeyList = $this->getConfigKeyList($appID); + foreach ($configkeyList as $config) { + if ($config['configkey'] === $key) { + return true; + } + } + return false; + } + + /** + * @Then /^app ((?:'[^']*')|(?:"[^"]*")) should (not|)\s?have config key ((?:'[^']*')|(?:"[^"]*"))$/ + * + * @param string $appID + * @param string $shouldOrNot + * @param string $key + * + * @return void + * @throws Exception + */ + public function appShouldHaveConfigKey(string $appID, string $shouldOrNot, string $key): void { + $appID = \trim($appID, $appID[0]); + $key = \trim($key, $key[0]); + + $should = ($shouldOrNot !== "not"); + + if ($should) { + Assert::assertTrue( + $this->checkConfigKeyInApp($key, $appID), + "App $appID does not have config key $key" + ); + } else { + Assert::assertFalse( + $this->checkConfigKeyInApp($key, $appID), + "App $appID has config key $key but was not expected to" + ); + } + } + + /** + * @Then /^following config keys should (not|)\s?exist$/ + * + * @param string $shouldOrNot + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function followingConfigKeysShouldExist(string $shouldOrNot, TableNode $table): void { + $should = ($shouldOrNot !== "not"); + if ($should) { + foreach ($table as $item) { + Assert::assertTrue( + $this->checkConfigKeyInApp($item['configkey'], $item['appid']), + "{$item['appid']} was expected to have config key {$item['configkey']} but does not" + ); + } + } else { + foreach ($table as $item) { + Assert::assertFalse( + $this->checkConfigKeyInApp($item['configkey'], $item['appid']), + "Expected : {$item['appid']} should not have config key {$item['configkey']}" + ); + } + } + } + + /** + * @param string $user + * @param string|null $asUser + * @param string|null $password + * + * @return void + */ + public function sendUserSyncRequest(string $user, ?string $asUser = null, ?string $password = null): void { + $user = $this->getActualUsername($user); + $asUser = $asUser ?? $this->getAdminUsername(); + $password = $password ?? $this->getPasswordForUser($asUser); + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $asUser, + $password, + 'POST', + "/cloud/user-sync/$user", + $this->getStepLineRef(), + [], + $this->getOcsApiVersion() + ); + $this->setResponse($response); + } + + /** + * @When the administrator tries to sync user :user using the OCS API + * + * @param string $user + * + * @return void + */ + public function theAdministratorTriesToSyncUserUsingTheOcsApi(string $user): void { + $this->sendUserSyncRequest($user); + } + + /** + * @When user :asUser tries to sync user :user using the OCS API + * + * @param string $asUser + * @param string $user + * + * @return void + */ + public function userTriesToSyncUserUsingTheOcsApi(string $asUser, string $user): void { + $asUser = $this->getActualUsername($asUser); + $user = $this->getActualUsername($user); + $this->sendUserSyncRequest($user, $asUser); + } + + /** + * @When the administrator tries to sync user :user using password :password and the OCS API + * + * @param string|null $user + * @param string|null $password + * + * @return void + */ + public function theAdministratorTriesToSyncUserUsingPasswordAndTheOcsApi(?string $user, ?string $password): void { + $this->sendUserSyncRequest($user, null, $password); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + * @throws Exception + */ + public function before(BeforeScenarioScope $scope): void { + $this->scenarioStartTime = \time(); + // Get the environment + $environment = $scope->getEnvironment(); + // registers context in every suite, as every suite has FeatureContext + // that calls BasicStructure.php + $this->ocsContext = new OCSContext(); + $this->authContext = new AuthContext(); + $this->appConfigurationContext = new AppConfigurationContext(); + $this->ocsContext->before($scope); + $this->authContext->setUpScenario($scope); + $this->appConfigurationContext->setUpScenario($scope); + $environment->registerContext($this->ocsContext); + $environment->registerContext($this->authContext); + $environment->registerContext($this->appConfigurationContext); + $scenarioLine = $scope->getScenario()->getLine(); + $featureFile = $scope->getFeature()->getFile(); + $suiteName = $scope->getSuite()->getName(); + $featureFileName = \basename($featureFile); + + if ($this->sendScenarioLineReferencesInXRequestId()) { + $this->scenarioString = $suiteName . '/' . $featureFileName . ':' . $scenarioLine; + } else { + $this->scenarioString = ''; + } + + // Initialize SetupHelper + SetupHelper::init( + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getBaseUrl(), + $this->getOcPath() + ); + + if ($this->isTestingWithLdap()) { + $suiteParameters = SetupHelper::getSuiteParameters($scope); + $this->connectToLdap($suiteParameters); + } + + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext = new GraphContext(); + $this->graphContext->before($scope); + $environment->registerContext($this->graphContext); + } + } + + /** + * This will run before EVERY step. + * + * @BeforeStep + * + * @param BeforeStepScope $scope + * + * @return void + */ + public function beforeEachStep(BeforeStepScope $scope): void { + if ($this->sendScenarioLineReferencesInXRequestId()) { + $this->stepLineRef = $this->scenarioString . '-' . $scope->getStep()->getLine(); + } else { + $this->stepLineRef = ''; + } + } + + /** + * @BeforeScenario @local_storage + * + * @return void + * @throws Exception + */ + public function setupLocalStorageBefore(): void { + $storageName = "local_storage"; + $result = SetupHelper::createLocalStorageMount( + $storageName, + $this->getStepLineRef() + ); + $storageId = $result['storageId']; + if (!is_numeric($storageId)) { + throw new Exception( + __METHOD__ . " storageId '$storageId' is not numeric" + ); + } + $this->addStorageId($storageName, (int)$storageId); + SetupHelper::runOcc( + [ + 'files_external:option', + $storageId, + 'enable_sharing', + 'true' + ], + $this->getStepLineRef() + ); + } + + /** + * @AfterScenario + * + * @return void + */ + public function restoreAdminPassword(): void { + if ($this->adminPassword !== $this->originalAdminPassword) { + $this->resetUserPasswordAsAdminUsingTheProvisioningApi( + $this->getAdminUsername(), + $this->originalAdminPassword + ); + $this->adminPassword = $this->originalAdminPassword; + } + } + + /** + * @AfterScenario + * + * @return void + */ + public function deleteAllResourceCreatedByAdmin(): void { + foreach ($this->adminResources as $resource) { + $this->userDeletesFile("admin", $resource); + } + } + + /** + * deletes all created storages + * + * @return void + * @throws Exception + */ + public function deleteAllStorages(): void { + $allStorageIds = \array_keys($this->getStorageIds()); + foreach ($allStorageIds as $storageId) { + SetupHelper::runOcc( + [ + 'files_external:delete', + '-y', + $storageId + ], + $this->getStepLineRef() + ); + } + $this->storageIds = []; + } + + /** + * @AfterScenario @local_storage + * + * @return void + * @throws Exception + */ + public function removeLocalStorageAfter(): void { + $this->removeExternalStorage(); + $this->removeTemporaryStorageOnServerAfter(); + } + + /** + * This will remove test created external mount points + * + * @AfterScenario @external_storage + * + * @return void + * @throws Exception + */ + public function removeExternalStorage(): void { + if ($this->getStorageIds() !== null) { + $this->deleteAllStorages(); + } + } + + /** + * @BeforeScenario @temporary_storage_on_server + * + * @return void + * @throws Exception + */ + public function makeTemporaryStorageOnServerBefore(): void { + $this->mkDirOnServer( + TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER + ); + } + + /** + * @AfterScenario @temporary_storage_on_server + * + * @return void + * @throws Exception + */ + public function removeTemporaryStorageOnServerAfter(): void { + SetupHelper::rmDirOnServer( + TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER, + $this->getStepLineRef() + ); + } + + /** + * @AfterScenario + * + * @return void + */ + public function removeCreatedFilesAfter(): void { + foreach ($this->createdFiles as $file) { + \unlink($file); + } + } + + /** + * + * @param string $serverUrl + * + * @return void + */ + public function clearFileLocksForServer(string $serverUrl): void { + $response = OcsApiHelper::sendRequest( + $serverUrl, + $this->getAdminUsername(), + $this->getAdminPassword(), + 'delete', + "/apps/testing/api/v1/lockprovisioning", + $this->getStepLineRef(), + ["global" => "true"] + ); + Assert::assertEquals("200", $response->getStatusCode()); + } + + /** + * After Scenario. clear file locks + * + * @AfterScenario + * + * @return void + * @throws Exception + */ + public function clearFileLocks(): void { + if (!OcisHelper::isTestingOnOcisOrReva()) { + $this->authContext->deleteTokenAuthEnforcedAfterScenario(); + $this->clearFileLocksForServer($this->getBaseUrl()); + if ($this->remoteBaseUrl !== $this->localBaseUrl) { + $this->clearFileLocksForServer($this->getRemoteBaseUrl()); + } + } + } + + /** + * @AfterScenario + * + * clear space id reference + * + * @return void + * @throws Exception + */ + public function clearSpaceId(): void { + if (\count(WebDavHelper::$spacesIdRef) > 0) { + WebDavHelper::$spacesIdRef = []; + } + } + + /** + * @BeforeSuite + * + * @param BeforeSuiteScope $scope + * + * @return void + * @throws Exception + */ + public static function useBigFileIDs(BeforeSuiteScope $scope): void { + if (OcisHelper::isTestingOnOcisOrReva()) { + return; + } + $fullUrl = \getenv('TEST_SERVER_URL'); + if (\substr($fullUrl, -1) !== '/') { + $fullUrl .= '/'; + } + $fullUrl .= "ocs/v1.php/apps/testing/api/v1/increasefileid"; + $suiteSettingsContexts = $scope->getSuite()->getSettings()['contexts']; + $adminUsername = null; + $adminPassword = null; + foreach ($suiteSettingsContexts as $context) { + if (isset($context[__CLASS__])) { + $adminUsername = $context[__CLASS__]['adminUsername']; + $adminPassword = $context[__CLASS__]['adminPassword']; + break; + } + } + + // get the admin username from the environment (if defined) + $adminUsernameFromEnvironment = self::getAdminUsernameFromEnvironment(); + if ($adminUsernameFromEnvironment !== false) { + $adminUsername = $adminUsernameFromEnvironment; + } + + // get the admin password from the environment (if defined) + $adminPasswordFromEnvironment = self::getAdminPasswordFromEnvironment(); + if ($adminPasswordFromEnvironment !== false) { + $adminPassword = $adminPasswordFromEnvironment; + } + + if (($adminUsername === null) || ($adminPassword === null)) { + throw new Exception( + "Could not find adminUsername and/or adminPassword in useBigFileIDs" + ); + } + + HttpRequestHelper::post( + $fullUrl, + '', + $adminUsername, + $adminPassword + ); + } + + /** + * runs a function on every server (LOCAL & REMOTE). + * The callable function receives the server (LOCAL or REMOTE) as first argument + * + * @param callable $callback + * + * @return array + */ + public function runFunctionOnEveryServer(callable $callback): array { + $previousServer = $this->getCurrentServer(); + $result = []; + foreach (['LOCAL', 'REMOTE'] as $server) { + $this->usingServer($server); + if (($server === 'LOCAL') + || $this->federatedServerExists() + ) { + $result[$server] = \call_user_func($callback, $server); + } + } + $this->usingServer($previousServer); + return $result; + } + + /** + * Verify that the tableNode contains expected headers + * + * @param TableNode $table + * @param array|null $requiredHeader + * @param array|null $allowedHeader + * + * @return void + * @throws Exception + */ + public function verifyTableNodeColumns(TableNode $table, ?array $requiredHeader = [], ?array $allowedHeader = []): void { + if (\count($table->getHash()) < 1) { + throw new Exception("Table should have at least one row."); + } + $tableHeaders = $table->getRows()[0]; + $allowedHeader = \array_unique(\array_merge($requiredHeader, $allowedHeader)); + if ($requiredHeader != []) { + foreach ($requiredHeader as $element) { + if (!\in_array($element, $tableHeaders)) { + throw new Exception("Row with header '$element' expected to be in table but not found"); + } + } + } + + if ($allowedHeader != []) { + foreach ($tableHeaders as $element) { + if (!\in_array($element, $allowedHeader)) { + throw new Exception("Row with header '$element' is not allowed in table but found"); + } + } + } + } + + /** + * Verify that the tableNode contains expected rows + * + * @param TableNode $table + * @param array $requiredRows + * @param array $allowedRows + * + * @return void + * @throws Exception + */ + public function verifyTableNodeRows(TableNode $table, array $requiredRows = [], array $allowedRows = []): void { + if (\count($table->getRows()) < 1) { + throw new Exception("Table should have at least one row."); + } + $tableHeaders = $table->getColumn(0); + $allowedRows = \array_unique(\array_merge($requiredRows, $allowedRows)); + if ($requiredRows != []) { + foreach ($requiredRows as $element) { + if (!\in_array($element, $tableHeaders)) { + throw new Exception("Row with name '$element' expected to be in table but not found"); + } + } + } + + if ($allowedRows != []) { + foreach ($tableHeaders as $element) { + if (!\in_array($element, $allowedRows)) { + throw new Exception("Row with name '$element' is not allowed in table but found"); + } + } + } + } + + /** + * Verify that the tableNode contains expected number of columns + * + * @param TableNode $table + * @param int $count + * + * @return void + * @throws Exception + */ + public function verifyTableNodeColumnsCount(TableNode $table, int $count): void { + if (\count($table->getRows()) < 1) { + throw new Exception("Table should have at least one row."); + } + $rowCount = \count($table->getRows()[0]); + if ($count !== $rowCount) { + throw new Exception("Table expected to have $count rows but found $rowCount"); + } + } + + /** + * @return void + */ + public function resetAppConfigs(): void { + // Set the required starting values for testing + $this->setCapabilities($this->getCommonSharingConfigs()); + } + + /** + * @Given the administrator has set the last login date for user :user to :days days ago + * @When the administrator sets the last login date for user :user to :days days ago using the testing API + * + * @param string $user + * @param string $days + * + * @return void + */ + public function theAdministratorSetsTheLastLoginDateForUserToDaysAgoUsingTheTestingApi(string $user, string $days): void { + $user = $this->getActualUsername($user); + $adminUser = $this->getAdminUsername(); + $baseUrl = "/apps/testing/api/v1/lastlogindate/$user"; + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $adminUser, + $this->getAdminPassword(), + 'POST', + $baseUrl, + $this->getStepLineRef(), + ['days' => $days], + $this->getOcsApiVersion() + ); + $this->setResponse($response); + } + + /** + * @param array $capabilitiesArray with each array entry containing keys for: + * ['capabilitiesApp'] the "app" name in the capabilities response + * ['capabilitiesParameter'] the parameter name in the capabilities response + * ['testingApp'] the "app" name as understood by "testing" + * ['testingParameter'] the parameter name as understood by "testing" + * ['testingState'] boolean state the parameter must be set to for the test + * + * @return void + */ + public function setCapabilities(array $capabilitiesArray): void { + AppConfigHelper::setCapabilities( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + $capabilitiesArray, + $this->getStepLineRef() + ); + } + + /** + * After Scenario. restore trusted servers + * + * @AfterScenario @federation-app-required + * + * @return void + */ + public function restoreTrustedServersAfterScenario(): void { + $this->restoreTrustedServers('LOCAL'); + if ($this->federatedServerExists()) { + $this->restoreTrustedServers('REMOTE'); + } + } + + /** + * Invokes an OCC command + * + * @param array|null $args of the occ command + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return int exit code + * @throws Exception if ocPath has not been set yet or the testing app is not enabled + */ + public function runOcc( + ?array $args = [], + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null + ): int { + return $this->runOccWithEnvVariables( + $args, + null, + $adminUsername, + $adminPassword, + $baseUrl, + $ocPath + ); + } + + /** + * Invokes an OCC command with an optional array of environment variables + * + * @param array|null $args of the occ command + * @param array|null $envVariables to be defined before the command is run + * @param string|null $adminUsername + * @param string|null $adminPassword + * @param string|null $baseUrl + * @param string|null $ocPath + * + * @return int exit code + * @throws Exception if ocPath has not been set yet or the testing app is not enabled + */ + public function runOccWithEnvVariables( + ?array $args = [], + ?array $envVariables = null, + ?string $adminUsername = null, + ?string $adminPassword = null, + ?string $baseUrl = null, + ?string $ocPath = null + ): int { + $args[] = '--no-ansi'; + if ($baseUrl == null) { + $baseUrl = $this->getBaseUrl(); + } + $return = SetupHelper::runOcc( + $args, + $this->getStepLineRef(), + $adminUsername, + $adminPassword, + $baseUrl, + $ocPath, + $envVariables + ); + $this->lastStdOut = $return['stdOut']; + $this->lastStdErr = $return['stdErr']; + $occStatusCode = (int)$return['code']; + return $occStatusCode; + } + + /** + * Find exception texts in stderr + * + * @return array of exception texts + */ + public function findExceptions(): array { + $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; + } + + /** + * remember the result of the last occ command + * + * @param string[] $result associated array with "code", "stdOut", "stdErr" + * + * @return void + */ + public function setResultOfOccCommand(array $result): void { + Assert::assertIsArray($result); + Assert::assertArrayHasKey('code', $result); + Assert::assertArrayHasKey('stdOut', $result); + Assert::assertArrayHasKey('stdErr', $result); + $this->occLastCode = (int)$result['code']; + $this->lastStdOut = $result['stdOut']; + $this->lastStdErr = $result['stdErr']; + } + + /** + * @param string $sourceUser + * @param string $targetUser + * + * @return string|null + * @throws Exception + */ + public function findLastTransferFolderForUser(string $sourceUser, string $targetUser): ?string { + $foundPaths = []; + $responseXmlObject = $this->listFolderAndReturnResponseXml( + $targetUser, + '', + '1' + ); + $transferredElements = $responseXmlObject->xpath( + "//d:response/d:href[contains(., '/transferred%20from%20$sourceUser%20on%')]" + ); + foreach ($transferredElements as $transferredElement) { + // $transferredElement is an XML object. We want to work with the string in the XML element. + $path = \rawurldecode((string)$transferredElement); + $parts = \explode(' ', $path); + // 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->getFullDavFilesPath($targetUser), '/'); + + $foundPath = \end($foundPaths)['path']; + // strip DAV path + return \substr($foundPath, \strlen($davPath) + 1); + } + + /** + * After Scenario. restore trusted servers + * + * @param string $server 'LOCAL'/'REMOTE' + * + * @return void + * @throws Exception + */ + public function restoreTrustedServers(string $server): void { + $currentTrustedServers = $this->getTrustedServers($server); + foreach (\array_diff($currentTrustedServers, $this->initialTrustedServer[$server]) as $url => $id) { + $this->appConfigurationContext->theAdministratorDeletesUrlFromTrustedServersUsingTheTestingApi($url); + } + foreach (\array_diff($this->initialTrustedServer[$server], $currentTrustedServers) as $url => $id) { + $this->appConfigurationContext->theAdministratorAddsUrlAsTrustedServerUsingTheTestingApi($url); + } + } + + /** + * + * @return void + * @throws Exception + */ + public function restoreParametersAfterScenario(): void { + if (!OcisHelper::isTestingOnOcisOrReva()) { + $this->authContext->deleteTokenAuthEnforcedAfterScenario(); + $user = $this->getCurrentUser(); + $this->setCurrentUser($this->getAdminUsername()); + $this->runFunctionOnEveryServer( + function ($server) { + $this->restoreParameters($server); + } + ); + $this->setCurrentUser($user); + } + } + + /** + * Get the array of trusted servers in format ["url" => "id"] + * + * @param string $server 'LOCAL'/'REMOTE' + * + * @return array + * @throws Exception + */ + public function getTrustedServers(string $server = 'LOCAL'): array { + if ($server === 'LOCAL') { + $url = $this->getLocalBaseUrl(); + } elseif ($server === 'REMOTE') { + $url = $this->getRemoteBaseUrl(); + } else { + throw new Exception(__METHOD__ . " Invalid value for server : $server"); + } + $adminUser = $this->getAdminUsername(); + $response = OcsApiHelper::sendRequest( + $url, + $adminUser, + $this->getAdminPassword(), + 'GET', + "/apps/testing/api/v1/trustedservers", + $this->getStepLineRef() + ); + if ($response->getStatusCode() !== 200) { + throw new Exception("Could not get the list of trusted servers" . $response->getBody()->getContents()); + } + $responseXml = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $serverData = \json_decode( + \json_encode( + $responseXml->data + ), + true + ); + if (!\array_key_exists('element', $serverData)) { + return []; + } else { + return isset($serverData['element'][0]) ? + \array_column($serverData['element'], 'id', 'url') : + \array_column($serverData, 'id', 'url'); + } + } + + /** + * @param string $method http request method + * @param string $property property in form d:getetag + * if property is `doesnotmatter` body is also set `doesnotmatter` + * + * @return string + */ + public function getBodyForOCSRequest(string $method, string $property): ?string { + $body = null; + if ($method === 'PROPFIND') { + $body = '<' . $property . '/>'; + } elseif ($method === 'LOCK') { + $body = " <" . $property . " />"; + } elseif ($method === 'PROPPATCH') { + if ($property === 'favorite') { + $property = '1'; + } + $body = '' . $property . ''; + } + if ($property === '') { + $body = ''; + } + return $body; + } + + /** + * @BeforeScenario + * + * @return void + * @throws Exception + */ + public function prepareParametersBeforeScenario(): void { + if (!OcisHelper::isTestingOnOcisOrReva()) { + $user = $this->getCurrentUser(); + $this->setCurrentUser($this->getAdminUsername()); + $previousServer = $this->getCurrentServer(); + foreach (['LOCAL', 'REMOTE'] as $server) { + if (($server === 'LOCAL') || $this->federatedServerExists()) { + $this->usingServer($server); + $this->resetAppConfigs(); + $result = SetupHelper::runOcc( + ['config:list', '--private'], + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getBaseUrl(), + $this->getOcPath() + ); + $this->savedConfigList[$server] = \json_decode($result['stdOut'], true); + } + } + $this->usingServer($previousServer); + $this->setCurrentUser($user); + } + } + + /** + * Before Scenario to Save trusted Servers + * + * @BeforeScenario @federation-app-required + * + * @return void + * @throws Exception + */ + public function setInitialTrustedServersBeforeScenario(): void { + $this->initialTrustedServer = [ + 'LOCAL' => $this->getTrustedServers(), + 'REMOTE' => $this->getTrustedServers('REMOTE') + ]; + } + + /** + * restore settings of the system and delete new settings that were created in the test runs + * + * @param string $server LOCAL|REMOTE + * + * @return void + * + * @throws Exception + * @throws GuzzleException + * + */ + private function restoreParameters(string $server): void { + $commands = []; + if ($this->isTestingWithLdap()) { + $this->resetOldLdapConfig(); + } + $result = SetupHelper::runOcc( + ['config:list'], + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getBaseUrl(), + $this->getOcPath() + ); + $currentConfigList = \json_decode($result['stdOut'], true); + foreach ($currentConfigList['system'] as $configKey => $configValue) { + if (!\array_key_exists( + $configKey, + $this->savedConfigList[$server]['system'] + ) + ) { + $commands[] = ["command" => ['config:system:delete', $configKey]]; + } + } + foreach ($this->savedConfigList[$server]['system'] as $configKey => $configValue) { + if (!\array_key_exists($configKey, $currentConfigList["system"]) + || $currentConfigList["system"][$configKey] !== $this->savedConfigList[$server]['system'][$configKey] + ) { + $commands[] = ["command" => ['config:system:set', "--type=json", "--value=" . \json_encode($configValue), $configKey]]; + } + } + foreach ($currentConfigList['apps'] as $appName => $appSettings) { + foreach ($appSettings as $configKey => $configValue) { + //only check if the app was there in the original configuration + if (\array_key_exists($appName, $this->savedConfigList[$server]['apps']) + && !\array_key_exists( + $configKey, + $this->savedConfigList[$server]['apps'][$appName] + ) + ) { + $commands[] = ["command" => ['config:app:delete', $appName, $configKey]]; + } elseif (\array_key_exists($appName, $this->savedConfigList[$server]['apps']) + && \array_key_exists($configKey, $this->savedConfigList[$server]['apps'][$appName]) + && $this->savedConfigList[$server]['apps'][$appName][$configKey] !== $configValue + ) { + // Do not accidentally disable apps here (perhaps too early) + // That is done in Provisioning.php restoreAppEnabledDisabledState() + if ($configKey !== "enabled") { + $commands[] = [ + "command" => [ + 'config:app:set', + $appName, + $configKey, + "--value=" . $this->savedConfigList[$server]['apps'][$appName][$configKey] + ] + ]; + } + } + } + } + SetupHelper::runBulkOcc( + $commands, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getBaseUrl() + ); + } +} diff --git a/tests/acceptance/features/bootstrap/FilesVersionsContext.php b/tests/acceptance/features/bootstrap/FilesVersionsContext.php new file mode 100644 index 000000000..7f26e3ed7 --- /dev/null +++ b/tests/acceptance/features/bootstrap/FilesVersionsContext.php @@ -0,0 +1,443 @@ + + * @copyright Copyright (c) 2018, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; +use TestHelpers\HttpRequestHelper; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * Steps that relate to files_versions app + */ +class FilesVersionsContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * @param string $fileId + * + * @return string + */ + private function getVersionsPathForFileId(string $fileId):string { + return "/meta/$fileId/v"; + } + + /** + * @When user :user tries to get versions of file :file from :fileOwner + * + * @param string $user + * @param string $file + * @param string $fileOwner + * + * @return void + * @throws Exception + */ + public function userTriesToGetFileVersions(string $user, string $file, string $fileOwner):void { + $user = $this->featureContext->getActualUsername($user); + $fileOwner = $this->featureContext->getActualUsername($fileOwner); + $fileId = $this->featureContext->getFileIdForPath($fileOwner, $file); + Assert::assertNotNull($fileId, __METHOD__ . " fileid of file $file user $fileOwner not found (the file may not exist)"); + $response = $this->featureContext->makeDavRequest( + $user, + "PROPFIND", + $this->getVersionsPathForFileId($fileId), + null, + null, + null, + '2' + ); + $this->featureContext->setResponse($response, $user); + } + + /** + * @When user :user gets the number of versions of file :file + * + * @param string $user + * @param string $file + * + * @return void + * @throws Exception + */ + public function userGetsFileVersions(string $user, string $file):void { + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $file); + Assert::assertNotNull($fileId, __METHOD__ . " fileid of file $file user $user not found (the file may not exist)"); + $response = $this->featureContext->makeDavRequest( + $user, + "PROPFIND", + $this->getVersionsPathForFileId($fileId), + null, + null, + null, + '2' + ); + $this->featureContext->setResponse($response, $user); + } + + /** + * @When user :user gets the version metadata of file :file + * + * @param string $user + * @param string $file + * + * @return void + * @throws Exception + */ + public function userGetsVersionMetadataOfFile(string $user, string $file):void { + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $file); + Assert::assertNotNull($fileId, __METHOD__ . " fileid of file $file user $user not found (the file may not exist)"); + $body = ' + + + + + + '; + $response = $this->featureContext->makeDavRequest( + $user, + "PROPFIND", + $this->getVersionsPathForFileId($fileId), + null, + $body, + null, + '2' + ); + $this->featureContext->setResponse($response, $user); + } + + /** + * @When user :user restores version index :versionIndex of file :path using the WebDAV API + * @Given user :user has restored version index :versionIndex of file :path + * + * @param string $user + * @param int $versionIndex + * @param string $path + * + * @return void + * @throws Exception + */ + public function userRestoresVersionIndexOfFile(string $user, int $versionIndex, string $path):void { + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $path); + Assert::assertNotNull($fileId, __METHOD__ . " fileid of file $path user $user not found (the file may not exist)"); + $responseXml = $this->listVersionFolder($user, $fileId, 1); + $xmlPart = $responseXml->xpath("//d:response/d:href"); + //restoring the version only works with DAV path v2 + $destinationUrl = $this->featureContext->getBaseUrl() . "/" . + WebDavHelper::getDavPath($user, 2) . \trim($path, "/"); + $fullUrl = $this->featureContext->getBaseUrlWithoutPath() . + $xmlPart[$versionIndex]; + $response = HttpRequestHelper::sendRequest( + $fullUrl, + $this->featureContext->getStepLineRef(), + 'COPY', + $user, + $this->featureContext->getPasswordForUser($user), + ['Destination' => $destinationUrl] + ); + $this->featureContext->setResponse($response, $user); + } + + /** + * @Then the version folder of file :path for user :user should contain :count element(s) + * + * @param string $path + * @param string $user + * @param int $count + * + * @return void + * @throws Exception + */ + public function theVersionFolderOfFileShouldContainElements( + string $path, + string $user, + int $count + ):void { + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $path); + Assert::assertNotNull($fileId, __METHOD__ . " file $path user $user not found (the file may not exist)"); + $this->theVersionFolderOfFileIdShouldContainElements($fileId, $user, $count); + } + + /** + * @Then the version folder of fileId :fileId for user :user should contain :count element(s) + * + * @param string $fileId + * @param string $user + * @param int $count + * + * @return void + * @throws Exception + */ + public function theVersionFolderOfFileIdShouldContainElements( + string $fileId, + string $user, + int $count + ):void { + $responseXml = $this->listVersionFolder($user, $fileId, 1); + $xmlPart = $responseXml->xpath("//d:prop/d:getetag"); + Assert::assertEquals( + $count, + \count($xmlPart) - 1, + "could not find $count version element(s) in \n" . $responseXml->asXML() + ); + } + + /** + * @Then the content length of file :path with version index :index for user :user in versions folder should be :length + * + * @param string $path + * @param int $index + * @param string $user + * @param int $length + * + * @return void + * @throws Exception + */ + public function theContentLengthOfFileForUserInVersionsFolderIs( + string $path, + int $index, + string $user, + int $length + ):void { + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $path); + Assert::assertNotNull($fileId, __METHOD__ . " fileid of file $path user $user not found (the file may not exist)"); + $responseXml = $this->listVersionFolder( + $user, + $fileId, + 1, + ['getcontentlength'] + ); + $xmlPart = $responseXml->xpath("//d:prop/d:getcontentlength"); + Assert::assertEquals( + $length, + (int) $xmlPart[$index], + "The content length of file {$path} with version {$index} for user {$user} was + expected to be {$length} but the actual content length is {$xmlPart[$index]}" + ); + } + + /** + * @Then /^as (?:users|user) "([^"]*)" the authors of the versions of file "([^"]*)" should be:$/ + * + * @param string $users comma-separated list of usernames + * @param string $filename + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function asUsersAuthorsOfVersionsOfFileShouldBe( + string $users, + string $filename, + TableNode $table + ): void { + $this->featureContext->verifyTableNodeColumns( + $table, + ['index', 'author'] + ); + $requiredVersionMetadata = $table->getHash(); + $usersArray = \explode(",", $users); + foreach ($usersArray as $username) { + $actualUsername = $this->featureContext->getActualUsername($username); + $this->userGetsVersionMetadataOfFile($actualUsername, $filename); + foreach ($requiredVersionMetadata as $versionMetadata) { + $this->featureContext->theAuthorOfEditedVersionFile( + $versionMetadata['index'], + $versionMetadata['author'] + ); + } + } + } + + /** + * @When user :user downloads the version of file :path with the index :index + * + * @param string $user + * @param string $path + * @param string $index + * + * @return void + * @throws Exception + */ + public function downloadVersion(string $user, string $path, string $index):void { + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $path); + Assert::assertNotNull($fileId, __METHOD__ . " fileid of file $path user $user not found (the file may not exist)"); + $index = (int)$index; + $responseXml = $this->listVersionFolder($user, $fileId, 1); + $xmlPart = $responseXml->xpath("//d:response/d:href"); + if (!isset($xmlPart[$index])) { + Assert::fail( + 'could not find version of path "' . $path . '" with index "' . $index . '"' + ); + } + // the href already contains the path + $url = WebDavHelper::sanitizeUrl( + $this->featureContext->getBaseUrlWithoutPath() . $xmlPart[$index] + ); + $response = HttpRequestHelper::get( + $url, + $this->featureContext->getStepLineRef(), + $user, + $this->featureContext->getPasswordForUser($user) + ); + $this->featureContext->setResponse($response, $user); + } + + /** + * @Then /^the content of version index "([^"]*)" of file "([^"]*)" for user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $index + * @param string $path + * @param string $user + * @param string $content + * + * @return void + * @throws Exception + */ + public function theContentOfVersionIndexOfFileForUserShouldBe( + string $index, + string $path, + string $user, + string $content + ): void { + $this->downloadVersion($user, $path, $index); + $this->featureContext->theHTTPStatusCodeShouldBe("200"); + $this->featureContext->downloadedContentShouldBe($content); + } + + /** + * @When /^user "([^"]*)" retrieves the meta information of (file|fileId) "([^"]*)" using the meta API$/ + * + * @param string $user + * @param string $fileOrFileId + * @param string $path + * + * @return void + */ + public function userGetMetaInfo(string $user, string $fileOrFileId, string $path):void { + $user = $this->featureContext->getActualUsername($user); + $baseUrl = $this->featureContext->getBaseUrl(); + $password = $this->featureContext->getPasswordForUser($user); + + if ($fileOrFileId === "file") { + $fileId = $this->featureContext->getFileIdForPath($user, $path); + $metaPath = "/meta/$fileId/"; + } else { + $metaPath = "/meta/$path/"; + } + + $body = ' + + + + + '; + + $response = WebDavHelper::makeDavRequest( + $baseUrl, + $user, + $password, + "PROPFIND", + $metaPath, + ['Content-Type' => 'text/xml','Depth' => '0'], + $this->featureContext->getStepLineRef(), + $body, + $this->featureContext->getDavPathVersion(), + null + ); + $this->featureContext->setResponse($response); + $responseXml = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $this->featureContext->setResponseXmlObject($responseXml); + } + + /** + * returns the result parsed into an SimpleXMLElement + * with an registered namespace with 'd' as prefix and 'DAV:' as namespace + * + * @param string $user + * @param string $fileId + * @param int $folderDepth + * @param string[]|null $properties + * + * @return SimpleXMLElement + * @throws Exception + */ + public function listVersionFolder( + string $user, + string $fileId, + int $folderDepth, + ?array $properties = null + ):SimpleXMLElement { + if (!$properties) { + $properties = [ + 'getetag' + ]; + } + $user = $this->featureContext->getActualUsername($user); + $password = $this->featureContext->getPasswordForUser($user); + $response = WebDavHelper::propfind( + $this->featureContext->getBaseUrl(), + $user, + $password, + $this->getVersionsPathForFileId($fileId), + $properties, + $this->featureContext->getStepLineRef(), + (string) $folderDepth, + "versions" + ); + return HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/OCSContext.php b/tests/acceptance/features/bootstrap/OCSContext.php new file mode 100644 index 000000000..75f7ea6a8 --- /dev/null +++ b/tests/acceptance/features/bootstrap/OCSContext.php @@ -0,0 +1,1011 @@ + + * @copyright Copyright (c) 2019, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\Assert; +use TestHelpers\OcsApiHelper; +use TestHelpers\TranslationHelper; + +require_once 'bootstrap.php'; + +/** + * steps needed to send requests to the OCS API + */ +class OCSContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * @When /^the user sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/ + * + * @param string $verb + * @param string $url + * + * @return void + */ + public function theUserSendsToOcsApiEndpoint(string $verb, string $url):void { + $this->theUserSendsToOcsApiEndpointWithBody($verb, $url, null); + } + + /** + * @Given /^the user has sent HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/ + * + * @param string $verb + * @param string $url + * + * @return void + */ + public function theUserHasSentToOcsApiEndpoint(string $verb, string $url):void { + $this->theUserSendsToOcsApiEndpointWithBody($verb, $url, null); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/ + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" using password "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param string|null $password + * + * @return void + */ + public function userSendsToOcsApiEndpoint(string $user, string $verb, string $url, ?string $password = null):void { + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $verb, + $url, + null, + $password + ); + } + + /** + * @Given /^user "([^"]*)" has sent HTTP method "([^"]*)" to API endpoint "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param string|null $password + * + * @return void + */ + public function userHasSentToOcsApiEndpoint(string $user, string $verb, string $url, ?string $password = null):void { + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $verb, + $url, + null, + $password + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $user + * @param string $verb + * @param string $url + * @param TableNode|null $body + * @param string|null $password + * @param array|null $headers + * + * @return void + */ + public function userSendsHTTPMethodToOcsApiEndpointWithBody( + string $user, + string $verb, + string $url, + ?TableNode $body = null, + ?string $password = null, + ?array $headers = null + ):void { + /** + * array of the data to be sent in the body. + * contains $body data converted to an array + * + * @var array $bodyArray + */ + $bodyArray = []; + if ($body instanceof TableNode) { + $bodyArray = $body->getRowsHash(); + } elseif ($body !== null && \is_array($body)) { + $bodyArray = $body; + } + + if ($user !== 'UNAUTHORIZED_USER') { + if ($password === null) { + $password = $this->featureContext->getPasswordForUser($user); + } + $user = $this->featureContext->getActualUsername($user); + } else { + $user = null; + $password = null; + } + $response = OcsApiHelper::sendRequest( + $this->featureContext->getBaseUrl(), + $user, + $password, + $verb, + $url, + $this->featureContext->getStepLineRef(), + $bodyArray, + $this->featureContext->getOcsApiVersion(), + $headers + ); + $this->featureContext->setResponse($response); + } + + /** + * @param string $verb + * @param string $url + * @param TableNode|null $body + * + * @return void + */ + public function adminSendsHttpMethodToOcsApiEndpointWithBody( + string $verb, + string $url, + ?TableNode $body + ):void { + $admin = $this->featureContext->getAdminUsername(); + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $admin, + $verb, + $url, + $body + ); + } + + /** + * @param string $verb + * @param string $url + * @param TableNode|null $body + * + * @return void + */ + public function theUserSendsToOcsApiEndpointWithBody(string $verb, string $url, ?TableNode $body = null):void { + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $this->featureContext->getCurrentUser(), + $verb, + $url, + $body + ); + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with body$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param TableNode|null $body + * @param string|null $password + * + * @return void + */ + public function userSendHTTPMethodToOcsApiEndpointWithBody( + string $user, + string $verb, + string $url, + ?TableNode $body = null, + ?string $password = null + ):void { + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $verb, + $url, + $body, + $password + ); + } + + /** + * @Given /^user "([^"]*)" has sent HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with body$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param TableNode|null $body + * @param string $password + * + * @return void + */ + public function userHasSentHTTPMethodToOcsApiEndpointWithBody( + string $user, + string $verb, + string $url, + ?TableNode $body = null, + ?string $password = null + ):void { + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $verb, + $url, + $body, + $password + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the administrator sends HTTP method :verb to OCS API endpoint :url + * @When the administrator sends HTTP method :verb to OCS API endpoint :url using password :password + * + * @param string $verb + * @param string $url + * @param string|null $password + * + * @return void + */ + public function theAdministratorSendsHttpMethodToOcsApiEndpoint( + string $verb, + string $url, + ?string $password = null + ):void { + $admin = $this->featureContext->getAdminUsername(); + $this->userSendsToOcsApiEndpoint($admin, $verb, $url, $password); + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param TableNode $headersTable + * + * @return void + * @throws Exception + */ + public function userSendsToOcsApiEndpointWithHeaders( + string $user, + string $verb, + string $url, + TableNode $headersTable + ):void { + $user = $this->featureContext->getActualUsername($user); + $password = $this->featureContext->getPasswordForUser($user); + $this->userSendsToOcsApiEndpointWithHeadersAndPassword( + $user, + $verb, + $url, + $password, + $headersTable + ); + } + + /** + * @When /^the administrator sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers$/ + * + * @param string $verb + * @param string $url + * @param TableNode $headersTable + * + * @return void + * @throws Exception + */ + public function administratorSendsToOcsApiEndpointWithHeaders( + string $verb, + string $url, + TableNode $headersTable + ):void { + $this->userSendsToOcsApiEndpointWithHeaders( + $this->featureContext->getAdminUsername(), + $verb, + $url, + $headersTable + ); + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers using password "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param string $password + * @param TableNode $headersTable + * + * @return void + * @throws Exception + */ + public function userSendsToOcsApiEndpointWithHeadersAndPassword( + string $user, + string $verb, + string $url, + string $password, + TableNode $headersTable + ):void { + $this->featureContext->verifyTableNodeColumns( + $headersTable, + ['header', 'value'] + ); + $user = $this->featureContext->getActualUsername($user); + $headers = []; + foreach ($headersTable as $row) { + $headers[$row['header']] = $row ['value']; + } + + $response = OcsApiHelper::sendRequest( + $this->featureContext->getBaseUrl(), + $user, + $password, + $verb, + $url, + $this->featureContext->getStepLineRef(), + [], + $this->featureContext->getOcsApiVersion(), + $headers + ); + $this->featureContext->setResponse($response); + } + + /** + * @When /^the administrator sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers using password "([^"]*)"$/ + * + * @param string $verb + * @param string $url + * @param string $password + * @param TableNode $headersTable + * + * @return void + * @throws Exception + */ + public function administratorSendsToOcsApiEndpointWithHeadersAndPassword( + string $verb, + string $url, + string $password, + TableNode $headersTable + ):void { + $this->userSendsToOcsApiEndpointWithHeadersAndPassword( + $this->featureContext->getAdminUsername(), + $verb, + $url, + $password, + $headersTable + ); + } + + /** + * @When the administrator sends HTTP method :verb to OCS API endpoint :url with body + * + * @param string $verb + * @param string $url + * @param TableNode|null $body + * + * @return void + */ + public function theAdministratorSendsHttpMethodToOcsApiEndpointWithBody( + string $verb, + string $url, + ?TableNode $body + ):void { + $this->adminSendsHttpMethodToOcsApiEndpointWithBody( + $verb, + $url, + $body + ); + } + + /** + * @Given the administrator has sent HTTP method :verb to OCS API endpoint :url with body + * + * @param string $verb + * @param string $url + * @param TableNode|null $body + * + * @return void + */ + public function theAdministratorHasSentHttpMethodToOcsApiEndpointWithBody( + string $verb, + string $url, + ?TableNode $body + ):void { + $this->adminSendsHttpMethodToOcsApiEndpointWithBody( + $verb, + $url, + $body + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When /^the user sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with body$/ + * + * @param string $verb + * @param string $url + * @param TableNode $body + * + * @return void + */ + public function theUserSendsHTTPMethodToOcsApiEndpointWithBody(string $verb, string $url, TableNode $body):void { + $this->theUserSendsHTTPMethodToOcsApiEndpointWithBody( + $verb, + $url, + $body + ); + } + + /** + * @Given /^the user has sent HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with body$/ + * + * @param string $verb + * @param string $url + * @param TableNode $body + * + * @return void + */ + public function theUserHasSentHTTPMethodToOcsApiEndpointWithBody(string $verb, string $url, TableNode $body):void { + $this->theUserSendsHTTPMethodToOcsApiEndpointWithBody( + $verb, + $url, + $body + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When the administrator sends HTTP method :verb to OCS API endpoint :url with body using password :password + * + * @param string $verb + * @param string $url + * @param string $password + * @param TableNode $body + * + * @return void + */ + public function theAdministratorSendsHttpMethodToOcsApiWithBodyAndPassword( + string $verb, + string $url, + string $password, + TableNode $body + ):void { + $admin = $this->featureContext->getAdminUsername(); + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $admin, + $verb, + $url, + $body, + $password + ); + } + + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with body using password "([^"]*)"$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param string $password + * @param TableNode $body + * + * @return void + */ + public function userSendsHTTPMethodToOcsApiEndpointWithBodyAndPassword( + string $user, + string $verb, + string $url, + string $password, + TableNode $body + ):void { + $this->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $verb, + $url, + $body, + $password + ); + } + + /** + * @When user :user requests these endpoints with :method using password :password about user :ofUser + * + * @param string $user + * @param string $method + * @param string $password + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userSendsRequestToTheseEndpointsWithOutBodyUsingPassword( + string $user, + string $method, + string $password, + string $ofUser, + TableNode $table + ):void { + $this->userSendsRequestToTheseEndpointsWithBodyUsingPassword( + $user, + $method, + null, + $password, + $ofUser, + $table + ); + } + + /** + * @When user :user requests these endpoints with :method including body :body using password :password about user :ofUser + * + * @param string|null $user + * @param string|null $method + * @param string|null $body + * @param string|null $password + * @param string|null $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userSendsRequestToTheseEndpointsWithBodyUsingPassword( + ?string $user, + ?string $method, + ?string $body, + ?string $password, + ?string $ofUser, + TableNode $table + ):void { + $user = $this->featureContext->getActualUsername($user); + $ofUser = $this->featureContext->getActualUsername($ofUser); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['destination']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $ofUser + ); + $header = []; + if (isset($row['destination'])) { + $header['Destination'] = $this->featureContext->substituteInLineCodes( + $this->featureContext->getBaseUrl() . $row['destination'], + $ofUser + ); + } + $this->featureContext->authContext->userRequestsURLWithUsingBasicAuth( + $user, + $row['endpoint'], + $method, + $password, + $body, + $header + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When user :user requests these endpoints with :method including body :body about user :ofUser + * + * @param string $user + * @param string $method + * @param string $body + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userSendsRequestToTheseEndpointsWithBody(string $user, string $method, string $body, string $ofUser, TableNode $table):void { + $header = []; + if ($method === 'MOVE' || $method === 'COPY') { + $header['Destination'] = '/path/to/destination'; + } + + $this->sendRequestToTheseEndpointsAsNormalUser( + $user, + $method, + $ofUser, + $table, + $body, + null, + $header, + ); + } + + /** + * @When user :user requests these endpoints with :method about user :ofUser + * + * @param string $user + * @param string $method + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userSendsRequestToTheseEndpointsWithOutBody(string $user, string $method, string $ofUser, TableNode $table):void { + $header = []; + if ($method === 'MOVE' || $method === 'COPY') { + $header['Destination'] = '/path/to/destination'; + } + + $this->sendRequestToTheseEndpointsAsNormalUser( + $user, + $method, + $ofUser, + $table, + null, + null, + $header, + ); + } + + /** + * @When /^user "([^"]*)" requests these endpoints with "([^"]*)" to (?:get|set) property "([^"]*)" about user "([^"]*)"$/ + * + * @param string $user + * @param string $method + * @param string $property + * @param string $ofUser + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userSendsRequestToTheseEndpointsWithProperty(string $user, string $method, string $property, string $ofUser, TableNode $table):void { + $this->sendRequestToTheseEndpointsAsNormalUser( + $user, + $method, + $ofUser, + $table, + null, + $property + ); + } + + /** + * @param string $user + * @param string $method + * @param string $ofUser + * @param TableNode $table + * @param string|null $body + * @param string|null $property + * @param Array|null $header + * + * @return void + * @throws Exception + */ + public function sendRequestToTheseEndpointsAsNormalUser( + string $user, + string $method, + string $ofUser, + TableNode $table, + ?string $body = null, + ?string $property = null, + ?array $header = null + ):void { + $user = $this->featureContext->getActualUsername($user); + $ofUser = $this->featureContext->getActualUsername($ofUser); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + if (!$body && $property) { + $body = $this->featureContext->getBodyForOCSRequest($method, $property); + } + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $ofUser + ); + $this->featureContext->authContext->userRequestsURLWithUsingBasicAuth( + $user, + $row['endpoint'], + $method, + $this->featureContext->getPasswordForUser($user), + $body, + $header + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When user :asUser requests these endpoints with :method including body :body using the password of user :user + * + * @param string $asUser + * @param string $method + * @param string|null $body + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsTheseEndpointsWithBodyUsingThePasswordOfUser(string $asUser, string $method, ?string $body, string $user, TableNode $table):void { + $asUser = $this->featureContext->getActualUsername($asUser); + $userRenamed = $this->featureContext->getActualUsername($user); + $this->featureContext->verifyTableNodeColumns($table, ['endpoint']); + $this->featureContext->emptyLastHTTPStatusCodesArray(); + $this->featureContext->emptyLastOCSStatusCodesArray(); + foreach ($table->getHash() as $row) { + $row['endpoint'] = $this->featureContext->substituteInLineCodes( + $row['endpoint'], + $userRenamed + ); + $this->featureContext->authContext->userRequestsURLWithUsingBasicAuth( + $asUser, + $row['endpoint'], + $method, + $this->featureContext->getPasswordForUser($user), + $body + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @When user :asUser requests these endpoints with :method using the password of user :user + * + * @param string $asUser + * @param string $method + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRequestsTheseEndpointsWithoutBodyUsingThePasswordOfUser(string $asUser, string $method, string $user, TableNode $table):void { + $this->userRequestsTheseEndpointsWithBodyUsingThePasswordOfUser( + $asUser, + $method, + null, + $user, + $table + ); + } + + /** + * @Then /^the OCS status code should be "([^"]*)"$/ + * + * @param string $statusCode + * @param string $message + * + * @return void + * @throws Exception + */ + public function theOCSStatusCodeShouldBe(string $statusCode, $message = ""):void { + $statusCodes = explode(",", $statusCode); + $responseStatusCode = $this->getOCSResponseStatusCode( + $this->featureContext->getResponse() + ); + if (\is_array($statusCodes)) { + if ($message === "") { + $message = "OCS status code is not any of the expected values " . \implode(",", $statusCodes) . " got " . $responseStatusCode; + } + Assert::assertContainsEquals( + $responseStatusCode, + $statusCodes, + $message + ); + $this->featureContext->emptyLastOCSStatusCodesArray(); + } else { + if ($message === "") { + $message = "OCS status code is not the expected value " . $statusCodes . " got " . $responseStatusCode; + } + + Assert::assertEquals( + $statusCodes, + $responseStatusCode, + $message + ); + } + } + + /** + * @Then /^the OCS status code should be "([^"]*)" or "([^"]*)"$/ + * + * @param string $statusCode1 + * @param string $statusCode2 + * + * @return void + * @throws Exception + */ + public function theOcsStatusCodeShouldBeOr(string $statusCode1, string $statusCode2):void { + $this->theOCSStatusCodeShouldBe( + $statusCode1 . "," . $statusCode2 + ); + } + + /** + * Check the text in an OCS status message + * + * @Then /^the OCS status message should be "([^"]*)"$/ + * @Then /^the OCS status message should be "([^"]*)" in language "([^"]*)"$/ + * + * @param string $statusMessage + * @param string|null $language + * + * @return void + */ + public function theOCSStatusMessageShouldBe(string $statusMessage, ?string $language = null):void { + $language = TranslationHelper::getLanguage($language); + $statusMessage = $this->getActualStatusMessage($statusMessage, $language); + + Assert::assertEquals( + $statusMessage, + $this->getOCSResponseStatusMessage( + $this->featureContext->getResponse() + ), + 'Unexpected OCS status message :"' . $this->getOCSResponseStatusMessage( + $this->featureContext->getResponse() + ) . '" in response' + ); + } + + /** + * Check the text in an OCS status message + * + * @Then /^the OCS status message about user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $user + * @param string $statusMessage + * + * @return void + */ + public function theOCSStatusMessageAboutUserShouldBe(string $user, string $statusMessage):void { + $user = \strtolower($this->featureContext->getActualUsername($user)); + $statusMessage = $this->featureContext->substituteInLineCodes( + $statusMessage, + $user + ); + Assert::assertEquals( + $statusMessage, + $this->getOCSResponseStatusMessage( + $this->featureContext->getResponse() + ), + 'Unexpected OCS status message :"' . $this->getOCSResponseStatusMessage( + $this->featureContext->getResponse() + ) . '" in response' + ); + } + + /** + * Check the text in an OCS status message. + * Use this step form if the expected text contains double quotes, + * single quotes and other content that theOCSStatusMessageShouldBe() + * cannot handle. + * + * After the step, write the expected text in PyString form like: + * + * """ + * File "abc.txt" can't be shared due to reason "xyz" + * """ + * + * @Then /^the OCS status message should be:$/ + * + * @param PyStringNode $statusMessage + * + * @return void + */ + public function theOCSStatusMessageShouldBePyString( + PyStringNode $statusMessage + ):void { + Assert::assertEquals( + $statusMessage->getRaw(), + $this->getOCSResponseStatusMessage( + $this->featureContext->getResponse() + ), + 'Unexpected OCS status message: "' . $this->getOCSResponseStatusMessage( + $this->featureContext->getResponse() + ) . '" in response' + ); + } + + /** + * 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 + * @throws Exception + */ + public function getOCSResponseStatusCode(ResponseInterface $response):string { + $responseXml = $this->featureContext->getResponseXml($response, __METHOD__); + if (isset($responseXml->meta[0], $responseXml->meta[0]->statuscode)) { + return (string) $responseXml->meta[0]->statuscode; + } + throw new Exception( + "No OCS status code found in responseXml" + ); + } + + /** + * Parses the xml answer to get ocs response message which doesn't match with + * http one in v1 of the api. + * + * @param ResponseInterface $response + * + * @return string + */ + public function getOCSResponseStatusMessage(ResponseInterface $response):string { + return (string) $this->featureContext->getResponseXml($response, __METHOD__)->meta[0]->message; + } + + /** + * convert status message in the desired language + * + * @param string $statusMessage + * @param string|null $language + * + * @return string + */ + public function getActualStatusMessage(string $statusMessage, ?string $language = null):string { + if ($language !== null) { + $multiLingualMessage = \json_decode( + \file_get_contents(__DIR__ . "/../../fixtures/multiLanguageErrors.json"), + true + ); + + if (isset($multiLingualMessage[$statusMessage][$language])) { + $statusMessage = $multiLingualMessage[$statusMessage][$language]; + } + } + return $statusMessage; + } + + /** + * check if the HTTP status code and the OCS status code indicate that the request was successful + * this function is aware of the currently used OCS version + * + * @param string|null $message + * + * @return void + * @throws Exception + */ + public function assertOCSResponseIndicatesSuccess(?string $message = ""):void { + $this->featureContext->theHTTPStatusCodeShouldBe('200', $message); + if ($this->featureContext->getOcsApiVersion() === 1) { + $this->theOCSStatusCodeShouldBe('100', $message); + } else { + $this->theOCSStatusCodeShouldBe('200', $message); + } + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/OccContext.php b/tests/acceptance/features/bootstrap/OccContext.php new file mode 100644 index 000000000..53d62081c --- /dev/null +++ b/tests/acceptance/features/bootstrap/OccContext.php @@ -0,0 +1,3748 @@ + + * @copyright Copyright (c) 2019, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use GuzzleHttp\Exception\GuzzleException; +use PHPUnit\Framework\Assert; +use TestHelpers\OcisHelper; +use TestHelpers\SetupHelper; +use Behat\Gherkin\Node\PyStringNode; + +require_once 'bootstrap.php'; + +/** + * Occ context for test steps that test occ commands + */ +class OccContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * + * @var ImportedCertificates + */ + private $importedCertificates = []; + + /** + * + * @var RemovedCertificates + */ + private $removedCertificates = []; + + /** + * @var string lastDeletedJobId + */ + private $lastDeletedJobId; + + /** + * The code to manage dav.enable.tech_preview was used in 10.4/10.3 + * The use of the steps to enable/disable it has been removed from the + * feature files. But the infrastructure has been left here, as a similar + * thing might likely happen in the future. + * + * @var boolean + */ + private $doTechPreview = false; + + /** + * @var boolean techPreviewEnabled + */ + private $techPreviewEnabled = false; + + /** + * @var string initialTechPreviewStatus + */ + private $initialTechPreviewStatus; + + /** + * @return boolean + */ + public function isTechPreviewEnabled():bool { + return $this->techPreviewEnabled; + } + + /** + * @return boolean + * @throws Exception + */ + public function enableDAVTechPreview():bool { + if ($this->doTechPreview) { + if (!$this->isTechPreviewEnabled()) { + $this->addSystemConfigKeyUsingTheOccCommand( + "dav.enable.tech_preview", + "true", + "boolean" + ); + $this->techPreviewEnabled = true; + return true; + } + } + return false; + } + + /** + * @return boolean true if delete-system-config-key was done + * @throws Exception + */ + public function disableDAVTechPreview():bool { + if ($this->doTechPreview) { + $this->deleteSystemConfigKeyUsingTheOccCommand( + "dav.enable.tech_preview" + ); + $this->techPreviewEnabled = false; + return true; + } + return false; + } + + /** + * @param string $cmd + * + * @return void + * @throws Exception + */ + public function invokingTheCommand(string $cmd):void { + $this->featureContext->setOccLastCode( + $this->featureContext->runOcc([$cmd]) + ); + } + + /** + * @param string $path + * + * @return void + * @throws Exception + */ + public function importSecurityCertificateFromFileInTemporaryStorage(string $path):void { + $this->invokingTheCommand("security:certificates:import " . TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/$path"); + if ($this->featureContext->getExitStatusCodeOfOccCommand() === 0) { + $pathComponents = \explode("/", $path); + $certificate = \end($pathComponents); + \array_push($this->importedCertificates, $certificate); + } + } + + /** + * @param string $cmd + * @param string $envVariableName + * @param string $envVariableValue + * + * @return void + * @throws Exception + */ + public function invokingTheCommandWithEnvVariable( + string $cmd, + string $envVariableName, + string $envVariableValue + ):int { + $args = [$cmd]; + return( + $this->featureContext->runOccWithEnvVariables( + $args, + [$envVariableName => $envVariableValue] + ) + ); + } + + /** + * @param string $mode + * + * @return void + * @throws Exception + */ + public function changeBackgroundJobsModeUsingTheOccCommand(string $mode):void { + $this->invokingTheCommand("background:$mode"); + } + + /** + * @param string $mountPoint + * @param boolean $setting + * + * @return void + * @throws Exception + */ + public function setExtStorageReadOnlyUsingTheOccCommand(string $mountPoint, bool $setting = true):void { + $command = "files_external:option"; + + $mountId = $this->featureContext->getStorageId($mountPoint); + + $key = "read_only"; + + if ($setting) { + $value = "1"; + } else { + $value = "0"; + } + + $this->invokingTheCommand( + "$command $mountId $key $value" + ); + } + + /** + * @param string $mountPoint + * @param boolean $enable + * + * @return void + * @throws Exception + */ + public function setExtStorageSharingUsingTheOccCommand(string $mountPoint, bool $enable = true):void { + $command = "files_external:option"; + + $mountId = $this->featureContext->getStorageId($mountPoint); + + $key = "enable_sharing"; + + $value = $enable ? "true" : "false"; + + $this->invokingTheCommand( + "$command $mountId $key $value" + ); + } + + /** + * @param string $mountPoint + * @param string $setting "never" (switch it off) otherwise "Once every direct access" + * + * @return void + * @throws Exception + */ + public function setExtStorageCheckChangesUsingTheOccCommand(string $mountPoint, string $setting):void { + $command = "files_external:option"; + + $mountId = $this->featureContext->getStorageId($mountPoint); + + $key = "filesystem_check_changes"; + + if ($setting === "never") { + $value = "0"; + } else { + $value = "1"; + } + + $this->invokingTheCommand( + "$command $mountId $key $value" + ); + } + + /** + * @return void + * @throws Exception + */ + public function scanFileSystemForAllUsersUsingTheOccCommand():void { + $this->invokingTheCommand( + "files:scan --all" + ); + } + + /** + * @param string $user + * + * @return void + * @throws Exception + */ + public function scanFileSystemForAUserUsingTheOccCommand(string $user):void { + $user = $this->featureContext->getActualUsername($user); + $this->invokingTheCommand( + "files:scan $user" + ); + } + + /** + * @param string $path + * @param string|null $user + * + * @return void + * @throws Exception + */ + public function scanFileSystemPathUsingTheOccCommand(string $path, ?string $user = null):void { + $path = $this->featureContext->substituteInLineCodes( + $path, + $user + ); + $this->invokingTheCommand( + "files:scan --path='$path'" + ); + } + + /** + * @param string $group + * + * @return void + * @throws Exception + */ + public function scanFileSystemForAGroupUsingTheOccCommand(string $group):void { + $this->invokingTheCommand( + "files:scan --group=$group" + ); + } + + /** + * @param string $groups + * + * @return void + * @throws Exception + */ + public function scanFileSystemForGroupsUsingTheOccCommand(string $groups):void { + $this->invokingTheCommand( + "files:scan --groups=$groups" + ); + } + + /** + * @param string $mount + * + * @return void + */ + public function createLocalStorageMountUsingTheOccCommand(string $mount):void { + $result = SetupHelper::createLocalStorageMount( + $mount, + $this->featureContext->getStepLineRef() + ); + $storageId = $result['storageId']; + if (!is_numeric($storageId)) { + throw new Exception( + __METHOD__ . " storageId '$storageId' is not numeric" + ); + } + $this->featureContext->setResultOfOccCommand($result); + $this->featureContext->addStorageId($mount, (int) $storageId); + } + + /** + * @param string $key + * @param string $value + * @param string $app + * + * @return void + * @throws Exception + */ + public function addConfigKeyWithValueInAppUsingTheOccCommand(string $key, string $value, string $app):void { + $this->invokingTheCommand( + "config:app:set --value ${value} ${app} ${key}" + ); + } + + /** + * @param string $key + * @param string $app + * + * @return void + * @throws Exception + */ + public function deleteConfigKeyOfAppUsingTheOccCommand(string $key, string $app):void { + $this->invokingTheCommand( + "config:app:delete ${app} ${key}" + ); + } + + /** + * @param string $key + * @param string $value + * @param string $type + * + * @return void + * @throws Exception + */ + public function addSystemConfigKeyUsingTheOccCommand( + string $key, + string $value, + string $type = "string" + ):void { + $this->invokingTheCommand( + "config:system:set --value '${value}' --type ${type} ${key}" + ); + } + + /** + * @param string $key + * + * @return void + * @throws Exception + */ + public function deleteSystemConfigKeyUsingTheOccCommand(string $key):void { + $this->invokingTheCommand( + "config:system:delete ${key}" + ); + } + + /** + * + * @return void + * @throws Exception + */ + public function cleanOrphanedRemoteStoragesUsingOccCommand():void { + $this->invokingTheCommand( + "sharing:cleanup-remote-storages" + ); + } + + /** + * @param string $user + * + * @return void + * @throws Exception + */ + public function emptyTrashBinOfUserUsingOccCommand(string $user):void { + $user = $this->featureContext->getActualUsername($user); + $this->invokingTheCommand( + "trashbin:cleanup $user" + ); + } + + /** + * Create a calendar for given user with given calendar name + * + * @param string $user + * @param string $calendarName + * + * @return void + * @throws Exception + */ + public function createCalendarForUserUsingOccCommand(string $user, string $calendarName):void { + $this->invokingTheCommand("dav:create-calendar $user $calendarName"); + } + + /** + * Create an address book for given user with given address book name + * + * @param string $user + * @param string $addressBookName + * + * @return void + * @throws Exception + */ + public function createAnAddressBookForUserUsingOccCommand(string $user, string $addressBookName):void { + $this->invokingTheCommand("dav:create-addressbook $user $addressBookName"); + } + + /** + * @return void + * @throws Exception + */ + public function getAllJobsInBackgroundQueueUsingOccCommand():void { + $this->invokingTheCommand( + "background:queue:status" + ); + } + + /** + * @param string $user + * + * @return void + * @throws Exception + */ + public function deleteAllVersionsForUserUsingOccCommand(string $user = ""): void { + $this->invokingTheCommand( + "versions:cleanup $user" + ); + } + + /** + * @param string $users space-separated usernames. E.g. "Alice Brian" (without ) + * + * @return void + * @throws Exception + */ + public function deleteAllVersionsForMultipleUsersUsingOccCommand(string $users): void { + $this->deleteAllVersionsForUserUsingOccCommand($users); + } + + /** + * @return void + * @throws Exception + */ + public function deleteAllVersionsForAllUsersUsingOccCommand(): void { + $this->deleteAllVersionsForUserUsingOccCommand(); + } + + /** + * @param string $user + * + * @return void + * @throws Exception + */ + public function deleteExpiredVersionsForUserUsingOccCommand(string $user = ""): void { + $this->invokingTheCommand( + "versions:expire $user" + ); + } + + /** + * @param string $users space-separated usernames. E.g. "Alice Brian" (without ) + * + * @return void + * @throws Exception + */ + public function deleteExpiredVersionsForMultipleUsersUsingOccCommand(string $users): void { + $this->deleteExpiredVersionsForUserUsingOccCommand($users); + } + + /** + * @return void + * @throws Exception + */ + public function deleteExpiredVersionsForAllUsersUsingOccCommand(): void { + $this->deleteExpiredVersionsForUserUsingOccCommand(); + } + + /** + * @param string $job + * + * @return void + * @throws Exception + */ + public function deleteLastBackgroundJobUsingTheOccCommand(string $job):void { + $match = $this->getLastJobIdForJob($job); + if ($match === false) { + throw new Exception("Couldn't find jobId for given job: $job"); + } + $this->invokingTheCommand( + "background:queue:delete $match" + ); + $this->lastDeletedJobId = $match; + } + + /** + * List created local storage mount + * + * @return void + * @throws Exception + */ + public function listLocalStorageMount():void { + $this->invokingTheCommand('files_external:list --output=json'); + } + + /** + * @When the administrator lists all local storage mount points using the occ command + * + * List created local storage mount with --short + * + * @return void + * @throws Exception + */ + public function listLocalStorageMountShort():void { + $this->invokingTheCommand('files_external:list --short --output=json'); + } + + /** + * List created local storage mount with --mount-options + * + * @return void + * @throws Exception + */ + public function listLocalStorageMountOptions():void { + $this->invokingTheCommand('files_external:list --mount-options --output=json'); + } + + /** + * List available backends + * + * @return void + * @throws Exception + */ + public function listAvailableBackends():void { + $this->invokingTheCommand('files_external:backends --output=json'); + } + + /** + * List available backends of type + * + * @param String $type + * + * @return void + * @throws Exception + */ + public function listAvailableBackendsOfType(string $type):void { + $this->invokingTheCommand('files_external:backends ' . $type . ' --output=json'); + } + + /** + * List backend of type + * + * @param String $type + * @param String $backend + * + * @return void + * @throws Exception + */ + public function listBackendOfType(string $type, string $backend):void { + $this->invokingTheCommand('files_external:backends ' . $type . ' ' . $backend . ' --output=json'); + } + + /** + * List created local storage mount with --show-password + * + * @return void + * @throws Exception + */ + public function listLocalStorageShowPassword():void { + $this->invokingTheCommand('files_external:list --show-password --output=json'); + } + + /** + * @When the administrator enables DAV tech_preview + * + * @return void + * @throws Exception + */ + public function theAdministratorEnablesDAVTechPreview():void { + $this->enableDAVTechPreview(); + } + + /** + * @Given the administrator has enabled DAV tech_preview + * + * @return void + * @throws Exception + */ + public function theAdministratorHasEnabledDAVTechPreview():void { + if ($this->enableDAVTechPreview()) { + $this->theCommandShouldHaveBeenSuccessful(); + } + } + + /** + * @When /^the administrator invokes occ command "([^"]*)"$/ + * + * @param string $cmd + * + * @return void + * @throws Exception + */ + public function theAdministratorInvokesOccCommand(string $cmd):void { + $this->invokingTheCommand($cmd); + } + + /** + * @When /^the administrator invokes occ command "([^"]*)" for user "([^"]*)"$/ + * + * @param string $cmd + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorInvokesOccCommandForUser(string $cmd, string $user):void { + $user = $this->featureContext->getActualUsername($user); + $cmd = $this->featureContext->substituteInLineCodes( + $cmd, + $user + ); + $this->invokingTheCommand($cmd); + } + + /** + * @Given /^the administrator has invoked occ command "([^"]*)"$/ + * + * @param string $cmd + * + * @return void + * @throws Exception + */ + public function theAdministratorHasInvokedOccCommand(string $cmd):void { + $this->invokingTheCommand($cmd); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @Given the administrator has selected master key encryption type using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSelectedMasterKeyEncryptionTypeUsingTheOccCommand():void { + $this->featureContext->runOcc(['encryption:select-encryption-type', "masterkey --yes"]); + } + + /** + * @When the administrator imports security certificate from file :filename in temporary storage on the system under test + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function theAdministratorImportsSecurityCertificateFromFile(string $filename):void { + $this->importSecurityCertificateFromFileInTemporaryStorage($filename); + } + + /** + * @Given the administrator has imported security certificate from file :filename in temporary storage on the system under test + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function theAdministratorHasImportedSecurityCertificateFromFile(string $filename):void { + $this->importSecurityCertificateFromFileInTemporaryStorage($filename); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator removes the security certificate :certificate + * + * @param string $certificate + * + * @return void + * @throws Exception + */ + public function theAdministratorRemovesTheSecurityCertificate(string $certificate):void { + $this->invokingTheCommand("security:certificates:remove " . $certificate); + \array_push($this->removedCertificates, $certificate); + } + + /** + * @When /^the administrator invokes occ command "([^"]*)" with environment variable "([^"]*)" set to "([^"]*)"$/ + * + * @param string $cmd + * @param string $envVariableName + * @param string $envVariableValue + * + * @return void + * @throws Exception + */ + public function theAdministratorInvokesOccCommandWithEnvironmentVariable( + string $cmd, + string $envVariableName, + string $envVariableValue + ):void { + $this->featureContext->setOccLastCode( + $this->invokingTheCommandWithEnvVariable( + $cmd, + $envVariableName, + $envVariableValue + ) + ); + } + + /** + * @Given /^the administrator has invoked occ command "([^"]*)" with environment variable "([^"]*)" set to "([^"]*)"$/ + * + * @param string $cmd + * @param string $envVariableName + * @param string $envVariableValue + * + * @return void + * @throws Exception + */ + public function theAdministratorHasInvokedOccCommandWithEnvironmentVariable( + string $cmd, + string $envVariableName, + string $envVariableValue + ):void { + $this->invokingTheCommandWithEnvVariable( + $cmd, + $envVariableName, + $envVariableValue + ); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator runs upgrade routines on local server using the occ command + * + * @return void + */ + public function theAdministratorRunsUpgradeRoutinesOnLocalServerUsingTheOccCommand():void { + \system("./occ upgrade", $status); + if ($status !== 0) { + // if the above command fails make sure to turn off maintenance mode + \system("./occ maintenance:mode --off"); + } + } + + /** + * @Given the administrator has decrypted everything + * + * @return void + * @throws Exception + */ + public function theAdministratorHasDecryptedEverything():void { + $this->theAdministratorRunsEncryptionDecryptAllUsingTheOccCommand(); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator disables encryption using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorDisablesEncryptionUsingTheOccCommand():void { + $this->invokingTheCommand("encryption:disable"); + } + + /** + * @When the administrator runs encryption decrypt all using the occ command + * + * @return void + */ + public function theAdministratorRunsEncryptionDecryptAllUsingTheOccCommand():void { + \system("./occ maintenance:singleuser --on"); + \system("./occ encryption:decrypt-all -c yes", $status); + + $this->featureContext->setResultOfOccCommand(["code" => $status, "stdOut" => "", "stdErr" => ""]); + \system("./occ maintenance:singleuser --off"); + } + + /** + * @return bool + */ + public function theOccCommandExitStatusWasSuccess():bool { + return ($this->featureContext->getExitStatusCodeOfOccCommand() === 0); + } + + /** + * @Then /^the command should have been successful$/ + * + * @return void + * @throws Exception + */ + public function theCommandShouldHaveBeenSuccessful():void { + $exceptions = $this->featureContext->findExceptions(); + $exitStatusCode = $this->featureContext->getExitStatusCodeOfOccCommand(); + if ($exitStatusCode !== 0) { + $msg = "The command was not successful, exit code was " . + $exitStatusCode . ".\n" . + "stdOut was: '" . + $this->featureContext->getStdOutOfOccCommand() . "'\n" . + "stdErr was: '" . + $this->featureContext->getStdErrOfOccCommand() . "'\n"; + if (!empty($exceptions)) { + $msg .= ' Exceptions: ' . \implode(', ', $exceptions); + } + if ($exitStatusCode === null) { + $msg = "The occ command did not run "; + } + throw new Exception($msg); + } elseif (!empty($exceptions)) { + $msg = 'The command was successful but triggered exceptions: ' + . \implode(', ', $exceptions); + throw new Exception($msg); + } + } + + /** + * @Then /^the command should have failed with exit code ([0-9]+)$/ + * + * @param int $exitCode + * + * @return void + * @throws Exception + */ + public function theCommandFailedWithExitCode(int $exitCode):void { + $exitStatusCode = $this->featureContext->getExitStatusCodeOfOccCommand(); + Assert::assertEquals( + (int) $exitCode, + $exitStatusCode, + "The command was expected to fail with exit code $exitCode but got {$exitStatusCode}" + ); + } + + /** + * @Then /^the command output should be the text ((?:'[^']*')|(?:"[^"]*"))$/ + * + * @param string $text + * + * @return void + * @throws Exception + */ + public function theCommandOutputShouldBeTheText(string $text):void { + $text = \trim($text, $text[0]); + $commandOutput = \trim($this->featureContext->getStdOutOfOccCommand()); + Assert::assertEquals( + $text, + $commandOutput, + "The command output did not match the expected text on stdout '$text'\n" . + "The command output on stdout was:\n" . + $commandOutput + ); + } + + /** + * @Then /^the command should have failed with exception text "([^"]*)"$/ + * + * @param string $exceptionText + * + * @return void + * @throws Exception + */ + public function theCommandFailedWithExceptionText(string $exceptionText):void { + $exceptions = $this->featureContext->findExceptions(); + Assert::assertNotEmpty( + $exceptions, + 'The command did not throw any exceptions' + ); + + Assert::assertContains( + $exceptionText, + $exceptions, + "The command did not throw any exception with the text '$exceptionText'" + ); + } + + /** + * @Then /^the command output should contain the text ((?:'[^']*')|(?:"[^"]*"))$/ + * + * @param string $text + * + * @return void + * @throws Exception + */ + public function theCommandOutputContainsTheText(string $text):void { + // The capturing group of the regex always includes the quotes at each + // end of the captured string, so trim them. + $text = \trim($text, $text[0]); + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $lines = SetupHelper::findLines( + $commandOutput, + $text + ); + Assert::assertGreaterThanOrEqual( + 1, + \count($lines), + "The command output did not contain the expected text on stdout '$text'\n" . + "The command output on stdout was:\n" . + $commandOutput + ); + } + + /** + * @Then /^the command output should contain the text ((?:'[^']*')|(?:"[^"]*")) about user "([^"]*)"$/ + * + * @param string $text + * @param string $user + * + * @return void + * @throws Exception + */ + public function theCommandOutputContainsTheTextAboutUser(string $text, string $user):void { + // The capturing group of the regex always includes the quotes at each + // end of the captured string, so trim them. + $text = \trim($text, $text[0]); + $text = $this->featureContext->substituteInLineCodes( + $text, + $user + ); + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $lines = SetupHelper::findLines( + $commandOutput, + $text + ); + Assert::assertGreaterThanOrEqual( + 1, + \count($lines), + "The command output did not contain the expected text on stdout '$text'\n" . + "The command output on stdout was:\n" . + $commandOutput + ); + } + + /** + * @Then /^the command error output should contain the text ((?:'[^']*')|(?:"[^"]*"))$/ + * + * @param string $text + * + * @return void + * @throws Exception + */ + public function theCommandErrorOutputContainsTheText(string $text):void { + // The capturing group of the regex always includes the quotes at each + // end of the captured string, so trim them. + $text = \trim($text, $text[0]); + $commandOutput = $this->featureContext->getStdErrOfOccCommand(); + $lines = SetupHelper::findLines( + $commandOutput, + $text + ); + Assert::assertGreaterThanOrEqual( + 1, + \count($lines), + "The command output did not contain the expected text on stderr '$text'\n" . + "The command output on stderr was:\n" . + $commandOutput + ); + } + + /** + * @Then the occ command JSON output should be empty + * + * @return void + */ + public function theOccCommandJsonOutputShouldNotReturnAnyData():void { + Assert::assertEquals( + \trim($this->featureContext->getStdOutOfOccCommand()), + "[]", + "Expected command output to be '[]' but got '" + . \trim($this->featureContext->getStdOutOfOccCommand()) + . "'" + ); + Assert::assertEmpty( + $this->featureContext->getStdErrOfOccCommand(), + "Expected occ command error to be empty but got '" + . $this->featureContext->getStdErrOfOccCommand() + . "'" + ); + } + + /** + * @Then :noOfOrphanedShare orphaned remote storage should have been cleared + * + * @param int $noOfOrphanedShare + * + * @return void + * @throws Exception + */ + public function theOrphanedRemoteStorageShouldBeCleared(int $noOfOrphanedShare):void { + $this->theCommandShouldHaveBeenSuccessful(); + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + // removing blank lines + $commandOutput = implode("\n", array_filter(explode("\n", $commandOutput))); + // splitting strings based on newline into an array + $outputLines = preg_split("/\r\n|\n|\r/", $commandOutput); + // first line of command output contains total remote storage scanned + $totalRemoteStorage = (int) filter_var($outputLines[0], FILTER_SANITIZE_NUMBER_INT); + echo "totalremotestorage: $totalRemoteStorage"; + // calculating total no of shares deleted from remote storage: first getting total length of the array + // then minus 2 for first two lines of scan info message + // then divide by 2 because each share delete has message of two line + $totalSharesDeleted = ((\count($outputLines) - 2) / 2) / $totalRemoteStorage; + Assert::assertEquals( + $noOfOrphanedShare, + $totalSharesDeleted, + "The command was expected to delete '$noOfOrphanedShare' orphaned shares but only deleted '$totalSharesDeleted' orphaned shares." + ); + } + + /** + * @Given the administrator has set the default folder for received shares to :folder + * + * @param string $folder + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetTheDefaultFolderForReceivedSharesTo(string $folder):void { + if (OcisHelper::isTestingOnOcisOrReva()) { + // The default folder for received shares is already "Shares" on OCIS and REVA. + // If the step is asking for a different folder, then fail. + // Otherwise just return - the setting is already done by default. + Assert::assertEquals( + "Shares", + \trim($folder, "/"), + __METHOD__ . " tried to set the default folder for received shares to $folder but that is not possible on OCIS" + ); + return; + } + $this->addSystemConfigKeyUsingTheOccCommand( + "share_folder", + $folder + ); + } + + /** + * @When the administrator has set depth_infinity_allowed to :depth_infinity_allowed + * + * @param int $depthInfinityAllowed + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetDepthInfinityAllowedTo($depthInfinityAllowed) { + $depthInfinityAllowedString = (string) $depthInfinityAllowed; + $this->addSystemConfigKeyUsingTheOccCommand( + "dav.propfind.depth_infinity", + $depthInfinityAllowedString + ); + if ($depthInfinityAllowedString === "0") { + $this->featureContext->davPropfindDepthInfinityDisabled(); + } else { + $this->featureContext->davPropfindDepthInfinityEnabled(); + } + } + + /** + * @Given the administrator has set the mail smtpmode to :smtpmode + * + * @param string $smtpmode + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetTheMailSmtpmodeTo(string $smtpmode):void { + $this->addSystemConfigKeyUsingTheOccCommand( + "mail_smtpmode", + $smtpmode + ); + } + + /** + * @When the administrator sets the log level to :level using the occ command + * + * @param string $level + * + * @return void + * @throws Exception + */ + public function theAdministratorSetsLogLevelUsingTheOccCommand(string $level):void { + $this->invokingTheCommand( + "log:manage --level $level" + ); + } + + /** + * @When the administrator sets the timezone to :timezone using the occ command + * + * @param string $timezone + * + * @return void + * @throws Exception + */ + public function theAdministratorSetsTimeZoneUsingTheOccCommand(string $timezone):void { + $this->invokingTheCommand( + "log:manage --timezone $timezone" + ); + } + + /** + * @When the administrator sets the backend to :backend using the occ command + * + * @param string $backend + * + * @return void + * @throws Exception + */ + public function theAdministratorSetsBackendUsingTheOccCommand(string $backend):void { + $this->invokingTheCommand( + "log:manage --backend $backend" + ); + } + + /** + * @When the administrator enables the ownCloud backend using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorEnablesOwnCloudBackendUsingTheOccCommand():void { + $this->invokingTheCommand( + "log:owncloud --enable" + ); + } + + /** + * @When the administrator sets the log file path to :path using the occ command + * + * @param string $path + * + * @return void + * @throws Exception + */ + public function theAdministratorSetsLogFilePathUsingTheOccCommand(string $path):void { + $this->invokingTheCommand( + "log:owncloud --file $path" + ); + } + + /** + * @When the administrator sets the log rotate file size to :size using the occ command + * + * @param string $size + * + * @return void + * @throws Exception + */ + public function theAdministratorSetsLogRotateFileSizeUsingTheOccCommand(string $size):void { + $this->invokingTheCommand( + "log:owncloud --rotate-size $size" + ); + } + + /** + * @Then the command output should be: + * + * @param PyStringNode $content + * + * @return void + * @throws Exception + */ + public function theCommandOutputShouldBe(PyStringNode $content):void { + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + // removing blank lines + $commandOutput = \implode("\n", \array_filter(\explode("\n", $commandOutput))); + $content = \implode("\n", \array_filter(\explode("\n", $content->getRaw()))); + Assert::assertEquals( + $content, + $commandOutput, + "The command output was expected to be '$content' but got '$commandOutput'" + ); + } + + /** + * @When the administrator changes the background jobs mode to :mode using the occ command + * + * @param string $mode + * + * @return void + * @throws Exception + */ + public function theAdministratorChangesTheBackgroundJobsModeTo(string $mode):void { + $this->changeBackgroundJobsModeUsingTheOccCommand($mode); + } + + /** + * @Given the administrator has changed the background jobs mode to :mode + * + * @param string $mode + * + * @return void + * @throws Exception + */ + public function theAdministratorHasChangedTheBackgroundJobsModeTo(string $mode):void { + $this->changeBackgroundJobsModeUsingTheOccCommand($mode); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator sets the external storage :mountPoint to read-only using the occ command + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdminSetsTheExtStorageToReadOnly(string $mountPoint):void { + $this->setExtStorageReadOnlyUsingTheOccCommand($mountPoint); + } + + /** + * @Given the administrator has set the external storage :mountPoint to read-only + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdminHasSetTheExtStorageToReadOnly(string $mountPoint):void { + $this->setExtStorageReadOnlyUsingTheOccCommand($mountPoint); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @Given the administrator has set the external storage :mountPoint to sharing + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdminHasSetTheExtStorageToSharing(string $mountPoint):void { + $this->setExtStorageSharingUsingTheOccCommand($mountPoint); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When /^the administrator (enables|disables) sharing for the external storage "([^"]*)" using the occ command$/ + * + * @param string $action + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdminDisablesSharingForTheExtStorage(string $action, string $mountPoint):void { + $this->setExtStorageSharingUsingTheOccCommand($mountPoint, $action === "enables"); + } + + /** + * @When the administrator sets the external storage :mountPoint to be never scanned automatically using the occ command + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdminSetsTheExtStorageToBeNeverScannedAutomatically(string $mountPoint):void { + $this->setExtStorageCheckChangesUsingTheOccCommand($mountPoint, "never"); + } + + /** + * @Given the administrator has set the external storage :mountPoint to be never scanned automatically + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function theAdminHasSetTheExtStorageToBeNeverScannedAutomatically(string $mountPoint):void { + $this->setExtStorageCheckChangesUsingTheOccCommand($mountPoint, "never"); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator scans the filesystem for all users using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorScansTheFilesystemForAllUsersUsingTheOccCommand():void { + $this->scanFileSystemForAllUsersUsingTheOccCommand(); + } + + /** + * @Given the administrator has scanned the filesystem for all users + * + * @return void + * @throws Exception + */ + public function theAdministratorHasScannedTheFilesystemForAllUsersUsingTheOccCommand():void { + $this->scanFileSystemForAllUsersUsingTheOccCommand(); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator scans the filesystem for user :user using the occ command + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorScansTheFilesystemForUserUsingTheOccCommand(string $user):void { + $this->scanFileSystemForAUserUsingTheOccCommand($user); + } + + /** + * @Given the administrator has scanned the filesystem for user :user + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorHasScannedTheFilesystemForUserUsingTheOccCommand(string $user):void { + $this->scanFileSystemForAUserUsingTheOccCommand($user); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator scans the filesystem in path :path of user :user using the occ command + * + * @param string $path + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorScansTheFilesystemInPathUsingTheOccCommand(string $path, string $user):void { + $user = $this->featureContext->getActualUsername($user); + $this->scanFileSystemPathUsingTheOccCommand($path, $user); + } + + /** + * @Given the administrator scans the filesystem in path :path + * + * @param string $path + * + * @return void + * @throws Exception + */ + public function theAdministratorHasScannedTheFilesystemInPathUsingTheOccCommand(string $path):void { + $this->scanFileSystemPathUsingTheOccCommand($path); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator scans the filesystem for group :group using the occ command + * + * Used to test the --group option of the files:scan command + * + * @param string $group a single group name + * + * @return void + * @throws Exception + */ + public function theAdministratorScansTheFilesystemForGroupUsingTheOccCommand(string $group):void { + $this->scanFileSystemForAGroupUsingTheOccCommand($group); + } + + /** + * @Given the administrator has scanned the filesystem for group :group + * + * Used to test the --group option of the files:scan command + * + * @param string $group a single group name + * + * @return void + * @throws Exception + */ + public function theAdministratorHasScannedTheFilesystemForGroupUsingTheOccCommand(string $group):void { + $this->scanFileSystemForAGroupUsingTheOccCommand($group); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator scans the filesystem for groups list :groups using the occ command + * + * Used to test the --groups option of the files:scan command + * + * @param string $groups a comma-separated list of group names + * + * @return void + * @throws Exception + */ + public function theAdministratorScansTheFilesystemForGroupsUsingTheOccCommand(string $groups):void { + $this->scanFileSystemForGroupsUsingTheOccCommand($groups); + } + + /** + * @Given the administrator has scanned the filesystem for groups list :groups + * + * Used to test the --groups option of the files:scan command + * + * @param string $groups a comma-separated list of group names + * + * @return void + * @throws Exception + */ + public function theAdministratorHasScannedTheFilesystemForGroupsUsingTheOccCommand(string $groups):void { + $this->scanFileSystemForGroupsUsingTheOccCommand($groups); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator cleanups the filesystem for all users using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorCleanupsTheFilesystemForAllUsersUsingTheOccCommand():void { + $this->invokingTheCommand( + "files:cleanup" + ); + } + + /** + * @When the administrator creates the local storage mount :mount using the occ command + * + * @param string $mount + * + * @return void + */ + public function theAdministratorCreatesTheLocalStorageMountUsingTheOccCommand(string $mount):void { + $this->createLocalStorageMountUsingTheOccCommand($mount); + } + + /** + * @Given the administrator has created the local storage mount :mount + * + * @param string $mount + * + * @return void + * @throws Exception + */ + public function theAdministratorHasCreatedTheLocalStorageMountUsingTheOccCommand(string $mount):void { + $this->createLocalStorageMountUsingTheOccCommand($mount); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @param string $action + * @param string $userOrGroup + * @param string $userOrGroupName + * @param string $mountName + * + * @return void + * @throws Exception + */ + public function addRemoveUserOrGroupToOrFromMount( + string $action, + string $userOrGroup, + string $userOrGroupName, + string $mountName + ):void { + if ($action === "adds" || $action === "added") { + $action = "--add"; + } else { + $action = "--remove"; + } + if ($userOrGroup === "user") { + $action = "$action-user"; + $userOrGroupName = $this->featureContext->getActualUsername($userOrGroupName); + } else { + $action = "$action-group"; + } + $mountId = $this->featureContext->getStorageId($mountName); + $this->featureContext->setOccLastCode( + $this->featureContext->runOcc( + [ + 'files_external:applicable', + $mountId, + "$action ", + "$userOrGroupName" + ] + ) + ); + } + + /** + * @param string $mount + * @param string $userOrGroup + * + * @return array + * @throws Exception + */ + public function getListOfApplicableUserOrGroupForMount(string $mount, string $userOrGroup):array { + $validArgs = ["users", "groups"]; + if (!\in_array($userOrGroup, $validArgs)) { + throw new Exception( + "Invalid key provided. Expected:" . + \implode(", ", $validArgs) . + "Found: " . $userOrGroup + ); + } + $mountId = $this->getMountIdForLocalStorage($mount); + $this->featureContext->runOcc( + [ + 'files_external:applicable', + $mountId, + '--output=json' + ] + ); + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + return ($userOrGroup === "users") ? $commandOutput->users : $commandOutput->groups; + } + + /** + * @param string $action + * @param string $userOrGroup + * @param string $userOrGroupName + * + * @return void + * @throws Exception + */ + public function addRemoveUserOrGroupToOrFromLastLocalMount( + string $action, + string $userOrGroup, + string $userOrGroupName + ):void { + $storageIds = $this->featureContext->getStorageIds(); + Assert::assertGreaterThan( + 0, + \count($storageIds), + "addRemoveAsApplicableUserLastLocalMount no local mounts exist" + ); + $lastMountName = \end($storageIds); + $this->addRemoveUserOrGroupToOrFromMount( + $action, + $userOrGroup, + $userOrGroupName, + $lastMountName + ); + } + + /** + * @When /^the administrator (adds|removes) (user|group) "([^"]*)" (?:as|from) the applicable (?:user|group) for the last local storage mount using the occ command$/ + * + * @param string $action + * @param string $userOrGroup + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdminAddsRemovesAsTheApplicableUserLastLocalMountUsingTheOccCommand( + string $action, + string $userOrGroup, + string $user + ):void { + $this->addRemoveUserOrGroupToOrFromLastLocalMount( + $action, + $userOrGroup, + $user + ); + } + + /** + * @Given /^the administrator has (added|removed) (user|group) "([^"]*)" (?:as|from) the applicable (?:user|group) for the last local storage mount$/ + * + * @param string $action + * @param string $userOrGroup + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdminHasAddedRemovedAsTheApplicableUserLastLocalMountUsingTheOccCommand( + string $action, + string $userOrGroup, + string $user + ):void { + $this->addRemoveUserOrGroupToOrFromLastLocalMount( + $action, + $userOrGroup, + $user + ); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When /^the administrator (adds|removes) (user|group) "([^"]*)" (?:as|from) the applicable (?:user|group) for local storage mount "([^"]*)" using the occ command$/ + * + * @param string $action + * @param string $userOrGroup + * @param string $user + * @param string $mount + * + * @return void + * @throws Exception + */ + public function theAdminAddsRemovesAsTheApplicableUserForMountUsingTheOccCommand( + string $action, + string $userOrGroup, + string $user, + string $mount + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->addRemoveUserOrGroupToOrFromMount( + $action, + $userOrGroup, + $user, + $mount + ); + } + + /** + * @Then /^the following (users|groups) should be listed as applicable for local storage mount "([^"]*)"$/ + * + * @param string $usersOrGroups comma separated lists eg: Alice, Brian + * @param string $localStorage + * @param TableNode $applicable + * + * @return void + * @throws Exception + */ + public function theFollowingUsersOrGroupsShouldBeListedAsApplicable(string $usersOrGroups, string $localStorage, TableNode $applicable): void { + $this->featureContext->verifyTableNodeRows( + $applicable, + [], + ["users", "groups"] + ); + $expectedApplicableList = $applicable->getRowsHash(); + $actualApplicableList = $this->getListOfApplicableUserOrGroupForMount($localStorage, $usersOrGroups); + foreach ($expectedApplicableList as $expectedApplicable) { + Assert::assertContains( + $expectedApplicable, + $actualApplicableList, + __METHOD__ + . $usersOrGroups + . " not found!\nexpected: " + . $expectedApplicable + . " to be in the list [" + . \implode(", ", $actualApplicableList) + . "]." + ); + } + } + + /** + * @Then /^the applicable (users|groups) list should be empty for local storage mount "([^"]*)"$/ + * + * @param string $usersOrGroups + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function theApplicableUsersOrGroupsListShouldBeEmptyForLocalStorageMount(string $usersOrGroups, string $localStorage): void { + $actualApplicableList = $this->getListOfApplicableUserOrGroupForMount($localStorage, $usersOrGroups); + Assert::assertEquals( + 0, + \count($actualApplicableList), + __METHOD__ + . "Expected empty list for applicable " + . $usersOrGroups + . " but found: [" + . \implode(", ", $actualApplicableList) + . "]." + ); + } + + /** + * @When the administrator removes all from the applicable users and groups for local storage mount :localStorage using the occ command + * + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function theAdminRemovesAllForTheMountUsingOccCommand(string $localStorage):void { + $mountId = $this->featureContext->getStorageId($localStorage); + $this->featureContext->runOcc( + [ + 'files_external:applicable', + $mountId, + "--remove-all" + ] + ); + } + + /** + * @Given /^the administrator has (added|removed) (user|group) "([^"]*)" (?:as|from) the applicable (?:user|group) for local storage mount "([^"]*)"$/ + * + * @param string $action + * @param string $userOrGroup + * @param string $user + * @param string $mount + * + * @return void + * @throws Exception + */ + public function theAdminHasAddedRemovedTheApplicableUserForMountUsingTheOccCommand( + string $action, + string $userOrGroup, + string $user, + string $mount + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->addRemoveUserOrGroupToOrFromMount( + $action, + $userOrGroup, + $user, + $mount + ); + $this->theCommandShouldHaveBeenSuccessful(); + // making plural "users" or "groups" + $userOrGroup = $userOrGroup . "s"; + $actualApplicableList = $this->getListOfApplicableUserOrGroupForMount($mount, $userOrGroup); + + if ($action === "added") { + Assert::assertContains( + $user, + $actualApplicableList, + __METHOD__ + . " The expected applicable " + . $userOrGroup + . " is not present in the actual list of applicable " + . $userOrGroup + . " for mount point: " + . $mount . ".\n" + ); + } else { + Assert::assertNotContains( + $user, + $actualApplicableList, + __METHOD__ + . " The applicable " + . $userOrGroup + . " is present in the actual list of applicable " + . $userOrGroup + . " for mount point: " + . $mount . ".\n" + ); + } + } + + /** + * @When the administrator configures the key :key with value :value for the local storage mount :localStorage + * + * @param string $key + * @param string $value + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function adminConfiguresLocalStorageMountUsingTheOccCommand(string $key, string $value, string $localStorage):void { + $mountId = $this->featureContext->getStorageId($localStorage); + $this->featureContext->runOcc( + [ + "files_external:config", + $mountId, + $key, + $value + ] + ); + } + + /** + * @When the administrator lists the available backends using the occ command + * + * @return void + * @throws Exception + */ + public function adminListsAvailableBackendsUsingTheOccCommand():void { + $this->listAvailableBackends(); + } + + /** + * @When the administrator lists the available backends of type :type using the occ command + * + * @param String $type + * + * @return void + * @throws Exception + */ + public function adminListsAvailableBackendsOfTypeUsingTheOccCommand(string $type):void { + $this->listAvailableBackendsOfType($type); + } + + /** + * @When the administrator lists the :backend backend of type :backendType using the occ command + * + * @param String $backend + * @param String $backendType + * + * @return void + * @throws Exception + */ + public function adminListsBackendOfTypeUsingTheOccCommand(string $backend, string $backendType):void { + $this->listBackendOfType($backendType, $backend); + } + + /** + * @When the administrator lists configurations with the existing key :key for the local storage mount :localStorage + * + * @param string $key + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function adminListsConfigurationsWithExistingKeyForLocalStorageMountUsingTheOccCommand(string $key, string $localStorage):void { + $mountId = $this->featureContext->getStorageId($localStorage); + $this->featureContext->runOcc( + [ + "files_external:config", + $mountId, + $key, + ] + ); + } + + /** + * @Given the administrator has configured the key :key with value :value for the local storage mount :localStorage + * + * @param string $key + * @param string $value + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function adminConfiguredLocalStorageMountUsingTheOccCommand(string $key, string $value, string $localStorage):void { + $mountId = $this->featureContext->getStorageId($localStorage); + $this->featureContext->runOcc( + [ + "files_external:config", + $mountId, + $key, + $value + ] + ); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator lists the local storage using the occ command + * + * @return void + * @throws Exception + */ + public function adminListsLocalStorageMountUsingTheOccCommand():void { + $this->listLocalStorageMount(); + } + + /** + * @When the administrator lists the local storage with --short using the occ command + * + * @return void + * @throws Exception + */ + public function adminListsLocalStorageMountShortUsingTheOccCommand():void { + $this->listLocalStorageMountShort(); + } + + /** + * @When the administrator lists the local storage with --mount-options using the occ command + * + * @return void + * @throws Exception + */ + public function adminListsLocalStorageMountOptionsUsingTheOccCommand():void { + $this->listLocalStorageMountOptions(); + } + + /** + * @When the administrator lists the local storage with --show-password using the occ command + * + * @return void + * @throws Exception + */ + public function adminListsLocalStorageShowPasswordUsingTheOccCommand():void { + $this->listLocalStorageShowPassword(); + } + + /** + * @Then the following local storage should exist + * + * @param TableNode $mountPoints + * + * @return void + */ + public function theFollowingLocalStoragesShouldExist(TableNode $mountPoints):void { + $createdLocalStorage = []; + $expectedLocalStorages = $mountPoints->getColumnsHash(); + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($commandOutput as $storageEntry) { + $createdLocalStorage[$storageEntry->mount_id] = \ltrim($storageEntry->mount_point, '/'); + } + foreach ($expectedLocalStorages as $expectedStorageEntry) { + Assert::assertContains( + $expectedStorageEntry['localStorage'], + $createdLocalStorage, + "'" + . \implode(', ', $createdLocalStorage) + . "' does not contain '${expectedStorageEntry['localStorage']}' " + . __METHOD__ + ); + } + } + + /** + * @Then the following local storage should not exist + * + * @param TableNode $mountPoints + * + * @return void + * @throws Exception + */ + public function theFollowingLocalStoragesShouldNotExist(TableNode $mountPoints):void { + $createdLocalStorage = []; + $this->listLocalStorageMount(); + $expectedLocalStorages = $mountPoints->getColumnsHash(); + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($commandOutput as $i) { + $createdLocalStorage[$i->mount_id] = \ltrim($i->mount_point, '/'); + } + foreach ($expectedLocalStorages as $i) { + Assert::assertNotContains( + $i['localStorage'], + $createdLocalStorage, + "{$i['localStorage']} exists but was not expected to exist" + ); + } + } + + /** + * @Then the following backend types should be listed: + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingBackendTypesShouldBeListed(TableNode $table):void { + $expectedBackendTypes = $table->getColumnsHash(); + foreach ($expectedBackendTypes as $expectedBackendTypeEntry) { + Assert::assertArrayHasKey( + 'backend-type', + $expectedBackendTypeEntry, + __METHOD__ + . " The provided expected backend type entry '" + . \implode(', ', $expectedBackendTypeEntry) + . "' do not have key 'backend-type'" + ); + } + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + $keys = \array_keys((array) $commandOutput); + foreach ($expectedBackendTypes as $backendTypesEntry) { + Assert::assertContains( + $backendTypesEntry['backend-type'], + $keys, + __METHOD__ + . " ${backendTypesEntry['backend-type']} is not contained in '" + . \implode(', ', $keys) + . "' but was expected to be." + ); + } + } + + /** + * @Then the following authentication/storage backends should be listed: + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingBackendsShouldBeListed(TableNode $table):void { + $expectedBackends = $table->getColumnsHash(); + foreach ($expectedBackends as $expectedBackendEntry) { + Assert::assertArrayHasKey( + 'backends', + $expectedBackendEntry, + __METHOD__ + . " The provided expected backend entry '" + . \implode(', ', $expectedBackendEntry) + . "' do not have key 'backends'" + ); + } + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + $keys = \array_keys((array) $commandOutput); + foreach ($expectedBackends as $backendsEntry) { + Assert::assertContains( + $backendsEntry['backends'], + $keys, + __METHOD__ + . " ${backendsEntry['backends']} is not contained in '" + . \implode(', ', $keys) + . "' but was expected to be." + ); + } + } + + /** + * @Then the following authentication/storage backend keys should be listed: + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingBackendKeysOfTypeShouldBeListed(TableNode $table):void { + $expectedBackendKeys = $table->getColumnsHash(); + foreach ($expectedBackendKeys as $expectedBackendKeyEntry) { + Assert::assertArrayHasKey( + 'backend-keys', + $expectedBackendKeyEntry, + __METHOD__ + . " The provided expected backend key entry '" + . \implode(', ', $expectedBackendKeyEntry) + . "' do not have key 'backend-keys'" + ); + } + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + $keys = \array_keys((array) $commandOutput); + foreach ($expectedBackendKeys as $backendKeysEntry) { + Assert::assertContains( + $backendKeysEntry['backend-keys'], + $keys, + __METHOD__ + . " ${backendKeysEntry['backend-keys']} is not contained in '" + . \implode(', ', $keys) + . "' but was expected to be." + ); + } + } + + /** + * @Then the following local storage should be listed: + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingLocalStorageShouldBeListed(TableNode $table):void { + $this->featureContext->verifyTableNodeColumns( + $table, + ['MountPoint', 'ApplicableUsers', 'ApplicableGroups'], + ['Storage', 'AuthenticationType', 'Configuration', 'Options', 'Auth', 'Type'] + ); + $expectedLocalStorages = $table->getColumnsHash(); + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($expectedLocalStorages as $expectedStorageEntry) { + $isStorageEntryListed = false; + foreach ($commandOutput as $listedStorageEntry) { + if ($expectedStorageEntry["MountPoint"] === $listedStorageEntry->mount_point) { + if (isset($expectedStorageEntry['Storage'])) { + Assert::assertEquals( + $expectedStorageEntry['Storage'], + $listedStorageEntry->storage, + "Storage column does not have the expected value for storage " + . $expectedStorageEntry['MountPoint'] + ); + } + if (isset($expectedStorageEntry['AuthenticationType'])) { + Assert::assertEquals( + $expectedStorageEntry['AuthenticationType'], + $listedStorageEntry->authentication_type, + "AuthenticationType column does not have the expected value for storage " + . $expectedStorageEntry['MountPoint'] + ); + } + if (isset($expectedStorageEntry['Auth'])) { + Assert::assertEquals( + $expectedStorageEntry['Auth'], + $listedStorageEntry->auth, + "Auth column does not have the expected value for storage " + . $expectedStorageEntry['MountPoint'] + ); + } + if (isset($expectedStorageEntry['Configuration'])) { + if ($expectedStorageEntry['Configuration'] === '') { + Assert::assertEquals( + '', + $listedStorageEntry->configuration, + 'Configuration column should be empty but is ' + . $listedStorageEntry->configuration + ); + } else { + if (\is_string($listedStorageEntry->configuration)) { + Assert::assertStringStartsWith( + $expectedStorageEntry['Configuration'], + $listedStorageEntry->configuration, + "Configuration column does not start with the expected value for storage " + . $expectedStorageEntry['Configuration'] + ); + } else { + $item = \strtok($expectedStorageEntry['Configuration'], ':'); + Assert::assertTrue( + \property_exists($listedStorageEntry->configuration, $item), + "$item was not found in the Configuration column" + ); + } + } + } + if (isset($expectedStorageEntry['Options'])) { + Assert::assertEquals( + $expectedStorageEntry['Options'], + $listedStorageEntry->options, + "Options column does not have the expected value for storage " + . $expectedStorageEntry['MountPoint'] + ); + } + if (isset($expectedStorageEntry['ApplicableUsers'])) { + if (\is_string($listedStorageEntry->applicable_users)) { + if ($listedStorageEntry->applicable_users === '') { + $listedApplicableUsers = []; + } else { + $listedApplicableUsers = \explode(', ', $listedStorageEntry->applicable_users); + } + } else { + $listedApplicableUsers = $listedStorageEntry->applicable_users; + } + if ($expectedStorageEntry['ApplicableUsers'] === '') { + Assert::assertEquals( + [], + $listedApplicableUsers, + "ApplicableUsers was expected to be an empty array but was not empty" + ); + } else { + $expectedApplicableUsers = \explode(', ', $expectedStorageEntry['ApplicableUsers']); + foreach ($expectedApplicableUsers as $expectedApplicableUserEntry) { + $expectedApplicableUserEntry = $this->featureContext->getActualUsername($expectedApplicableUserEntry); + Assert::assertContains( + $expectedApplicableUserEntry, + $listedApplicableUsers, + __METHOD__ + . " '$expectedApplicableUserEntry' is not listed in '" + . \implode(', ', $listedApplicableUsers) + . "'" + ); + } + } + } + if (isset($expectedStorageEntry['ApplicableGroups'])) { + if (\is_string($listedStorageEntry->applicable_groups)) { + if ($listedStorageEntry->applicable_groups === '') { + $listedApplicableGroups = []; + } else { + $listedApplicableGroups = \explode(', ', $listedStorageEntry->applicable_groups); + } + } else { + $listedApplicableGroups = $listedStorageEntry->applicable_groups; + } + if ($expectedStorageEntry['ApplicableGroups'] === '') { + Assert::assertEquals( + [], + $listedApplicableGroups, + "ApplicableGroups was expected to be an empty array but was not empty" + ); + Assert::assertEquals([], $listedApplicableGroups); + } else { + $expectedApplicableGroups = \explode(', ', $expectedStorageEntry['ApplicableGroups']); + foreach ($expectedApplicableGroups as $expectedApplicableGroupEntry) { + Assert::assertContains( + $expectedApplicableGroupEntry, + $listedApplicableGroups, + __METHOD__ + . " '$expectedApplicableGroupEntry' is not listed in '" + . \implode(', ', $listedApplicableGroups) + . "'" + ); + } + } + } + if (isset($expectedStorageEntry['Type'])) { + Assert::assertEquals( + $expectedStorageEntry['Type'], + $listedStorageEntry->type, + "Type column does not have the expected value for storage " + . $expectedStorageEntry['MountPoint'] + ); + } + $isStorageEntryListed = true; + break; + } + } + Assert::assertTrue($isStorageEntryListed, __METHOD__ . " Expected local storage {$expectedStorageEntry['MountPoint']} not found"); + } + } + + /** + * @Then the configuration output should be :expectedOutput + * + * @param string $expectedOutput + * + * @return void + * @throws Exception + */ + public function theConfigurationOutputShouldBe(string $expectedOutput):void { + $actualOutput = $this->featureContext->getStdOutOfOccCommand(); + $trimmedOutput = \trim($actualOutput); + Assert::assertEquals( + $expectedOutput, + $trimmedOutput, + __METHOD__ + . " The expected configuration output was '$expectedOutput', but got '$actualOutput' instead." + ); + } + + /** + * @Then the following should be included in the configuration of local storage :localStorage: + * + * @param string $localStorage + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingShouldBeIncludedInTheConfigurationOfLocalStorage(string $localStorage, TableNode $table):void { + $expectedConfigurations = $table->getColumnsHash(); + foreach ($expectedConfigurations as $expectedConfigurationEntry) { + Assert::assertArrayHasKey( + 'configuration', + $expectedConfigurationEntry, + __METHOD__ + . " The provided expected configuration entry '" + . \implode(', ', $expectedConfigurationEntry) + . "' do not have key 'configuration'" + ); + } + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + $isStorageEntryListed = false; + foreach ($commandOutput as $listedStorageEntry) { + if ($listedStorageEntry->mount_point === $localStorage) { + $isStorageEntryListed = true; + $configurations = $listedStorageEntry->configuration; + $configurationsSplitted = \explode(', ', $configurations); + foreach ($expectedConfigurations as $expectedConfigArray) { + foreach ($expectedConfigArray as $expectedConfigEntry) { + Assert::assertContains( + $expectedConfigEntry, + $configurationsSplitted, + __METHOD__ + . " $expectedConfigEntry is not contained in '" + . \implode(', ', $configurationsSplitted) + . "' but was expected to be." + ); + } + } + break; + } + } + Assert::assertTrue($isStorageEntryListed, "Expected local storage '$localStorage' not found "); + } + + /** + * @When the administrator adds an option with key :key and value :value for the local storage mount :localStorage + * + * @param string $key + * @param string $value + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function adminAddsOptionForLocalStorageMountUsingTheOccCommand(string $key, string $value, string $localStorage):void { + $mountId = $this->featureContext->getStorageId($localStorage); + $this->featureContext->runOcc( + [ + "files_external:option", + $mountId, + $key, + $value + ] + ); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @Then the following should be included in the options of local storage :localStorage: + * + * @param string $localStorage + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingShouldBeIncludedInTheOptionsOfLocalStorage(string $localStorage, TableNode $table):void { + $expectedOptions = $table->getColumnsHash(); + foreach ($expectedOptions as $expectedOptionEntry) { + Assert::assertArrayHasKey( + 'options', + $expectedOptionEntry, + __METHOD__ + . " The provided expected option '" + . \implode(', ', $expectedOptionEntry) + . "' do not have key 'option'" + ); + } + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + $isStorageEntryListed = false; + foreach ($commandOutput as $listedStorageEntry) { + if ($listedStorageEntry->mount_point === $localStorage) { + $isStorageEntryListed = true; + $options = $listedStorageEntry->options; + $optionsSplitted = \explode(', ', $options); + foreach ($expectedOptions as $expectedOptionArray) { + foreach ($expectedOptionArray as $expectedOptionEntry) { + Assert::assertContains( + $expectedOptionEntry, + $optionsSplitted, + __METHOD__ + . " $expectedOptionEntry is not contained in '" + . \implode(', ', $optionsSplitted) + . "' , but was expected to be." + ); + } + } + break; + } + } + Assert::assertTrue($isStorageEntryListed, "Expected local storage '$localStorage' not found "); + } + + /** + * @Then the following should not be included in the options of local storage :localStorage: + * + * @param string $localStorage + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingShouldNotBeIncludedInTheOptionsOfLocalStorage(string $localStorage, TableNode $table):void { + $expectedOptions = $table->getColumnsHash(); + foreach ($expectedOptions as $expectedOptionEntry) { + Assert::assertArrayHasKey( + 'options', + $expectedOptionEntry, + __METHOD__ + . " The provided expected option '" + . \implode(', ', $expectedOptionEntry) + . "' do not have key 'options'" + ); + } + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + $isStorageEntryListed = false; + foreach ($commandOutput as $listedStorageEntry) { + if ($listedStorageEntry->mount_point === $localStorage) { + $isStorageEntryListed = true; + $options = $listedStorageEntry->options; + $optionsSplitted = \explode(', ', $options); + foreach ($expectedOptions as $expectedOptionArray) { + foreach ($expectedOptionArray as $expectedOptionEntry) { + Assert::assertNotContains( + $expectedOptionEntry, + $optionsSplitted, + __METHOD__ + . " $expectedOptionEntry is contained in '" + . \implode(', ', $optionsSplitted) + . "' , but was not expected to be." + ); + } + } + break; + } + } + Assert::assertTrue($isStorageEntryListed, "Expected local storage '$localStorage' not found "); + } + + /** + * @When the administrator deletes local storage :folder using the occ command + * + * @param string $folder + * + * @return integer|boolean + * @throws Exception + */ + public function administratorDeletesFolder(string $folder) { + return $this->deleteLocalStorageFolderUsingTheOccCommand($folder); + } + + /** + * @Given the administrator has deleted local storage :folder using the occ command + * + * @param string $folder + * + * @return integer + * @throws Exception + */ + public function administratorHasDeletedLocalStorageFolderUsingTheOccCommand(string $folder):int { + $mount_id = $this->deleteLocalStorageFolderUsingTheOccCommand($folder); + $this->theCommandShouldHaveBeenSuccessful(); + return $mount_id; + } + + /** + * @param string $folder + * + * @return integer|null + * @throws Exception + */ + public function getMountIdForLocalStorage(string $folder): ?int { + $createdLocalStorage = []; + $mount_id = null; + $this->listLocalStorageMount(); + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($commandOutput as $i) { + $createdLocalStorage[$i->mount_id] = \ltrim($i->mount_point, '/'); + } + foreach ($createdLocalStorage as $key => $value) { + if ($value === $folder) { + $mount_id = $key; + } + } + + return (int) $mount_id; + } + + /** + * @param string $folder + * @param bool $mustExist + * + * @return integer|bool + * @throws Exception + */ + public function deleteLocalStorageFolderUsingTheOccCommand(string $folder, bool $mustExist = true) { + $mount_id = $this->getMountIdForLocalStorage($folder); + + if (!isset($mount_id)) { + if ($mustExist) { + throw new Exception("Id not found for folder to be deleted"); + } + return false; + } + $this->invokingTheCommand('files_external:delete --yes ' . $mount_id); + return $mount_id; + } + + /** + * @When the administrator exports the local storage mounts using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorExportsTheMountsUsingTheOccCommand():void { + $this->invokingTheCommand('files_external:export'); + } + + /** + * @When the administrator imports the local storage mount from file :file using the occ command + * + * @param string $file + * + * @return void + * @throws Exception + */ + public function theAdministratorImportsTheMountFromFileUsingTheOccCommand(string $file):void { + $this->invokingTheCommand( + 'files_external:import ' . TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/$file" + ); + } + + /** + * @Given /^the administrator has exported the (local|external) storage mounts using the occ command$/ + * + * @return void + * @throws Exception + */ + public function theAdministratorHasExportedTheMountsUsingTheOccCommand():void { + $this->invokingTheCommand('files_external:export'); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @Then the command should output configuration for local storage mount :mount + * + * @param string $mount + * + * @return void + * @throws Exception + */ + public function theOutputShouldContainConfigurationForMount(string $mount):void { + $actualConfig = null; + + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($commandOutput as $i) { + if ($mount === \ltrim($i->mount_point, '/')) { + $actualConfig = $i; + break; + } + } + + Assert::assertNotNull($actualConfig, 'Configuration for local storage mount ' . $mount . ' not found.'); + } + + /** + * @When the administrator verifies the mount configuration for local storage :localStorage using the occ command + * + * @param string $localStorage + * + * @return void + * @throws Exception + */ + public function theAdministratorVerifiesTheMountConfigurationForLocalStorageUsingTheOccCommand(string $localStorage):void { + $this->listLocalStorageMount(); + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($commandOutput as $entry) { + if (\ltrim($entry->mount_point, '/') == $localStorage) { + $mountId = $entry->mount_id; + } + } + if (!isset($mountId)) { + throw new Exception("Id not found for local storage $localStorage to be verified"); + } + $this->invokingTheCommand('files_external:verify ' . $mountId); + } + + /** + * @Then the following mount configuration information should be listed: + * + * @param TableNode $info + * + * @return void + */ + public function theFollowingInformationShouldBeListed(TableNode $info):void { + $ResultArray = []; + $expectedInfo = $info->getColumnsHash(); + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $commandOutputSplitted = \preg_split("/[-]/", $commandOutput); + $filteredArray = \array_filter(\array_map("trim", $commandOutputSplitted)); + foreach ($filteredArray as $entry) { + $keyValue = \preg_split("/[:]/", $entry); + if (isset($keyValue[1])) { + $ResultArray[$keyValue[0]] = $keyValue[1]; + } else { + $ResultArray[$keyValue[0]] = ""; + } + } + foreach ($expectedInfo as $element) { + Assert::assertEquals( + $element, + \array_map('trim', $ResultArray), + __METHOD__ + . " '" . \implode(', ', $element) + . "' was expected to be listed, but is not listed in the mount configuration information" + ); + } + } + + /** + * @When the administrator list the repair steps using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorListTheRepairStepsUsingTheOccCommand():void { + $this->invokingTheCommand('maintenance:repair --list'); + } + + /** + * @Then the background jobs mode should be :mode + * + * @param string $mode + * + * @return void + * @throws Exception + */ + public function theBackgroundJobsModeShouldBe(string $mode):void { + $this->invokingTheCommand( + "config:app:get core backgroundjobs_mode" + ); + $lastOutput = $this->featureContext->getStdOutOfOccCommand(); + Assert::assertEquals( + $mode, + \trim($lastOutput), + "The background jobs mode was expected to be {$mode} but got '" + . \trim($lastOutput) + . "'" + ); + } + + /** + * @Then the update channel should be :value + * + * @param string $value + * + * @return void + * @throws Exception + */ + public function theUpdateChannelShouldBe(string $value):void { + $this->invokingTheCommand( + "config:app:get core OC_Channel" + ); + $lastOutput = $this->featureContext->getStdOutOfOccCommand(); + Assert::assertEquals( + $value, + \trim($lastOutput), + "The update channel was expected to be '$value' but got '" + . \trim($lastOutput) + . "'" + ); + } + + /** + * @Then the log level should be :logLevel + * + * @param string $logLevel + * + * @return void + * @throws Exception + */ + public function theLogLevelShouldBe(string $logLevel):void { + $this->invokingTheCommand( + "config:system:get loglevel" + ); + $lastOutput = $this->featureContext->getStdOutOfOccCommand(); + Assert::assertEquals( + $logLevel, + \trim($lastOutput), + "The log level was expected to be '$logLevel' but got '" + . \trim($lastOutput) + . "'" + ); + } + + /** + * @When the administrator adds/updates config key :key with value :value in app :app using the occ command + * + * @param string $key + * @param string $value + * @param string $app + * + * @return void + * @throws Exception + */ + public function theAdministratorAddsConfigKeyWithValueInAppUsingTheOccCommand(string $key, string $value, string $app):void { + $this->addConfigKeyWithValueInAppUsingTheOccCommand( + $key, + $value, + $app + ); + } + + /** + * @Given the administrator has added config key :key with value :value in app :app + * + * @param string $key + * @param string $value + * @param string $app + * + * @return void + * @throws Exception + */ + public function theAdministratorHasAddedConfigKeyWithValueInAppUsingTheOccCommand(string $key, string $value, string $app):void { + $this->addConfigKeyWithValueInAppUsingTheOccCommand( + $key, + $value, + $app + ); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator deletes config key :key of app :app using the occ command + * + * @param string $key + * @param string $app + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesConfigKeyOfAppUsingTheOccCommand(string $key, string $app):void { + $this->deleteConfigKeyOfAppUsingTheOccCommand($key, $app); + } + + /** + * @When the administrator adds/updates system config key :key with value :value using the occ command + * @When the administrator adds/updates system config key :key with value :value and type :type using the occ command + * + * @param string $key + * @param string $value + * @param string $type + * + * @return void + * @throws Exception + */ + public function theAdministratorAddsSystemConfigKeyWithValueUsingTheOccCommand( + string $key, + string $value, + string $type = "string" + ):void { + $this->addSystemConfigKeyUsingTheOccCommand( + $key, + $value, + $type + ); + } + + /** + * @Given the administrator has added/updated system config key :key with value :value + * @Given the administrator has added/updated system config key :key with value :value and type :type + * + * @param string $key + * @param string $value + * @param string $type + * + * @return void + * @throws Exception + */ + public function theAdministratorHasAddedSystemConfigKeyWithValueUsingTheOccCommand( + string $key, + string $value, + string $type = "string" + ):void { + $this->addSystemConfigKeyUsingTheOccCommand( + $key, + $value, + $type + ); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator deletes system config key :key using the occ command + * + * @param string $key + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesSystemConfigKeyUsingTheOccCommand(string $key):void { + $this->deleteSystemConfigKeyUsingTheOccCommand($key); + } + + /** + * @When the administrator empties the trashbin of user :user using the occ command + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorEmptiesTheTrashbinOfUserUsingTheOccCommand(string $user):void { + $this->emptyTrashBinOfUserUsingOccCommand($user); + } + + /** + * @When the administrator deletes all the versions for user :user + * @When the administrator tries to delete all the versions for user :user + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesAllTheVersionsForUser(string $user):void { + $user = $this->featureContext->getActualUsername($user); + $this->deleteAllVersionsForUserUsingOccCommand($user); + } + + /** + * @When the administrator cleanups all the orphaned remote storages of shares using the occ command + * + * @return void + * @throws Exception + */ + public function theAdminCleanupsOrphanedRemoteStoragesOfSharesUsingOccCommand():void { + $this->cleanOrphanedRemoteStoragesUsingOccCommand(); + } + + /** + * @When the administrator deletes all the versions for the following users: + * + * @param TableNode $usersTable + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesAllTheVersionsForSpecificUsers(TableNode $usersTable):void { + $this->featureContext->verifyTableNodeColumns($usersTable, ["username"]); + $usernames = $usersTable->getHash(); + $usernamesArray = []; + foreach ($usernames as $username) { + array_push($usernamesArray, $username["username"]); + } + $users = implode(" ", $usernamesArray); + $this->deleteAllVersionsForMultipleUsersUsingOccCommand($users); + } + + /** + * @When the administrator deletes the file versions for all users + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesVersionsForAllUsers(): void { + $this->deleteAllVersionsForAllUsersUsingOccCommand(); + } + + /** + * @When the administrator deletes the expired versions for user :user + * @When the administrator tries to delete the expired versions for user :user + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesExpiredVersionsForUser(string $user): void { + $user = $this->featureContext->getActualUsername($user); + $this->deleteExpiredVersionsForUserUsingOccCommand($user); + } + + /** + * @When the administrator deletes the expired versions for the following users: + * + * @param TableNode $usersTable + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesExpiredVersionsForSpecificUsers(TableNode $usersTable): void { + $this->featureContext->verifyTableNodeColumns($usersTable, ["username"]); + $usernames = $usersTable->getHash(); + $usernamesArray = []; + foreach ($usernames as $username) { + array_push($usernamesArray, $username["username"]); + } + $users = implode(" ", $usernamesArray); + $this->deleteExpiredVersionsForMultipleUsersUsingOccCommand($users); + } + + /** + * @When the administrator deletes the expired versions for all users + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesExpiredVersionsForAllUsers(): void { + $this->deleteExpiredVersionsForAllUsersUsingOccCommand(); + } + + /** + * @When the administrator empties the trashbin of all users using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorEmptiesTheTrashbinOfAllUsersUsingTheOccCommand():void { + $this->emptyTrashBinOfUserUsingOccCommand(''); + } + + /** + * @When the administrator creates a calendar for user :user with name :calendarName using the occ command + * + * @param string $user + * @param string $calendarName + * + * @return void + * @throws Exception + */ + public function theAdminCreatesACalendarForUserUsingTheOccCommand(string $user, string $calendarName):void { + $user = $this->featureContext->getActualUsername($user); + $this->createCalendarForUserUsingOccCommand($user, $calendarName); + } + + /** + * @When the administrator creates an address book for user :user with name :addressBookName using the occ command + * + * @param string $user + * @param string $addressBookName + * + * @return void + * @throws Exception + */ + public function theAdminCreatesAnAddressBookForUserUsingTheOccCommand(string $user, string $addressBookName):void { + $user = $this->featureContext->getActualUsername($user); + $this->createAnAddressBookForUserUsingOccCommand($user, $addressBookName); + } + + /** + * @When the administrator gets all the jobs in the background queue using the occ command + * + * @return void + * @throws Exception + */ + public function theAdministratorGetsAllTheJobsInTheBackgroundQueueUsingTheOccCommand():void { + $this->getAllJobsInBackgroundQueueUsingOccCommand(); + } + + /** + * @When the administrator deletes the last background job :job using the occ command + * + * @param string $job + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesLastBackgroundJobUsingTheOccCommand(string $job):void { + $this->deleteLastBackgroundJobUsingTheOccCommand($job); + } + + /** + * @Then the last deleted background job :job should not be listed in the background jobs queue + * + * @param string $job + * + * @return void + * @throws Exception + */ + public function theLastDeletedJobShouldNotBeListedInTheJobsQueue(string $job):void { + $jobId = $this->lastDeletedJobId; + $match = $this->getLastJobIdForJob($job); + if ($match) { + Assert::assertNotEquals( + $jobId, + $match, + "job $job with jobId $jobId" . + " was not expected to be listed in background queue, but was" + ); + } + } + + /** + * @Then system config key :key should have value :value + * + * @param string $key + * @param string $value + * + * @return void + * @throws Exception + */ + public function systemConfigKeyShouldHaveValue(string $key, string $value):void { + $config = \trim( + SetupHelper::getSystemConfigValue( + $key, + $this->featureContext->getStepLineRef() + ) + ); + Assert::assertSame( + $value, + $config, + "The system config key '$key' was expected to have value '$value', but got '$config'" + ); + } + + /** + * @Then the command output table should contain the following text: + * + * @param TableNode $table table of patterns to find with table title as 'table_column' + * + * @return void + * @throws Exception + */ + public function theCommandOutputTableShouldContainTheFollowingText(TableNode $table):void { + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $this->featureContext->verifyTableNodeColumns($table, ['table_column']); + foreach ($table as $row) { + $lines = SetupHelper::findLines( + $commandOutput, + $row['table_column'] + ); + Assert::assertNotEmpty( + $lines, + "Value: " . $row['table_column'] . " not found" + ); + } + } + + /** + * @Then system config key :key should not exist + * + * @param string $key + * + * @return void + * @throws Exception + */ + public function systemConfigKeyShouldNotExist(string $key):void { + Assert::assertEmpty( + SetupHelper::getSystemConfig( + $key, + $this->featureContext->getStepLineRef() + )['stdOut'], + "The system config key '$key' was not expected to exist" + ); + } + + /** + * @When the administrator lists the config keys + * + * @return void + * @throws Exception + */ + public function theAdministratorListsTheConfigKeys():void { + $this->invokingTheCommand( + "config:list" + ); + } + + /** + * @Then the command output should contain the apps configs + * + * @return void + */ + public function theCommandOutputShouldContainTheAppsConfigs():void { + $config_list = \json_decode($this->featureContext->getStdOutOfOccCommand(), true); + Assert::assertArrayHasKey( + 'apps', + $config_list, + "The occ output does not contain apps configs" + ); + Assert::assertNotEmpty( + $config_list['apps'], + "The occ output does not contain apps configs" + ); + } + + /** + * @Then the command output should contain the system configs + * + * @return void + */ + public function theCommandOutputShouldContainTheSystemConfigs():void { + $config_list = \json_decode($this->featureContext->getStdOutOfOccCommand(), true); + Assert::assertArrayHasKey( + 'system', + $config_list, + "The occ output does not contain system configs" + ); + Assert::assertNotEmpty( + $config_list['system'], + "The occ output does not contain system configs" + ); + } + + /** + * @Given the administrator has cleared the versions for user :user + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorHasClearedTheVersionsForUser(string $user):void { + $user = $this->featureContext->getActualUsername($user); + $this->deleteAllVersionsForUserUsingOccCommand($user); + Assert::assertSame( + "Delete versions of $user", + \trim($this->featureContext->getStdOutOfOccCommand()) + ); + } + + /** + * @Given the administrator has cleared the versions for all users + * + * @return void + * @throws Exception + */ + public function theAdministratorHasClearedTheVersionsForAllUsers():void { + $this->deleteAllVersionsForAllUsersUsingOccCommand(); + Assert::assertStringContainsString( + "Delete all versions", + \trim($this->featureContext->getStdOutOfOccCommand()), + "Expected 'Delete all versions' to be contained in the output of occ command: " + . \trim($this->featureContext->getStdOutOfOccCommand()) + ); + } + + /** + * get jobId of the latest job found of given job class + * + * @param string $job + * + * @return string|bool + * @throws Exception + */ + public function getLastJobIdForJob(string $job) { + $this->getAllJobsInBackgroundQueueUsingOccCommand(); + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $lines = SetupHelper::findLines( + $commandOutput, + $job + ); + if (!$lines) { + return false; + } + // find the jobId of the newest job among the jobs with given class + $success = \preg_match("/\d+/", \end($lines), $match); + if ($success) { + return $match[0]; + } + return false; + } + + /** + * @Then the system config key :key from the last command output should match value :value of type :type + * + * @param string $key + * @param string $value + * @param string $type + * + * @return void + */ + public function theSystemConfigKeyFromLastCommandOutputShouldContainValue( + string $key, + string $value, + string $type + ):void { + $configList = \json_decode( + $this->featureContext->getStdOutOfOccCommand(), + true + ); + $systemConfig = $configList['system']; + + // convert the value to it's respective type based on type given in the type column + if ($type === 'boolean') { + $value = $value === 'true' ? true : false; + } elseif ($type === 'integer') { + $value = (int) $value; + } elseif ($type === 'json') { + // if the expected value of the key is a json + // match the value with the regular expression + $actualKeyValuePair = \json_encode( + $systemConfig[$key], + JSON_UNESCAPED_SLASHES + ); + + Assert::assertThat( + $actualKeyValuePair, + Assert::matchesRegularExpression($value) + ); + return; + } + + if (!\array_key_exists($key, $systemConfig)) { + Assert::fail( + "system config doesn't contain key: " . $key + ); + } + + Assert::assertEquals( + $value, + $systemConfig[$key], + "config: $key doesn't contain value: $value" + ); + } + + /** + * @Given the administrator has enabled the external storage + * + * @return void + * @throws Exception + */ + public function enableExternalStorageUsingOccAsAdmin():void { + SetupHelper::runOcc( + [ + 'config:app:set', + 'core', + 'enable_external_storage', + '--value=yes' + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $response = SetupHelper::runOcc( + [ + 'config:app:get', + 'core', + 'enable_external_storage', + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $status = \trim($response['stdOut']); + Assert::assertEquals( + 'yes', + $status, + "The external storage was expected to be enabled but got '$status'" + ); + } + + /** + * @Given the administrator has added group :group to the exclude group from sharing list + * + * @param string $groups + * multiple groups can be passed as comma separated string + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function theAdministratorHasAddedGroupToTheExcludeGroupFromSharingList(string $groups):void { + $groups = \explode(',', \trim($groups)); + $groups = \array_map('trim', $groups); //removing whitespaces around group names + $groups = '"' . \implode('","', $groups) . '"'; + SetupHelper::runOcc( + [ + 'config:app:set', + 'core', + 'shareapi_exclude_groups_list', + "--value='[$groups]'" + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $response = SetupHelper::runOcc( + [ + 'config:app:get', + 'core', + 'shareapi_exclude_groups_list' + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $excludedGroupsFromResponse = (\trim($response['stdOut'])); + $excludedGroupsFromResponse = \trim($excludedGroupsFromResponse, '[]'); + Assert::assertEquals( + $groups, + $excludedGroupsFromResponse, + "'$groups' is not added to exclude groups from sharing list: '" + . $excludedGroupsFromResponse + . "' but was expected to be" + ); + } + + /** + * @Given the administrator has enabled exclude groups from sharing + * + * @return void + * @throws Exception + */ + public function theAdministratorHasEnabledExcludeGroupsFromSharingUsingTheWebui():void { + SetupHelper::runOcc( + [ + "config:app:set", + "core", + "shareapi_exclude_groups", + "--value=yes" + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $response = SetupHelper::runOcc( + [ + "config:app:get", + "core", + "shareapi_exclude_groups" + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $status = \trim($response['stdOut']); + Assert::assertEquals( + "yes", + $status, + "Exclude groups from sharing was expected to be 'yes'(enabled) but got '$status'" + ); + } + + /** + * @Given /^the administrator has (enabled|disabled) the webUI lock file action$/ + * + * @param string $enabledOrDisabled + * + * @return void + * @throws Exception + */ + public function theAdministratorHasEnabledTheWebUILockFileAction(string $enabledOrDisabled):void { + $switch = ($enabledOrDisabled !== "disabled"); + if ($switch) { + $value = 'yes'; + } else { + $value = 'no'; + } + SetupHelper::runOcc( + [ + "config:app:set", + "files", + "enable_lock_file_action", + "--value=$value" + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $response = SetupHelper::runOcc( + [ + "config:app:get", + "files", + "enable_lock_file_action" + ], + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $status = \trim($response['stdOut']); + Assert::assertEquals( + $value, + $status, + "enable_lock_file_action was expected to be '$value'($enabledOrDisabled) but got '$status'" + ); + } + + /** + * @When the administrator creates an external mount point with the following configuration about user :user using the occ command + * + * @param string $user + * @param TableNode $settings + * + * necessary attributes inside $settings table: + * 1. host - remote server url + * 2. root - remote folder name -> mount path + * 3. secure - true/false (http or https) + * 4. user - remote server user username + * 5. password - remote server user password + * 6. mount_point - external storage name + * 7. storage_backend - options: [local, owncloud, smb, googledrive, sftp, dav] + * 8. authentication_backend - options: [null::null, password::password, password::sessioncredentials] + * + * @see [`php occ files_external:backends`] to view + * detailed information of parameters used above + * + * @return void + * @throws Exception + */ + public function createExternalMountPointUsingTheOccCommand(string $user, TableNode $settings):void { + $userRenamed = $this->featureContext->getActualUsername($user); + $this->featureContext->verifyTableNodeRows( + $settings, + ["host", "root", "storage_backend", + "authentication_backend", "mount_point", + "user", "password", "secure"] + ); + $extMntSettings = $settings->getRowsHash(); + $extMntSettings['user'] = $this->featureContext->substituteInLineCodes( + $extMntSettings['user'], + $userRenamed + ); + $password = $this->featureContext->substituteInLineCodes( + $extMntSettings['password'], + $user + ); + $args = [ + "files_external:create", + "-c host=" . + $this->featureContext->substituteInLineCodes($extMntSettings['host']), + "-c root=" . $extMntSettings['root'], + "-c secure=" . $extMntSettings['secure'], + "-c user=" . $extMntSettings['user'], + "-c password=" . $password, + $extMntSettings['mount_point'], + $extMntSettings['storage_backend'], + $extMntSettings['authentication_backend'] + ]; + $this->featureContext->setOccLastCode( + $this->featureContext->runOcc($args) + ); + // add to array of created storageIds + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $mountId = \preg_replace('/\D/', '', $commandOutput); + $this->featureContext->addStorageId($extMntSettings["mount_point"], (int) $mountId); + } + + /** + * @Given the administrator has created an external mount point with the following configuration about user :user using the occ command + * + * @param string $user + * @param TableNode $settings + * + * @return void + * @throws Exception + */ + public function adminHasCreatedAnExternalMountPointWithFollowingConfigUsingTheOccCommand(string $user, TableNode $settings):void { + $this->createExternalMountPointUsingTheOccCommand($user, $settings); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + private function deleteExternalMountPointUsingTheAdmin(string $mountPoint):void { + $mount_id = $this->administratorDeletesFolder($mountPoint); + $this->featureContext->popStorageId($mount_id); + } + + /** + * @Given the administrator has deleted external storage with mount point :mountPoint + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function adminHasDeletedExternalMountPoint(string $mountPoint):void { + $this->deleteExternalMountPointUsingTheAdmin($mountPoint); + $this->theCommandShouldHaveBeenSuccessful(); + } + + /** + * @When the administrator deletes external storage with mount point :mountPoint + * + * @param string $mountPoint + * + * @return void + * @throws Exception + */ + public function adminDeletesExternalMountPoint(string $mountPoint):void { + $this->deleteExternalMountPointUsingTheAdmin($mountPoint); + } + + /** + * @Then mount point :mountPoint should not be listed as an external storage + * + * @param string $mountPoint + * + * @return void + */ + public function mountPointShouldNotBeListedAsAnExternalStorage(string $mountPoint):void { + $commandOutput = \json_decode($this->featureContext->getStdOutOfOccCommand()); + foreach ($commandOutput as $entry) { + Assert::assertNotEquals($mountPoint, $entry->mount_point); + } + } + + /** + * @Given the administrator has changed the database type to :dbType + * + * @param string $dbType + * + * @return void + * @throws Exception + */ + public function theAdministratorHasChangedTheDatabaseType(string $dbType): void { + $this->theAdministratorChangesTheDatabaseType($dbType); + $exitStatusCode = $this->featureContext->getExitStatusCodeOfOccCommand(); + + if ($exitStatusCode !== 0) { + $exceptions = $this->featureContext->findExceptions(); + $commandErr = $this->featureContext->getStdErrOfOccCommand(); + $sameTypeError = "Can not convert from $dbType to $dbType."; + $lines = SetupHelper::findLines( + $commandErr, + $sameTypeError + ); + // pass if the same type error is found + if (\count($lines) === 0) { + $msg = "The command was not successful, exit code was " . + $exitStatusCode . ".\n" . + "stdOut was: '" . + $this->featureContext->getStdOutOfOccCommand() . "'\n" . + "stdErr was: '$commandErr'\n"; + if (!empty($exceptions)) { + $msg .= ' Exceptions: ' . \implode(', ', $exceptions); + } + throw new Exception($msg); + } + } + } + + /** + * @When the administrator changes the database type to :dbType + * @When the administrator tries to change the database type to :dbType + * + * @param string $dbType + * + * @return void + * @throws Exception + */ + public function theAdministratorChangesTheDatabaseType(string $dbType): void { + $dbUser = "owncloud"; + $dbHost = $dbType; + $dbName = "owncloud"; + $dbPass = "owncloud"; + + if ($dbType === "postgres") { + $dbType = "pgsql"; + } + if ($dbType === "oracle") { + $dbUser = "autotest"; + $dbType = "oci"; + } + + $this->invokingTheCommand("db:convert-type --password=$dbPass $dbType $dbUser $dbHost $dbName"); + $this->featureContext->setDbConversionState(true); + } + + /** + * This will run after EVERY scenario. + * It will set the properties for this object. + * + * @AfterScenario + * + * @return void + * @throws Exception + */ + public function removeImportedCertificates():void { + $remainingCertificates = \array_diff($this->importedCertificates, $this->removedCertificates); + foreach ($remainingCertificates as $certificate) { + $this->invokingTheCommand("security:certificates:remove " . $certificate); + $this->theCommandShouldHaveBeenSuccessful(); + } + } + + /** + * This will run after EVERY scenario. + * It will set the properties for this object. + * + * @AfterScenario + * + * @return void + * @throws Exception + */ + public function resetDAVTechPreview():void { + if ($this->doTechPreview) { + if ($this->initialTechPreviewStatus === "") { + SetupHelper::deleteSystemConfig( + 'dav.enable.tech_preview', + $this->featureContext->getStepLineRef() + ); + } elseif ($this->initialTechPreviewStatus === 'true' && !$this->techPreviewEnabled) { + $this->enableDAVTechPreview(); + } elseif ($this->initialTechPreviewStatus === 'false' && $this->techPreviewEnabled) { + $this->disableDAVTechPreview(); + } + } + } + + /** + * This will run after EVERY scenario. + * Some local_storage tests import storage from an export file. In that case + * we have not explicitly created the storage, and so we do not explicitly + * know to delete it. So delete the local storage that is known to be used + * in tests. + * + * @AfterScenario @local_storage + * + * @return void + * @throws Exception + */ + public function removeLocalStorageIfExists():void { + $this->deleteLocalStorageFolderUsingTheOccCommand('local_storage', false); + $this->deleteLocalStorageFolderUsingTheOccCommand('local_storage2', false); + $this->deleteLocalStorageFolderUsingTheOccCommand('local_storage3', false); + $this->deleteLocalStorageFolderUsingTheOccCommand('TestMountPoint', false); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + * @throws Exception + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + SetupHelper::init( + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $this->featureContext->getBaseUrl(), + $this->featureContext->getOcPath() + ); + $ocVersion = SetupHelper::getSystemConfigValue( + 'version', + $this->featureContext->getStepLineRef() + ); + // dav.enable.tech_preview was used in some ownCloud versions before 10.4.0 + // only set it on those versions of ownCloud + if (\version_compare($ocVersion, '10.4.0') === -1) { + $this->doTechPreview = true; + $techPreviewEnabled = \trim( + SetupHelper::getSystemConfigValue( + 'dav.enable.tech_preview', + $this->featureContext->getStepLineRef() + ) + ); + $this->initialTechPreviewStatus = $techPreviewEnabled; + $this->techPreviewEnabled = $techPreviewEnabled === 'true'; + } + } + + /** + * @Then /^the system config should have dbtype set as "([^"]*)"$/ + * + * @param string $value + * + * @return void + * @throws GuzzleException + */ + public function theSystemConfigKeyShouldBeSetAs(string $value):void { + $actual_value = SetupHelper::getSystemConfigValue( + "dbtype", + $this->featureContext->getStepLineRef() + ); + $actual_value = \str_replace("\n", "", $actual_value); + Assert::assertEquals( + $value, + $actual_value, + "System config mismatched.\n + Expected dbType to be: " . $actual_value . "\n + Found: " . $value + ); + } + + /** + * @When the administrator lists migration status of app :app + * + * @param string $app + * + * @return void + * @throws Exception + */ + public function theAdministratorListsMigrationStatusOfApp(string $app):void { + $this->featureContext->setStdOutOfOccCommand(""); + $this->featureContext->setOccLastCode( + $this->featureContext->runOcc(['migrations:status', $app]) + ); + } + + /** + * @Then the following migration status should have been listed + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingMigrationStatusShouldHaveBeenListed(TableNode $table): void { + $actualOuput = $this->getMigrationStatusInfo(); + $expectedOutput = $table->getRowsHash(); + foreach ($expectedOutput as $key => $value) { + try { + $actualValue = $actualOuput[$key]; + } catch (Exception $e) { + Assert:: fail("Expected '$key' but not found!\nActual Migration status: " . \print_r($actualOuput, true)); + } + if ($this->isRegex($value)) { + $match = preg_match($value, $actualValue); + Assert:: assertEquals(1, $match, "Pattern '$value' is not matchable with value '$actualValue'"); + } else { + Assert:: assertEquals($value, $actualValue, "Expected '$key' to have value '$value' but got '$actualValue'"); + } + } + } + + /** + * @Then the Executed Migrations should equal the Available Migrations + * + * @return void + * @throws Exception + */ + public function theExecutedMigrationsShouldEqualTheAvailableMigrations(): void { + $migrationStatus = $this->getMigrationStatusInfo(); + Assert:: assertEquals($migrationStatus["Executed Migrations"], $migrationStatus["Available Migrations"], "The 'Executed Migration' is not same as 'Available Migration'"); + } + + /** + * @param string $value + * + * @return int + */ + public function isRegex($value) { + $regex = "/^\/[\s\S]+\/$/"; + return preg_match($regex, $value); + } + + /** + * @return void + */ + public function getMigrationStatusInfo() { + $commandOutput = $this->featureContext->getStdOutOfOccCommand(); + $migrationStatus = []; + if (!empty($commandOutput)) { + $infoArr = explode("\n", $commandOutput); + foreach ($infoArr as $info) { + if (!empty($info)) { + $row = \trim(\str_replace('>>', '', $info)); + $rowCol = explode(":", $row); + $migrationStatus[\trim($rowCol[0])] = \trim($rowCol[1]); + } + } + return $migrationStatus; + } else { + throwException("Migration status information is empty!"); + } + } +} diff --git a/tests/acceptance/features/bootstrap/Provisioning.php b/tests/acceptance/features/bootstrap/Provisioning.php new file mode 100644 index 000000000..64ae80f98 --- /dev/null +++ b/tests/acceptance/features/bootstrap/Provisioning.php @@ -0,0 +1,6141 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @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 + * + */ + +use Behat\Gherkin\Node\TableNode; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\Assert; +use TestHelpers\OcsApiHelper; +use TestHelpers\SetupHelper; +use TestHelpers\UserHelper; +use TestHelpers\HttpRequestHelper; +use TestHelpers\OcisHelper; +use TestHelpers\WebDavHelper; +use Laminas\Ldap\Exception\LdapException; +use Laminas\Ldap\Ldap; + +/** + * Functions for provisioning of users and groups + */ +trait Provisioning { + /** + * list of users that were created on the local server during test runs + * key is the lowercase username, value is an array of user attributes + * + * @var array + */ + private $createdUsers = []; + + /** + * list of users that were created on the remote server during test runs + * key is the lowercase username, value is an array of user attributes + * + * @var array + */ + private $createdRemoteUsers = []; + + /** + * @var array + */ + private $enabledApps = []; + + /** + * @var array + */ + private $disabledApps = []; + + /** + * @var array + */ + private $startingGroups = []; + + /** + * @var array + */ + private $createdRemoteGroups = []; + + /** + * @var array + */ + private $createdGroups = []; + + /** + * @var array + */ + private $userResponseFields = [ + "enabled", "quota", "email", "displayname", "home", "two_factor_auth_enabled", + "quota definition", "quota free", "quota user", "quota total", "quota relative" + ]; + + /** + * Check if this is the admin group. That group is always a local group in + * ownCloud10, even if other groups come from LDAP. + * + * @param string $groupname + * + * @return boolean + */ + public function isLocalAdminGroup(string $groupname):bool { + return ($groupname === "admin"); + } + + /** + * Usernames are not case-sensitive, and can generally be specified with any + * mix of upper and lower case. For remembering usernames use the normalized + * form so that "alice" and "Alice" are remembered as the same user. + * + * @param string|null $username + * + * @return string + */ + public function normalizeUsername(?string $username):string { + return \strtolower((string)$username); + } + + /** + * @return array + */ + public function getCreatedUsers():array { + return $this->createdUsers; + } + + /** + * @return boolean + */ + public function someUsersHaveBeenCreated():bool { + return (\count($this->createdUsers) > 0); + } + + /** + * @return array + */ + public function getCreatedGroups():array { + return $this->createdGroups; + } + + /** + * returns the display name of the current user + * if no "Display Name" is set the user-name is returned instead + * + * @return string + */ + public function getCurrentUserDisplayName():string { + return $this->getUserDisplayName($this->getCurrentUser()); + } + + /** + * returns the display name of a user + * if no "Display Name" is set the username is returned instead + * + * @param string $username + * + * @return string + */ + public function getUserDisplayName(string $username):string { + $normalizedUsername = $this->normalizeUsername($username); + if (isset($this->createdUsers[$normalizedUsername]['displayname'])) { + $displayName = (string) $this->createdUsers[$normalizedUsername]['displayname']; + if ($displayName !== '') { + return $displayName; + } + } + return $username; + } + + /** + * @param string $user + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function rememberUserDisplayName(string $user, string $displayName):void { + $normalizedUsername = $this->normalizeUsername($user); + if ($this->isAdminUsername($normalizedUsername)) { + $this->adminDisplayName = $displayName; + } else { + if ($this->currentServer === 'LOCAL') { + if (\array_key_exists($normalizedUsername, $this->createdUsers)) { + $this->createdUsers[$normalizedUsername]['displayname'] = $displayName; + } else { + throw new Exception( + __METHOD__ . " tried to remember display name '$displayName' for nonexistent local user '$user'" + ); + } + } elseif ($this->currentServer === 'REMOTE') { + if (\array_key_exists($normalizedUsername, $this->createdRemoteUsers)) { + $this->createdRemoteUsers[$normalizedUsername]['displayname'] = $displayName; + } else { + throw new Exception( + __METHOD__ . " tried to remember display name '$displayName' for nonexistent federated user '$user'" + ); + } + } + } + } + + /** + * @param string $user + * @param string $emailAddress + * + * @return void + * @throws Exception + */ + public function rememberUserEmailAddress(string $user, string $emailAddress):void { + $normalizedUsername = $this->normalizeUsername($user); + if ($this->isAdminUsername($normalizedUsername)) { + $this->adminEmailAddress = $emailAddress; + } else { + if ($this->currentServer === 'LOCAL') { + if (\array_key_exists($normalizedUsername, $this->createdUsers)) { + $this->createdUsers[$normalizedUsername]['email'] = $emailAddress; + } else { + throw new Exception( + __METHOD__ . " tried to remember email address '$emailAddress' for nonexistent local user '$user'" + ); + } + } elseif ($this->currentServer === 'REMOTE') { + if (\array_key_exists($normalizedUsername, $this->createdRemoteUsers)) { + $this->createdRemoteUsers[$normalizedUsername]['email'] = $emailAddress; + } else { + throw new Exception( + __METHOD__ . " tried to remember email address '$emailAddress' for nonexistent federated user '$user'" + ); + } + } + } + } + + /** + * returns an array of the user display names, keyed by normalized username + * if no "Display Name" is set the user-name is returned instead + * + * @return array + */ + public function getCreatedUserDisplayNames():array { + $result = []; + foreach ($this->getCreatedUsers() as $normalizedUsername => $user) { + $result[$normalizedUsername] = $this->getUserDisplayName($normalizedUsername); + } + return $result; + } + + /** + * @param string $user + * @param string $attribute + * + * @return mixed + * @throws Exception + */ + public function getAttributeOfCreatedUser(string $user, string $attribute) { + $usersList = $this->getCreatedUsers(); + $normalizedUsername = $this->normalizeUsername($user); + if (\array_key_exists($normalizedUsername, $usersList)) { + // provide attributes only if the user exists + if ($usersList[$normalizedUsername]["shouldExist"]) { + if (\array_key_exists($attribute, $usersList[$normalizedUsername])) { + return $usersList[$normalizedUsername][$attribute]; + } else { + throw new Exception( + __METHOD__ . ": User '$user' has no attribute with name '$attribute'." + ); + } + } else { + throw new Exception( + __METHOD__ . ": User '$user' has been deleted." + ); + } + } else { + throw new Exception( + __METHOD__ . ": User '$user' does not exist in the created list." + ); + } + } + + /** + * @param string $group + * @param string $attribute + * + * @return mixed + * @throws Exception + */ + public function getAttributeOfCreatedGroup(string $group, string $attribute) { + $groupsList = $this->getCreatedGroups(); + if (\array_key_exists($group, $groupsList)) { + // provide attributes only if the group exists + if ($groupsList[$group]["shouldExist"]) { + if (\array_key_exists($attribute, $groupsList[$group])) { + return $groupsList[$group][$attribute]; + } else { + throw new Exception( + __METHOD__ . ": Group '$group' has no attribute with name '$attribute'." + ); + } + } else { + throw new Exception( + __METHOD__ . ": Group '$group' has been deleted." + ); + } + } else { + throw new Exception( + __METHOD__ . ": Group '$group' does not exist in the created list." + ); + } + } + + /** + * returns an array of the group display names, keyed by group name + * currently group name and display name are always the same, so this + * function is a convenience for getting the group names in a similar + * format to what getCreatedUserDisplayNames() returns + * + * @return array + */ + public function getCreatedGroupDisplayNames():array { + $result = []; + foreach ($this->getCreatedGroups() as $groupName => $groupData) { + $result[$groupName] = $groupName; + } + return $result; + } + + /** + * + * @param string $username + * + * @return string password + * @throws Exception + */ + public function getUserPassword(string $username):string { + $normalizedUsername = $this->normalizeUsername($username); + if ($normalizedUsername === $this->getAdminUsername()) { + $password = $this->getAdminPassword(); + } elseif (\array_key_exists($normalizedUsername, $this->createdUsers)) { + $password = $this->createdUsers[$normalizedUsername]['password']; + } elseif (\array_key_exists($normalizedUsername, $this->createdRemoteUsers)) { + $password = $this->createdRemoteUsers[$normalizedUsername]['password']; + } else { + throw new Exception( + "user '$username' was not created by this test run" + ); + } + + //make sure the function always returns a string + return (string) $password; + } + + /** + * + * @param string $username + * + * @return boolean + * @throws Exception + */ + public function theUserShouldExist(string $username):bool { + $normalizedUsername = $this->normalizeUsername($username); + if (\array_key_exists($normalizedUsername, $this->createdUsers)) { + return $this->createdUsers[$normalizedUsername]['shouldExist']; + } + + if (\array_key_exists($normalizedUsername, $this->createdRemoteUsers)) { + return $this->createdRemoteUsers[$normalizedUsername]['shouldExist']; + } + + throw new Exception( + __METHOD__ + . " user '$username' was not created by this test run" + ); + } + + /** + * + * @param string $groupname + * + * @return boolean + * @throws Exception + */ + public function theGroupShouldExist(string $groupname):bool { + if (\array_key_exists($groupname, $this->createdGroups)) { + if (\array_key_exists('shouldExist', $this->createdGroups[$groupname])) { + return $this->createdGroups[$groupname]['shouldExist']; + } + return false; + } + + if (\array_key_exists($groupname, $this->createdRemoteGroups)) { + if (\array_key_exists('shouldExist', $this->createdRemoteGroups[$groupname])) { + return $this->createdRemoteGroups[$groupname]['shouldExist']; + } + return false; + } + + throw new Exception( + __METHOD__ + . " group '$groupname' was not created by this test run" + ); + } + + /** + * + * @param string $groupname + * + * @return boolean + * @throws Exception + */ + public function theGroupShouldBeAbleToBeDeleted(string $groupname):bool { + if (\array_key_exists($groupname, $this->createdGroups)) { + return $this->createdGroups[$groupname]['possibleToDelete'] ?? true; + } + + if (\array_key_exists($groupname, $this->createdRemoteGroups)) { + return $this->createdRemoteGroups[$groupname]['possibleToDelete'] ?? true; + } + + throw new Exception( + __METHOD__ + . " group '$groupname' was not created by this test run" + ); + } + + /** + * @When /^the administrator creates user "([^"]*)" using the provisioning API$/ + * + * @param string|null $user + * + * @return void + * @throws Exception + */ + public function adminCreatesUserUsingTheProvisioningApi(?string $user):void { + $this->createUser( + $user, + null, + null, + null, + true, + 'api' + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has been created with default attributes in the database user backend$/ + * + * @param string|null $user + * + * @return void + * @throws Exception + */ + public function userHasBeenCreatedOnDatabaseBackend(?string $user):void { + $this->adminCreatesUserUsingTheProvisioningApi($user); + $this->userShouldExist($user); + } + + /** + * @Given /^user "([^"]*)" has been created with default attributes and (tiny|small|large)\s?skeleton files$/ + * + * @param string $user + * @param string $skeletonType + * @param boolean $skeleton + * + * @return void + * @throws Exception + */ + public function userHasBeenCreatedWithDefaultAttributes( + string $user, + string $skeletonType = "", + bool $skeleton = true + ):void { + if ($skeletonType === "") { + $skeletonType = $this->getSmallestSkeletonDirName(); + } + + $originalSkeletonPath = $this->setSkeletonDirByType($skeletonType); + + try { + $this->createUser( + $user, + null, + null, + null, + true, + null, + true, + $skeleton + ); + $this->userShouldExist($user); + } finally { + $this->setSkeletonDir($originalSkeletonPath); + } + } + + /** + * @Given /^user "([^"]*)" has been created with default attributes and without skeleton files$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userHasBeenCreatedWithDefaultAttributesAndWithoutSkeletonFiles(string $user):void { + $this->userHasBeenCreatedWithDefaultAttributes($user); + $this->resetOccLastCode(); + } + + /** + * @Given these users have been created with default attributes and without skeleton files: + * expects a table of users with the heading + * "|username|" + * + * @param TableNode $table + * + * @return void + * @throws Exception|GuzzleException + */ + public function theseUsersHaveBeenCreatedWithDefaultAttributesAndWithoutSkeletonFiles(TableNode $table):void { + $originalSkeletonPath = $this->setSkeletonDirByType($this->getSmallestSkeletonDirName()); + try { + $this->createTheseUsers(true, true, true, $table); + } finally { + // restore skeleton directory even if user creation failed + $this->setSkeletonDir($originalSkeletonPath); + } + } + + /** + * @Given /^these users have been created without skeleton files ?(and not initialized|):$/ + * expects a table of users with the heading + * "|username|password|displayname|email|" + * password, displayname & email are optional + * + * @param TableNode $table + * @param string $doNotInitialize + * + * @return void + * @throws Exception + */ + public function theseUsersHaveBeenCreatedWithoutSkeletonFiles(TableNode $table, string $doNotInitialize):void { + $this->theseUsersHaveBeenCreated("", "", $doNotInitialize, $table); + } + + /** + * @Given the administrator has set the system language to :defaultLanguage + * + * @param string $defaultLanguage + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetTheSystemLanguageTo(string $defaultLanguage):void { + $this->runOcc( + ["config:system:set default_language --value $defaultLanguage"] + ); + } + + /** + * + * @param string $path + * + * @return void + */ + public function importLdifFile(string $path):void { + $ldifData = \file_get_contents($path); + $this->importLdifData($ldifData); + } + + /** + * imports an ldif string + * + * @param string $ldifData + * + * @return void + */ + public function importLdifData(string $ldifData):void { + $items = Laminas\Ldap\Ldif\Encoder::decode($ldifData); + if (isset($items['dn'])) { + //only one item in the ldif data + $this->ldap->add($items['dn'], $items); + } else { + foreach ($items as $item) { + if (isset($item["objectclass"])) { + if (\in_array("posixGroup", $item["objectclass"])) { + \array_push($this->ldapCreatedGroups, $item["cn"][0]); + $this->addGroupToCreatedGroupsList($item["cn"][0]); + } elseif (\in_array("inetOrgPerson", $item["objectclass"])) { + \array_push($this->ldapCreatedUsers, $item["uid"][0]); + $this->addUserToCreatedUsersList($item["uid"][0], $item["userpassword"][0]); + } + } + $this->ldap->add($item['dn'], $item); + } + } + } + + /** + * @param array $suiteParameters + * + * @return void + * @throws Exception + * @throws \LdapException + */ + public function connectToLdap(array $suiteParameters):void { + $useSsl = false; + + $this->ldapBaseDN = OcisHelper::getBaseDN(); + $this->ldapUsersOU = OcisHelper::getUsersOU(); + $this->ldapGroupsOU = OcisHelper::getGroupsOU(); + $this->ldapGroupSchema = OcisHelper::getGroupSchema(); + $this->ldapHost = OcisHelper::getHostname(); + $this->ldapPort = OcisHelper::getLdapPort(); + $useSsl = OcisHelper::useSsl(); + $this->ldapAdminUser = OcisHelper::getBindDN(); + $this->ldapAdminPassword = OcisHelper::getBindPassword(); + $this->skipImportLdif = (\getenv("REVA_LDAP_SKIP_LDIF_IMPORT") === "true"); + if ($useSsl === true) { + \putenv('LDAPTLS_REQCERT=never'); + } + + if ($this->ldapAdminPassword === "") { + $this->ldapAdminPassword = (string)$suiteParameters['ldapAdminPassword']; + } + $options = [ + 'host' => $this->ldapHost, + 'port' => $this->ldapPort, + 'password' => $this->ldapAdminPassword, + 'bindRequiresDn' => true, + 'useSsl' => $useSsl, + 'baseDn' => $this->ldapBaseDN, + 'username' => $this->ldapAdminUser + ]; + $this->ldap = new Ldap($options); + $this->ldap->bind(); + + $ldifFile = __DIR__ . (string)$suiteParameters['ldapInitialUserFilePath']; + if (OcisHelper::isTestingParallelDeployment()) { + $behatYml = \getenv("BEHAT_YML"); + if ($behatYml) { + $configPath = \dirname($behatYml); + $ldifFile = $configPath . "/" . \basename($ldifFile); + } + } + if (!$this->skipImportLdif) { + $this->importLdifFile($ldifFile); + } + $this->theLdapUsersHaveBeenResynced(); + } + + /** + * @Given the LDAP users have been resynced + * + * @return void + * @throws Exception + */ + public function theLdapUsersHaveBeenReSynced():void { + // we need to sync ldap users when testing for parallel deployment + if (!OcisHelper::isTestingOnOcisOrReva() || OcisHelper::isTestingParallelDeployment()) { + $occResult = SetupHelper::runOcc( + ['user:sync', 'OCA\User_LDAP\User_Proxy', '-m', 'remove'], + $this->getStepLineRef() + ); + if ($occResult['code'] !== "0") { + throw new Exception(__METHOD__ . " could not sync LDAP users " . $occResult['stdErr']); + } + } + } + + /** + * prepares suitable nested array with user-attributes for multiple users to be created + * + * @param boolean $setDefaultAttributes + * @param array $table + * + * @return array + * @throws JsonException + */ + public function buildUsersAttributesArray(bool $setDefaultAttributes, array $table):array { + $usersAttributes = []; + foreach ($table as $row) { + $userAttribute['userid'] = $this->getActualUsername($row['username']); + + if (isset($row['displayname'])) { + $userAttribute['displayName'] = $row['displayname']; + } elseif ($setDefaultAttributes) { + $userAttribute['displayName'] = $this->getDisplayNameForUser($row['username']); + if ($userAttribute['displayName'] === null) { + $userAttribute['displayName'] = $this->getDisplayNameForUser('regularuser'); + } + } else { + $userAttribute['displayName'] = null; + } + if (isset($row['email'])) { + $userAttribute['email'] = $row['email']; + } elseif ($setDefaultAttributes) { + $userAttribute['email'] = $this->getEmailAddressForUser($row['username']); + if ($userAttribute['email'] === null) { + $userAttribute['email'] = $row['username'] . '@owncloud.com'; + } + } else { + $userAttribute['email'] = null; + } + + if (isset($row['password'])) { + $userAttribute['password'] = $this->getActualPassword($row['password']); + } else { + $userAttribute['password'] = $this->getPasswordForUser($row['username']); + } + // Add request body to the bodies array. we will use that later to loop through created users. + $usersAttributes[] = $userAttribute; + } + return $usersAttributes; + } + + /** + * creates a user in the ldap server + * the created user is added to `createdUsersList` + * ldap users are re-synced after creating a new user + * + * @param array $setting + * + * @return void + * @throws Exception + */ + public function createLdapUser(array $setting):void { + $ou = $this->ldapUsersOU ; + // Some special characters need to be escaped in LDAP DN and attributes + // The special characters allowed in a username (UID) are +_.@- + // Of these, only + has to be escaped. + $userId = \str_replace('+', '\+', $setting["userid"]); + $newDN = 'uid=' . $userId . ',ou=' . $ou . ',' . $this->ldapBaseDN; + + //pick a high number as uidnumber to make sure there are no conflicts with existing uidnumbers + $uidNumber = \count($this->ldapCreatedUsers) + 30000; + $entry = []; + $entry['cn'] = $userId; + $entry['sn'] = $userId; + $entry['uid'] = $setting["userid"]; + $entry['homeDirectory'] = '/home/openldap/' . $setting["userid"]; + $entry['objectclass'][] = 'posixAccount'; + $entry['objectclass'][] = 'inetOrgPerson'; + $entry['objectclass'][] = 'organizationalPerson'; + $entry['objectclass'][] = 'person'; + $entry['objectclass'][] = 'top'; + + $entry['userPassword'] = $setting["password"]; + if (isset($setting["displayName"])) { + $entry['displayName'] = $setting["displayName"]; + } + if (isset($setting["email"])) { + $entry['mail'] = $setting["email"]; + } elseif (OcisHelper::isTestingOnOcis()) { + $entry['mail'] = $userId . '@owncloud.com'; + } + $entry['gidNumber'] = 5000; + $entry['uidNumber'] = $uidNumber; + + if (OcisHelper::isTestingOnOcis()) { + $entry['objectclass'][] = 'ownCloud'; + $entry['ownCloudUUID'] = WebDavHelper::generateUUIDv4(); + } + if (OcisHelper::isTestingParallelDeployment()) { + $entry['ownCloudSelector'] = $this->getOCSelector(); + } + + if ($this->federatedServerExists()) { + if (!\in_array($setting['userid'], $this->ldapCreatedUsers)) { + $this->ldap->add($newDN, $entry); + } + } else { + $this->ldap->add($newDN, $entry); + } + $this->ldapCreatedUsers[] = $setting["userid"]; + $this->theLdapUsersHaveBeenReSynced(); + } + + /** + * @param string $group group name + * + * @return void + * @throws Exception + * @throws LdapException + */ + public function createLdapGroup(string $group):void { + $baseDN = $this->getLdapBaseDN(); + $newDN = 'cn=' . $group . ',ou=' . $this->ldapGroupsOU . ',' . $baseDN; + $entry = []; + $entry['cn'] = $group; + $entry['objectclass'][] = 'top'; + + if ($this->ldapGroupSchema == "rfc2307") { + $entry['objectclass'][] = 'posixGroup'; + $entry['gidNumber'] = 5000; + } else { + $entry['objectclass'][] = 'groupOfNames'; + $entry['member'] = ""; + } + if (OcisHelper::isTestingOnOcis()) { + $entry['objectclass'][] = 'ownCloud'; + $entry['ownCloudUUID'] = WebDavHelper::generateUUIDv4(); + } + $this->ldap->add($newDN, $entry); + $this->ldapCreatedGroups[] = $group; + } + + /** + * + * @param string $configId + * @param string $configKey + * @param string $configValue + * + * @return void + * @throws Exception + */ + public function setLdapSetting(string $configId, string $configKey, string $configValue):void { + if ($configValue === "") { + $configValue = "''"; + } + $substitutions = [ + [ + "code" => "%ldap_host_without_scheme%", + "function" => [ + $this, + "getLdapHostWithoutScheme" + ], + "parameter" => [] + ], + [ + "code" => "%ldap_host%", + "function" => [ + $this, + "getLdapHost" + ], + "parameter" => [] + ], + [ + "code" => "%ldap_port%", + "function" => [ + $this, + "getLdapPort" + ], + "parameter" => [] + ] + ]; + $configValue = $this->substituteInLineCodes( + $configValue, + null, + [], + $substitutions + ); + $occResult = SetupHelper::runOcc( + ['ldap:set-config', $configId, $configKey, $configValue], + $this->getStepLineRef() + ); + if ($occResult['code'] !== "0") { + throw new Exception( + __METHOD__ . " could not set LDAP setting " . $occResult['stdErr'] + ); + } + } + + /** + * deletes LDAP users|groups created during test + * + * @return void + * @throws Exception + */ + public function deleteLdapUsersAndGroups():void { + $isOcisOrReva = OcisHelper::isTestingOnOcisOrReva(); + foreach ($this->ldapCreatedUsers as $user) { + if ($isOcisOrReva) { + $this->ldap->delete( + "uid=" . ldap_escape($user, "", LDAP_ESCAPE_DN) . ",ou=" . $this->ldapUsersOU . "," . $this->ldapBaseDN, + ); + } + $this->rememberThatUserIsNotExpectedToExist($user); + } + foreach ($this->ldapCreatedGroups as $group) { + if ($isOcisOrReva) { + $this->ldap->delete( + "cn=" . ldap_escape($group, "", LDAP_ESCAPE_DN) . ",ou=" . $this->ldapGroupsOU . "," . $this->ldapBaseDN, + ); + } + $this->rememberThatGroupIsNotExpectedToExist($group); + } + if (!$isOcisOrReva || !$this->skipImportLdif) { + //delete ou from LDIF import + $this->ldap->delete( + "ou=" . $this->ldapUsersOU . "," . $this->ldapBaseDN, + true + ); + //delete all created ldap groups + $this->ldap->delete( + "ou=" . $this->ldapGroupsOU . "," . $this->ldapBaseDN, + true + ); + } + $this->theLdapUsersHaveBeenResynced(); + } + + /** + * Sets back old settings + * + * @return void + * @throws Exception + */ + public function resetOldLdapConfig():void { + $toDeleteLdapConfig = $this->getToDeleteLdapConfigs(); + foreach ($toDeleteLdapConfig as $configId) { + SetupHelper::runOcc( + ['ldap:delete-config', $configId], + $this->getStepLineRef() + ); + } + foreach ($this->oldLdapConfig as $configId => $settings) { + foreach ($settings as $configKey => $configValue) { + $this->setLdapSetting($configId, $configKey, $configValue); + } + } + foreach ($this->toDeleteDNs as $dn) { + $this->getLdap()->delete($dn, true); + } + } + + /** + * Manually add skeleton files for a single user on OCIS and reva systems + * + * @param string $user + * @param string $password + * + * @return void + * @throws Exception + */ + public function manuallyAddSkeletonFilesForUser(string $user, string $password):void { + $settings = []; + $setting["userid"] = $user; + $setting["password"] = $password; + $settings[] = $setting; + $this->manuallyAddSkeletonFiles($settings); + } + + /** + * Manually add skeleton files on OCIS and reva systems + * + * @param array $usersAttributes + * + * @return void + * @throws Exception + */ + public function manuallyAddSkeletonFiles(array $usersAttributes):void { + if ($this->isEmptySkeleton()) { + // The empty skeleton has no files. There is nothing to do so return early. + return; + } + $skeletonDir = \getenv("SKELETON_DIR"); + $revaRoot = \getenv("OCIS_REVA_DATA_ROOT"); + $skeletonStrategy = \getenv("OCIS_SKELETON_STRATEGY"); + if (!$skeletonStrategy) { + $skeletonStrategy = 'upload'; //slower, but safer, so make it the default + } + if ($skeletonStrategy !== 'upload' && $skeletonStrategy !== 'copy') { + throw new Exception( + 'Wrong OCIS_SKELETON_STRATEGY environment variable. ' . + 'OCIS_SKELETON_STRATEGY has to be set to "upload" or "copy"' + ); + } + if (!$skeletonDir) { + throw new Exception('Missing SKELETON_DIR environment variable, cannot copy skeleton files for OCIS'); + } + if ($skeletonStrategy === 'copy' && !$revaRoot) { + throw new Exception( + 'OCIS_SKELETON_STRATEGY is set to "copy" ' . + 'but no "OCIS_REVA_DATA_ROOT" given' + ); + } + if ($skeletonStrategy === 'upload') { + foreach ($usersAttributes as $userAttributes) { + OcisHelper::recurseUpload( + $this->getBaseUrl(), + $skeletonDir, + $userAttributes['userid'], + $userAttributes['password'], + $this->getStepLineRef() + ); + } + } + if ($skeletonStrategy === 'copy') { + foreach ($usersAttributes as $userAttributes) { + $user = $userAttributes['userid']; + $dataDir = $revaRoot . "$user/files"; + if (!\file_exists($dataDir)) { + \mkdir($dataDir, 0777, true); + } + OcisHelper::recurseCopy($skeletonDir, $dataDir); + } + } + } + + /** + * This function will allow us to send user creation requests in parallel. + * This will be faster in comparison to waiting for each request to complete before sending another request. + * + * @param boolean $initialize + * @param array|null $usersAttributes + * @param string|null $method create the user with "ldap" or "api" + * @param boolean $skeleton + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function usersHaveBeenCreated( + bool $initialize, + ?array $usersAttributes, + ?string $method = null, + ?bool $skeleton = true + ) { + $requests = []; + $client = HttpRequestHelper::createClient( + $this->getAdminUsername(), + $this->getAdminPassword() + ); + + $useLdap = false; + $useGraph = false; + if ($method === null) { + $useLdap = $this->isTestingWithLdap(); + $useGraph = OcisHelper::isTestingWithGraphApi(); + } elseif ($method === "ldap") { + $useLdap = true; + } elseif ($method === "graph") { + $useGraph = true; + } + + foreach ($usersAttributes as $i => $userAttributes) { + if ($useLdap) { + $this->createLdapUser($userAttributes); + } else { + $attributesToCreateUser['userid'] = $userAttributes['userid']; + $attributesToCreateUser['password'] = $userAttributes['password']; + $attributesToCreateUser['displayname'] = $userAttributes['displayName']; + if (OcisHelper::isTestingOnOcisOrReva()) { + $attributesToCreateUser['username'] = $userAttributes['userid']; + if ($userAttributes['email'] === null) { + Assert::assertArrayHasKey( + 'userid', + $userAttributes, + __METHOD__ . " userAttributes array does not have key 'userid'" + ); + $attributesToCreateUser['email'] = $userAttributes['userid'] . '@owncloud.com'; + } else { + $attributesToCreateUser['email'] = $userAttributes['email']; + } + } + if ($useGraph) { + $body = \TestHelpers\GraphHelper::prepareCreateUserPayload( + $attributesToCreateUser['userid'], + $attributesToCreateUser['password'], + $attributesToCreateUser['email'], + $attributesToCreateUser['displayname'] + ); + $request = \TestHelpers\GraphHelper::createRequest( + $this->getBaseUrl(), + $this->getStepLineRef(), + "POST", + 'users', + $body, + ); + } else { + // Create a OCS request for creating the user. The request is not sent to the server yet. + $request = OcsApiHelper::createOcsRequest( + $this->getBaseUrl(), + 'POST', + "/cloud/users", + $this->stepLineRef, + $attributesToCreateUser + ); + } + // Add the request to the $requests array so that they can be sent in parallel. + $requests[] = $request; + } + } + + $exceptionToThrow = null; + if (!$useLdap) { + $results = HttpRequestHelper::sendBatchRequest($requests, $client); + // Check all requests to inspect failures. + foreach ($results as $key => $e) { + if ($e instanceof ClientException) { + if ($useGraph) { + $responseBody = $this->getJsonDecodedResponse($e->getResponse()); + $httpStatusCode = $e->getResponse()->getStatusCode(); + $graphStatusCode = $responseBody['error']['code']; + $messageText = $responseBody['error']['message']; + $exceptionToThrow = new Exception( + __METHOD__ . + " Unexpected failure when creating the user '" . + $usersAttributes[$key]['userid'] . "'" . + "\nHTTP status $httpStatusCode " . + "\nGraph status $graphStatusCode " . + "\nError message $messageText" + ); + } else { + $responseXml = $this->getResponseXml($e->getResponse(), __METHOD__); + $messageText = (string) $responseXml->xpath("/ocs/meta/message")[0]; + $ocsStatusCode = (string) $responseXml->xpath("/ocs/meta/statuscode")[0]; + $httpStatusCode = $e->getResponse()->getStatusCode(); + $reasonPhrase = $e->getResponse()->getReasonPhrase(); + $exceptionToThrow = new Exception( + __METHOD__ . " Unexpected failure when creating the user '" . + $usersAttributes[$key]['userid'] . "': HTTP status $httpStatusCode " . + "HTTP reason $reasonPhrase OCS status $ocsStatusCode " . + "OCS message $messageText" + ); + } + } + } + } + + // Create requests for setting displayname and email for the newly created users. + // These values cannot be set while creating the user, so we have to edit the newly created user to set these values. + $users = []; + $editData = []; + foreach ($usersAttributes as $userAttributes) { + $users[] = $userAttributes['userid']; + if ($useGraph) { + // for graph api, we need to save the user id to be able to add it in some group + // can be fetched with the "onPremisesSamAccountName" i.e. userid + $this->graphContext->adminHasRetrievedUserUsingTheGraphApi($userAttributes['userid']); + $userAttributes['id'] = $this->getJsonDecodedResponse()['id']; + } else { + $userAttributes['id'] = null; + } + $this->addUserToCreatedUsersList( + $userAttributes['userid'], + $userAttributes['password'], + $userAttributes['displayName'], + $userAttributes['email'], + $userAttributes['id'] + ); + + if (OcisHelper::isTestingOnOcisOrReva()) { + OcisHelper::createEOSStorageHome( + $this->getBaseUrl(), + $userAttributes['userid'], + $userAttributes['password'], + $this->getStepLineRef() + ); + // We don't need to set displayName and email while running in oCIS + // As they are set when creating the user + continue; + } + if (isset($userAttributes['displayName'])) { + $editData[] = ['user' => $userAttributes['userid'], 'key' => 'displayname', 'value' => $userAttributes['displayName']]; + } + if (isset($userAttributes['email'])) { + $editData[] = ['user' => $userAttributes['userid'], 'key' => 'email', 'value' => $userAttributes['email']]; + } + } + // Edit the users in parallel to make the process faster. + if (!OcisHelper::isTestingOnOcisOrReva() && !$useLdap && \count($editData) > 0) { + UserHelper::editUserBatch( + $this->getBaseUrl(), + $editData, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->stepLineRef + ); + } + + if (isset($exceptionToThrow)) { + throw $exceptionToThrow; + } + + // If the user should have skeleton files, and we are testing on OCIS + // then do some work to "manually" put the skeleton files in place. + // When testing on ownCloud 10 the user is already getting whatever + // skeleton dir is defined in the server-under-test. + if ($skeleton && OcisHelper::isTestingOnOcisOrReva()) { + $this->manuallyAddSkeletonFiles($usersAttributes); + } + + if ($initialize && ($this->isEmptySkeleton() || !OcisHelper::isTestingOnOcis())) { + // We need to initialize each user using the individual authentication of each user. + // That is not possible in Guzzle6 batch mode. So we do it with normal requests in serial. + $this->initializeUsers($users); + } + } + + /** + * @When /^the administrator creates these users with ?(default attributes and|) skeleton files ?(but not initialized|):$/ + * + * expects a table of users with the heading + * "|username|password|displayname|email|" + * password, displayname & email are optional + * + * @param string $setDefaultAttributes + * @param string $doNotInitialize + * @param TableNode $table + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function theAdministratorCreatesTheseUsers( + string $setDefaultAttributes, + string $doNotInitialize, + TableNode $table + ): void { + $this->verifyTableNodeColumns($table, ['username'], ['displayname', 'email', 'password']); + $table = $table->getColumnsHash(); + $setDefaultAttributes = $setDefaultAttributes !== ""; + $initialize = $doNotInitialize === ""; + $usersAttributes = $this->buildUsersAttributesArray($setDefaultAttributes, $table); + $this->usersHaveBeenCreated( + $initialize, + $usersAttributes + ); + } + + /** + * expects a table of users with the heading + * "|username|password|displayname|email|" + * password, displayname & email are optional + * + * @param boolean $setDefaultAttributes + * @param boolean $initialize + * @param boolean $skeleton + * @param TableNode $table + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function createTheseUsers(bool $setDefaultAttributes, bool $initialize, bool $skeleton, TableNode $table):void { + $this->verifyTableNodeColumns($table, ['username'], ['displayname', 'email', 'password']); + $table = $table->getColumnsHash(); + $usersAttributes = $this->buildUsersAttributesArray($setDefaultAttributes, $table); + $this->usersHaveBeenCreated( + $initialize, + $usersAttributes, + null, + $skeleton + ); + foreach ($usersAttributes as $expectedUser) { + $this->userShouldExist($expectedUser["userid"]); + } + } + + /** + * @Given /^these users have been created with ?(default attributes and|) (tiny|small|large)\s?skeleton files ?(but not initialized|):$/ + * + * expects a table of users with the heading + * "|username|password|displayname|email|" + * password, displayname & email are optional + * + * @param string $defaultAttributesText + * @param string $skeletonType + * @param string $doNotInitialize + * @param TableNode $table + * + * @return void + * @throws Exception|GuzzleException + */ + public function theseUsersHaveBeenCreated( + string $defaultAttributesText, + string $skeletonType, + string $doNotInitialize, + TableNode $table + ):void { + if ($skeletonType === "") { + $skeletonType = $this->getSmallestSkeletonDirName(); + } + + $originalSkeletonPath = $this->setSkeletonDirByType($skeletonType); + $setDefaultAttributes = $defaultAttributesText !== ""; + $initialize = $doNotInitialize === ""; + try { + $this->createTheseUsers($setDefaultAttributes, $initialize, true, $table); + } finally { + // The effective skeleton directory is the one when the user is initialized + // If we did not initialize the user on creation, then we need to leave + // the skeleton directory in effect so that it applies when some action + // happens later in the scenario that causes the user to be initialized. + if ($initialize) { + $this->setSkeletonDir($originalSkeletonPath); + } + } + } + + /** + * @When the administrator changes the password of user :user to :password using the provisioning API + * + * @param string $user + * @param string $password + * + * @return void + * @throws Exception + */ + public function adminChangesPasswordOfUserToUsingTheProvisioningApi( + string $user, + string $password + ):void { + $this->response = UserHelper::editUser( + $this->getBaseUrl(), + $user, + 'password', + $password, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef() + ); + } + + /** + * @Given the administrator has changed the password of user :user to :password + * + * @param string $user + * @param string $password + * + * @return void + * @throws Exception + */ + public function adminHasChangedPasswordOfUserTo( + string $user, + string $password + ):void { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminChangesPasswordOfUserToUsingTheGraphApi( + $user, + $password + ); + } else { + $this->adminChangesPasswordOfUserToUsingTheProvisioningApi( + $user, + $password + ); + } + $this->theHTTPStatusCodeShouldBe( + 200, + "could not change password of user $user" + ); + } + + /** + * @When /^user "([^"]*)" (enables|disables) app "([^"]*)"$/ + * + * @param string $user + * @param string $action enables or disables + * @param string $app + * + * @return void + */ + public function userEnablesOrDisablesApp(string $user, string $action, string $app):void { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/apps/$app"; + if ($action === 'enables') { + $this->response = HttpRequestHelper::post( + $fullUrl, + $this->getStepLineRef(), + $user, + $this->getPasswordForUser($user) + ); + } else { + $this->response = HttpRequestHelper::delete( + $fullUrl, + $this->getStepLineRef(), + $user, + $this->getPasswordForUser($user) + ); + } + } + + /** + * @When /^the administrator (enables|disables) app "([^"]*)"$/ + * + * @param string $action enables or disables + * @param string $app + * + * @return void + */ + public function adminEnablesOrDisablesApp(string $action, string $app):void { + $this->userEnablesOrDisablesApp( + $this->getAdminUsername(), + $action, + $app + ); + } + + /** + * @Given /^app "([^"]*)" has been (enabled|disabled)$/ + * + * @param string $app + * @param string $action enabled or disabled + * + * @return void + */ + public function appHasBeenDisabled(string $app, string $action):void { + if ($action === 'enabled') { + $action = 'enables'; + } else { + $action = 'disables'; + } + $this->userEnablesOrDisablesApp( + $this->getAdminUsername(), + $action, + $app + ); + } + + /** + * @When the administrator gets the info of app :app + * + * @param string $app + * + * @return void + */ + public function theAdministratorGetsTheInfoOfApp(string $app):void { + $this->ocsContext->userSendsToOcsApiEndpoint( + $this->getAdminUsername(), + "GET", + "/cloud/apps/$app" + ); + } + + /** + * @When the administrator gets all apps using the provisioning API + * + * @return void + */ + public function theAdministratorGetsAllAppsUsingTheProvisioningApi():void { + $this->getAllApps(); + } + + /** + * @When the administrator gets all enabled apps using the provisioning API + * + * @return void + */ + public function theAdministratorGetsAllEnabledAppsUsingTheProvisioningApi():void { + $this->getEnabledApps(); + } + + /** + * @When the administrator gets all disabled apps using the provisioning API + * + * @return void + */ + public function theAdministratorGetsAllDisabledAppsUsingTheProvisioningApi():void { + $this->getDisabledApps(); + } + + /** + * @When the administrator sends a user creation request with the following attributes using the provisioning API: + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function adminSendsUserCreationRequestWithFollowingAttributesUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeRows($table, ["username", "password"], ["email", "displayname"]); + $table = $table->getRowsHash(); + $username = $this->getActualUsername($table["username"]); + $password = $this->getActualPassword($table["password"]); + $displayname = \array_key_exists("displayname", $table) ? $table["displayname"] : null; + $email = \array_key_exists("email", $table) ? $table["email"] : null; + + if (OcisHelper::isTestingOnOcisOrReva()) { + if ($email === null) { + $email = $username . '@owncloud.com'; + } + } + + $userAttributes = [ + ["userid", $username], + ["password", $password], + ]; + + if ($displayname !== null) { + $userAttributes[] = ["displayname", $displayname]; + } + + if ($email !== null) { + $userAttributes[] = ["email", $email]; + } + + if (OcisHelper::isTestingOnOcisOrReva()) { + $userAttributes[] = ["username", $username]; + } + + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $this->getAdminUsername(), + "POST", + "/cloud/users", + new TableNode($userAttributes) + ); + $this->addUserToCreatedUsersList( + $username, + $password, + $displayname, + $email, + null, + $this->theHTTPStatusCodeWasSuccess() + ); + if (OcisHelper::isTestingOnOcisOrReva()) { + $this->manuallyAddSkeletonFilesForUser($username, $password); + } + } + + /** + * @When /^the administrator sends a user creation request for user "([^"]*)" password "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $password + * + * @return void + * @throws Exception + */ + public function adminSendsUserCreationRequestUsingTheProvisioningApi(string $user, string $password):void { + $user = $this->getActualUsername($user); + $password = $this->getActualPassword($password); + if (OcisHelper::isTestingOnOcisOrReva()) { + $email = $user . '@owncloud.com'; + $bodyTable = new TableNode( + [ + ['userid', $user], + ['password', $password], + ['username', $user], + ['email', $email] + ] + ); + } else { + $email = null; + $bodyTable = new TableNode([['userid', $user], ['password', $password]]); + } + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $this->getAdminUsername(), + "POST", + "/cloud/users", + $bodyTable + ); + $this->pushToLastStatusCodesArrays(); + $success = $this->theHTTPStatusCodeWasSuccess(); + $this->addUserToCreatedUsersList( + $user, + $password, + null, + $email, + null, + $success + ); + if (OcisHelper::isTestingOnOcisOrReva() && $success) { + OcisHelper::createEOSStorageHome( + $this->getBaseUrl(), + $user, + $password, + $this->getStepLineRef() + ); + $this->manuallyAddSkeletonFilesForUser($user, $password); + } + } + + /** + * @When /^the administrator sends a user creation request for the following users with password using the provisioning API$/ + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorSendsAUserCreationRequestForTheFollowingUsersWithPasswordUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username", "password"], ["comment"]); + $users = $table->getHash(); + foreach ($users as $user) { + $this->adminSendsUserCreationRequestUsingTheProvisioningApi($user["username"], $user["password"]); + } + } + + /** + * @When /^unauthorized user "([^"]*)" tries to create new user "([^"]*)" with password "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $userToCreate + * @param string $password + * + * @return void + * @throws Exception + */ + public function userSendsUserCreationRequestUsingTheProvisioningApi(string $user, string $userToCreate, string $password):void { + $userToCreate = $this->getActualUsername($userToCreate); + $password = $this->getActualPassword($password); + if (OcisHelper::isTestingOnOcisOrReva()) { + $email = $userToCreate . '@owncloud.com'; + $bodyTable = new TableNode( + [ + ['userid', $userToCreate], + ['password', $password], + ['username', $userToCreate], + ['email', $email] + ] + ); + } else { + $email = null; + $bodyTable = new TableNode([['userid', $userToCreate], ['password', $password]]); + } + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "POST", + "/cloud/users", + $bodyTable + ); + $this->addUserToCreatedUsersList( + $userToCreate, + $password, + null, + $email, + null, + $this->theHTTPStatusCodeWasSuccess() + ); + } + + /** + * @When /^the administrator sends a user creation request for user "([^"]*)" password "([^"]*)" group "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $password + * @param string $group + * + * @return void + * @throws Exception + */ + public function theAdministratorCreatesUserPasswordGroupUsingTheProvisioningApi( + string $user, + string $password, + string $group + ):void { + $user = $this->getActualUsername($user); + $password = $this->getActualPassword($password); + if (OcisHelper::isTestingOnOcisOrReva()) { + $email = $user . '@owncloud.com'; + $bodyTable = new TableNode( + [ + ['userid', $user], + ['password', $password], + ['username', $user], + ['email', $email], + ['groups[]', $group], + ] + ); + } else { + $email = null; + $bodyTable = new TableNode( + [['userid', $user], ['password', $password], ['groups[]', $group]] + ); + } + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $this->getAdminUsername(), + "POST", + "/cloud/users", + $bodyTable + ); + $this->addUserToCreatedUsersList( + $user, + $password, + null, + $email, + null, + $this->theHTTPStatusCodeWasSuccess() + ); + if (OcisHelper::isTestingOnOcisOrReva()) { + $this->manuallyAddSkeletonFilesForUser($user, $password); + } + } + + /** + * @When /^the groupadmin "([^"]*)" sends a user creation request for user "([^"]*)" password "([^"]*)" group "([^"]*)" using the provisioning API$/ + * + * @param string $groupadmin + * @param string $userToCreate + * @param string $password + * @param string $group + * + * @return void + * @throws Exception + */ + public function theGroupAdminCreatesUserPasswordGroupUsingTheProvisioningApi( + string $groupadmin, + string $userToCreate, + string $password, + string $group + ):void { + $userToCreate = $this->getActualUsername($userToCreate); + $password = $this->getActualPassword($password); + if (OcisHelper::isTestingOnOcisOrReva()) { + $email = $userToCreate . '@owncloud.com'; + $bodyTable = new TableNode( + [ + ['userid', $userToCreate], + ['password', $userToCreate], + ['username', $userToCreate], + ['email', $email], + ['groups[]', $group], + ] + ); + } else { + $email = null; + $bodyTable = new TableNode( + [['userid', $userToCreate], ['password', $password], ['groups[]', $group]] + ); + } + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $groupadmin, + "POST", + "/cloud/users", + $bodyTable + ); + $this->addUserToCreatedUsersList( + $userToCreate, + $password, + null, + $email, + null, + $this->theHTTPStatusCodeWasSuccess() + ); + if (OcisHelper::isTestingOnOcisOrReva()) { + $this->manuallyAddSkeletonFilesForUser($userToCreate, $password); + } + } + + /** + * @When /^the groupadmin "([^"]*)" tries to create new user "([^"]*)" password "([^"]*)" in other group "([^"]*)" using the provisioning API$/ + * + * @param string $groupadmin + * @param string $userToCreate + * @param string|null $password + * @param string $group + * + * @return void + */ + public function theGroupAdminCreatesUserPasswordInOtherGroupUsingTheProvisioningApi( + string $groupadmin, + string $userToCreate, + ?string $password, + string $group + ):void { + $userToCreate = $this->getActualUsername($userToCreate); + $password = $this->getActualPassword($password); + if (OcisHelper::isTestingOnOcisOrReva()) { + $email = $userToCreate . '@owncloud.com'; + $bodyTable = new TableNode( + [ + ['userid', $userToCreate], + ['password', $userToCreate], + ['username', $userToCreate], + ['email', $email], + ['groups[]', $group], + ] + ); + } else { + $email = null; + $bodyTable = new TableNode( + [['userid', $userToCreate], ['password', $password], ['groups[]', $group]] + ); + } + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $groupadmin, + "POST", + "/cloud/users", + $bodyTable + ); + $this->addUserToCreatedUsersList( + $userToCreate, + $password, + null, + $email, + null, + $this->theHTTPStatusCodeWasSuccess() + ); + } + + /** + * @param string $username + * @param string|null $password + * + * @return void + */ + public function resetUserPasswordAsAdminUsingTheProvisioningApi(string $username, ?string $password):void { + $this->userResetUserPasswordUsingProvisioningApi( + $this->getAdminUsername(), + $username, + $password + ); + } + + /** + * @When the administrator resets the password of user :username to :password using the provisioning API + * + * @param string $username of the user whose password is reset + * @param string|null $password + * + * @return void + */ + public function adminResetsPasswordOfUserUsingTheProvisioningApi(string $username, ?string $password):void { + $this->resetUserPasswordAsAdminUsingTheProvisioningApi( + $username, + $password + ); + } + + /** + * @Given the administrator has reset the password of user :username to :password + * + * @param string $username of the user whose password is reset + * @param string $password + * + * @return void + */ + public function adminHasResetPasswordOfUserUsingTheProvisioningApi(string $username, ?string $password):void { + $this->resetUserPasswordAsAdminUsingTheProvisioningApi( + $username, + $password + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string|null $user + * @param string|null $username + * @param string|null $password + * + * @return void + */ + public function userResetUserPasswordUsingProvisioningApi(?string $user, ?string $username, ?string $password):void { + $targetUsername = $this->getActualUsername($username); + $password = $this->getActualPassword($password); + $this->userTriesToResetUserPasswordUsingTheProvisioningApi( + $user, + $targetUsername, + $password + ); + $this->rememberUserPassword($targetUsername, $password); + } + + /** + * @When user :user resets the password of user :username to :password using the provisioning API + * + * @param string|null $user that does the password reset + * @param string|null $username of the user whose password is reset + * @param string|null $password + * + * @return void + */ + public function userResetsPasswordOfUserUsingTheProvisioningApi(?string $user, ?string $username, ?string $password):void { + $this->userResetUserPasswordUsingProvisioningApi( + $user, + $username, + $password + ); + } + + /** + * @Given user :user has reset the password of user :username to :password + * + * @param string|null $user that does the password reset + * @param string|null $username of the user whose password is reset + * @param string|null $password + * + * @return void + */ + public function userHasResetPasswordOfUserUsingTheProvisioningApi(?string $user, ?string $username, ?string $password):void { + $this->userResetUserPasswordUsingProvisioningApi( + $user, + $username, + $password + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string|null $user + * @param string|null $username + * @param string|null $password + * + * @return void + */ + public function userTriesToResetUserPasswordUsingTheProvisioningApi(?string $user, ?string $username, ?string $password):void { + $password = $this->getActualPassword($password); + $bodyTable = new TableNode([['key', 'password'], ['value', $password]]); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "PUT", + "/cloud/users/$username", + $bodyTable + ); + } + + /** + * @When user :user tries to reset the password of user :username to :password using the provisioning API + * + * @param string|null $user that does the password reset + * @param string|null $username of the user whose password is reset + * @param string|null $password + * + * @return void + */ + public function userTriesToResetPasswordOfUserUsingTheProvisioningApi(?string $user, ?string $username, ?string $password):void { + $this->userTriesToResetUserPasswordUsingTheProvisioningApi( + $user, + $username, + $password + ); + } + + /** + * @Given user :user has tried to reset the password of user :username to :password + * + * @param string|null $user that does the password reset + * @param string|null $username of the user whose password is reset + * @param string|null $password + * + * @return void + */ + public function userHasTriedToResetPasswordOfUserUsingTheProvisioningApi(?string $user, ?string $username, ?string $password):void { + $this->userTriesToResetUserPasswordUsingTheProvisioningApi( + $user, + $username, + $password + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Given /^the administrator has deleted user "([^"]*)" using the provisioning API$/ + * + * @param string|null $user + * + * @return void + * @throws Exception + */ + public function theAdministratorHasDeletedUserUsingTheProvisioningApi(?string $user):void { + $user = $this->getActualUsername($user); + $this->deleteTheUserUsingTheProvisioningApi($user); + WebDavHelper::removeSpaceIdReferenceForUser($user); + $this->userShouldNotExist($user); + } + + /** + * @When /^the administrator deletes user "([^"]*)" using the provisioning API$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdminDeletesUserUsingTheProvisioningApi(string $user):void { + $user = $this->getActualUsername($user); + $this->deleteTheUserUsingTheProvisioningApi($user); + $this->rememberThatUserIsNotExpectedToExist($user); + } + + /** + * @When the administrator deletes the following users using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesTheFollowingUsersUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->theAdminDeletesUserUsingTheProvisioningApi($username["username"]); + } + } + + /** + * @When user :user deletes user :otherUser using the provisioning API + * + * @param string $user + * @param string $otherUser + * + * @return void + * @throws Exception + */ + public function userDeletesUserUsingTheProvisioningApi( + string $user, + string $otherUser + ):void { + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $actualOtherUser = $this->getActualUsername($otherUser); + + $this->response = UserHelper::deleteUser( + $this->getBaseUrl(), + $actualOtherUser, + $actualUser, + $actualPassword, + $this->getStepLineRef(), + $this->ocsApiVersion + ); + } + + /** + * @When /^the administrator changes the email of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $email + * + * @return void + * @throws Exception + */ + public function adminChangesTheEmailOfUserToUsingTheProvisioningApi( + string $user, + string $email + ):void { + $user = $this->getActualUsername($user); + $this->response = UserHelper::editUser( + $this->getBaseUrl(), + $user, + 'email', + $email, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->rememberUserEmailAddress($user, $email); + } + + /** + * @Given /^the administrator has changed the email of user "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $email + * + * @return void + * @throws Exception + */ + public function adminHasChangedTheEmailOfUserTo(string $user, string $email):void { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userHasBeenEditedUsingTheGraphApi( + $user, + null, + null, + $email + ); + $updatedUserData = $this->getJsonDecodedResponse(); + Assert::assertEquals( + $email, + $updatedUserData['mail'] + ); + } else { + $this->adminChangesTheEmailOfUserToUsingTheProvisioningApi( + $user, + $email + ); + $this->theHTTPStatusCodeShouldBe( + 200, + "could not change email of user $user" + ); + } + } + + /** + * @Given the administrator has changed their own email address to :email + * + * @param string|null $email + * + * @return void + * @throws Exception + */ + public function theAdministratorHasChangedTheirOwnEmailAddressTo(?string $email):void { + $admin = $this->getAdminUsername(); + $this->adminHasChangedTheEmailOfUserTo($admin, $email); + } + + /** + * @param string $requestingUser + * @param string $targetUser + * @param string $email + * + * @return void + * @throws JsonException + */ + public function userChangesUserEmailUsingProvisioningApi( + string $requestingUser, + string $targetUser, + string $email + ):void { + $this->response = UserHelper::editUser( + $this->getBaseUrl(), + $this->getActualUsername($targetUser), + 'email', + $email, + $this->getActualUsername($requestingUser), + $this->getPasswordForUser($requestingUser), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + } + + /** + * @When /^user "([^"]*)" changes the email of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $email + * + * @return void + * @throws Exception + */ + public function userChangesTheEmailOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $email + ):void { + $this->userTriesToChangeTheEmailOfUserUsingTheProvisioningApi( + $requestingUser, + $targetUser, + $email + ); + $targetUser = $this->getActualUsername($targetUser); + $this->rememberUserEmailAddress($targetUser, $email); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" tries to change the email of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $email + * + * @return void + */ + public function userTriesToChangeTheEmailOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $email + ):void { + $requestingUser = $this->getActualUsername($requestingUser); + $targetUser = $this->getActualUsername($targetUser); + $this->userChangesUserEmailUsingProvisioningApi( + $requestingUser, + $targetUser, + $email + ); + } + + /** + * @Given /^user "([^"]*)" has changed the email of user "([^"]*)" to "([^"]*)"$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $email + * + * @return void + * @throws Exception + */ + public function userHasChangedTheEmailOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $email + ):void { + $requestingUser = $this->getActualUsername($requestingUser); + $targetUser = $this->getActualUsername($targetUser); + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userHasBeenEditedUsingTheGraphApi( + $targetUser, + null, + null, + $email, + null, + $requestingUser, + $this->getPasswordForUser($requestingUser) + ); + $updatedUserData = $this->getJsonDecodedResponse(); + Assert::assertEquals( + $email, + $updatedUserData['mail'], + ); + } else { + $this->userChangesUserEmailUsingProvisioningApi( + $requestingUser, + $targetUser, + $email + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + $this->rememberUserEmailAddress($targetUser, $email); + } + + /** + * Edit the "display name" of a user by sending the key "displayname" to the API end point. + * + * This is the newer and consistent key name. + * + * @see https://github.com/owncloud/core/pull/33040 + * + * @When /^the administrator changes the display name of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function adminChangesTheDisplayNameOfUserUsingTheProvisioningApi( + string $user, + string $displayName + ):void { + $user = $this->getActualUsername($user); + $this->adminChangesTheDisplayNameOfUserUsingKey( + $user, + 'displayname', + $displayName + ); + $this->rememberUserDisplayName($user, $displayName); + } + + /** + * @Given /^the administrator has changed the display name of user "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function adminHasChangedTheDisplayNameOfUser( + string $user, + string $displayName + ):void { + $user = $this->getActualUsername($user); + if ($this->isTestingWithLdap()) { + $this->editLdapUserDisplayName( + $user, + $displayName + ); + } elseif (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userHasBeenEditedUsingTheGraphApi( + $user, + null, + null, + null, + $displayName + ); + $updatedUserData = $this->getJsonDecodedResponse(); + Assert::assertEquals( + $displayName, + $updatedUserData['displayName'] + ); + } else { + $this->adminChangesTheDisplayNameOfUserUsingKey( + $user, + 'displayname', + $displayName + ); + $response = UserHelper::getUser( + $this->getBaseUrl(), + $user, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef() + ); + $this->setResponse($response); + $this->theDisplayNameReturnedByTheApiShouldBe($displayName); + } + $this->rememberUserDisplayName($user, $displayName); + } + + /** + * As the administrator, edit the "display name" of a user by sending the key "display" to the API end point. + * + * This is the older and inconsistent key name, which remains for backward-compatibility. + * + * @see https://github.com/owncloud/core/pull/33040 + * + * @When /^the administrator changes the display of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function adminChangesTheDisplayOfUserUsingTheProvisioningApi( + string $user, + string $displayName + ):void { + $user = $this->getActualUsername($user); + $this->adminChangesTheDisplayNameOfUserUsingKey( + $user, + 'display', + $displayName + ); + $this->rememberUserDisplayName($user, $displayName); + } + + /** + * + * @param string $user + * @param string $key + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function adminChangesTheDisplayNameOfUserUsingKey( + string $user, + string $key, + string $displayName + ):void { + $result = UserHelper::editUser( + $this->getBaseUrl(), + $this->getActualUsername($user), + $key, + $displayName, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + if ($result->getStatusCode() !== 200) { + throw new Exception( + __METHOD__ . " could not change display name of user using key $key " + . $result->getStatusCode() . " " . $result->getBody() + ); + } + } + + /** + * As a user, edit the "display name" of a user by sending the key "displayname" to the API end point. + * + * This is the newer and consistent key name. + * + * @see https://github.com/owncloud/core/pull/33040 + * + * @When /^user "([^"]*)" changes the display name of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function userChangesTheDisplayNameOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $displayName + ):void { + $this->userTriesToChangeTheDisplayNameOfUserUsingTheProvisioningApi( + $requestingUser, + $targetUser, + $displayName + ); + $targetUser = $this->getActualUsername($targetUser); + $this->rememberUserDisplayName($targetUser, $displayName); + } + + /** + * As a user, try to edit the "display name" of a user by sending the key "displayname" to the API end point. + * + * This is the newer and consistent key name. + * + * @see https://github.com/owncloud/core/pull/33040 + * + * @When /^user "([^"]*)" tries to change the display name of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $displayName + * + * @return void + */ + public function userTriesToChangeTheDisplayNameOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $displayName + ):void { + $requestingUser = $this->getActualUsername($requestingUser); + $targetUser = $this->getActualUsername($targetUser); + $this->userChangesTheDisplayNameOfUserUsingKey( + $requestingUser, + $targetUser, + 'displayname', + $displayName + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * As a user, edit the "display name" of a user by sending the key "display" to the API end point. + * + * This is the older and inconsistent key name. + * + * @see https://github.com/owncloud/core/pull/33040 + * + * @When /^user "([^"]*)" changes the display of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function userChangesTheDisplayOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $displayName + ):void { + $requestingUser = $this->getActualUsername($requestingUser); + $targetUser = $this->getActualUsername($targetUser); + $this->userChangesTheDisplayNameOfUserUsingKey( + $requestingUser, + $targetUser, + 'display', + $displayName + ); + $this->rememberUserDisplayName($targetUser, $displayName); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has changed the display name of user "([^"]*)" to "([^"]*)"$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $displayName + * + * @return void + * @throws Exception + */ + public function userHasChangedTheDisplayNameOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $displayName + ):void { + $requestingUser = $this->getActualUsername($requestingUser); + $targetUser = $this->getActualUsername($targetUser); + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userHasBeenEditedUsingTheGraphApi( + $targetUser, + null, + null, + null, + $displayName, + $requestingUser, + $this->getPasswordForUser($requestingUser) + ); + $updatedUserData = $this->getJsonDecodedResponse(); + Assert::assertEquals( + $displayName, + $updatedUserData['displayName'] + ); + } else { + $this->userChangesTheDisplayNameOfUserUsingKey( + $requestingUser, + $targetUser, + 'displayname', + $displayName + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + $this->rememberUserDisplayName($targetUser, $displayName); + } + /** + * + * @param string $requestingUser + * @param string $targetUser + * @param string $key + * @param string $displayName + * + * @return void + */ + public function userChangesTheDisplayNameOfUserUsingKey( + string $requestingUser, + string $targetUser, + string $key, + string $displayName + ):void { + $result = UserHelper::editUser( + $this->getBaseUrl(), + $this->getActualUsername($targetUser), + $key, + $displayName, + $this->getActualUsername($requestingUser), + $this->getPasswordForUser($requestingUser), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + } + + /** + * @When /^the administrator changes the quota of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $quota + * + * @return void + */ + public function adminChangesTheQuotaOfUserUsingTheProvisioningApi( + string $user, + string $quota + ):void { + $result = UserHelper::editUser( + $this->getBaseUrl(), + $this->getActualUsername($user), + 'quota', + $quota, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + } + + /** + * @Given /^the administrator has (?:changed|set) the quota of user "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $quota + * + * @return void + */ + public function adminHasChangedTheQuotaOfUserTo( + string $user, + string $quota + ):void { + $user = $this->getActualUsername($user); + $this->adminChangesTheQuotaOfUserUsingTheProvisioningApi( + $user, + $quota + ); + $this->theHTTPStatusCodeShouldBe( + 200, + "could not change quota of user $user" + ); + } + + /** + * @param string $requestingUser + * @param string $targetUser + * @param string $quota + * + * @return void + */ + public function userChangeQuotaOfUserUsingProvisioningApi( + string $requestingUser, + string $targetUser, + string $quota + ):void { + $result = UserHelper::editUser( + $this->getBaseUrl(), + $this->getActualUsername($targetUser), + 'quota', + $quota, + $this->getActualUsername($requestingUser), + $this->getPasswordForUser($requestingUser), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + } + + /** + * @When /^user "([^"]*)" changes the quota of user "([^"]*)" to "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $quota + * + * @return void + */ + public function userChangesTheQuotaOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $quota + ):void { + $this->userChangeQuotaOfUserUsingProvisioningApi( + $requestingUser, + $targetUser, + $quota + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has changed the quota of user "([^"]*)" to "([^"]*)"$/ + * + * @param string $requestingUser + * @param string $targetUser + * @param string $quota + * + * @return void + */ + public function userHasChangedTheQuotaOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser, + string $quota + ):void { + $this->userChangeQuotaOfUserUsingProvisioningApi( + $requestingUser, + $targetUser, + $quota + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $user + * + * @return void + * @throws JsonException + */ + public function retrieveUserInformationAsAdminUsingProvisioningApi( + string $user + ):void { + $result = UserHelper::getUser( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + } + + /** + * @When /^the administrator retrieves the information of user "([^"]*)" using the provisioning API$/ + * + * @param string $user + * + * @return void + * @throws JsonException + */ + public function adminRetrievesTheInformationOfUserUsingTheProvisioningApi( + string $user + ):void { + $user = $this->getActualUsername($user); + $this->retrieveUserInformationAsAdminUsingProvisioningApi( + $user + ); + } + + /** + * @Given /^the administrator has retrieved the information of user "([^"]*)"$/ + * + * @param string $user + * + * @return void + * @throws JsonException + */ + public function adminHasRetrievedTheInformationOfUserUsingTheProvisioningApi( + string $user + ):void { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminHasRetrievedUserUsingTheGraphApi($user); + } else { + $this->retrieveUserInformationAsAdminUsingProvisioningApi($user); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + } + + /** + * @param string $requestingUser + * @param string $targetUser + * + * @return void + * @throws JsonException + */ + public function userRetrieveUserInformationUsingProvisioningApi( + string $requestingUser, + string $targetUser + ):void { + $result = UserHelper::getUser( + $this->getBaseUrl(), + $this->getActualUsername($targetUser), + $this->getActualUsername($requestingUser), + $this->getPasswordForUser($requestingUser), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + } + + /** + * @When /^user "([^"]*)" retrieves the information of user "([^"]*)" using the provisioning API$/ + * + * @param string $requestingUser + * @param string $targetUser + * + * @return void + * @throws JsonException + */ + public function userRetrievesTheInformationOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser + ):void { + $this->userRetrieveUserInformationUsingProvisioningApi( + $requestingUser, + $targetUser + ); + } + + /** + * @Given /^user "([^"]*)" has retrieved the information of user "([^"]*)"$/ + * + * @param string $requestingUser + * @param string $targetUser + * + * @return void + * @throws JsonException + */ + public function userHasRetrievedTheInformationOfUserUsingTheProvisioningApi( + string $requestingUser, + string $targetUser + ):void { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userHasRetrievedUserUsingTheGraphApi( + $requestingUser, + $targetUser + ); + } else { + $this->userRetrieveUserInformationUsingProvisioningApi( + $requestingUser, + $targetUser + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + } + + /** + * @Then /^user "([^"]*)" should exist$/ + * + * @param string $user + * + * @return void + * @throws JsonException + */ + public function userShouldExist(string $user):void { + $user = $this->getActualUsername($user); + Assert::assertTrue( + $this->userExists($user), + "User '$user' should exist but does not exist" + ); + } + + /** + * @Then the following users should exist + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUsersShouldExist(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->userShouldExist($username["username"]); + } + } + + /** + * @Then /^user "([^"]*)" should not exist$/ + * + * @param string $user + * + * @return void + * @throws JsonException + */ + public function userShouldNotExist(string $user):void { + $user = $this->getActualUsername($user); + Assert::assertFalse( + $this->userExists($user), + "User '$user' should not exist but does exist" + ); + $this->rememberThatUserIsNotExpectedToExist($user); + } + + /** + * @Then the following users should not exist + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUsersShouldNotExist(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->userShouldNotExist($username["username"]); + } + } + + /** + * @Then /^group "([^"]*)" should exist$/ + * + * @param string $group + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function groupShouldExist(string $group):void { + Assert::assertTrue( + $this->groupExists($group), + "Group '$group' should exist but does not exist" + ); + } + + /** + * @Then /^group "([^"]*)" should not exist$/ + * + * @param string $group + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function groupShouldNotExist(string $group):void { + Assert::assertFalse( + $this->groupExists($group), + "Group '$group' should not exist but does exist" + ); + } + + /** + * @Then the following groups should not exist + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingGroupsShouldNotExist(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["groupname"]); + $groups = $table->getHash(); + foreach ($groups as $group) { + $this->groupShouldNotExist($group["groupname"]); + } + } + + /** + * @Then /^these groups should (not|)\s?exist:$/ + * expects a table of groups with the heading "groupname" + * + * @param string $shouldOrNot (not|) + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theseGroupsShouldNotExist(string $shouldOrNot, TableNode $table):void { + $should = ($shouldOrNot !== "not"); + $this->verifyTableNodeColumns($table, ['groupname']); + $useGraph = OcisHelper::isTestingWithGraphApi(); + if ($useGraph) { + $this->graphContext->theseGroupsShouldNotExist($shouldOrNot, $table); + } else { + $groups = $this->getArrayOfGroupsResponded($this->getAllGroups()); + foreach ($table as $row) { + if (\in_array($row['groupname'], $groups, true) !== $should) { + throw new Exception( + "group '" . $row['groupname'] . + "' does" . ($should ? " not" : "") . + " exist but should" . ($should ? "" : " not") + ); + } + } + } + } + + /** + * @Given /^user "([^"]*)" has been deleted$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userHasBeenDeleted(string $user):void { + $user = $this->getActualUsername($user); + if ($this->userExists($user)) { + if ($this->isTestingWithLdap() && \in_array($user, $this->ldapCreatedUsers)) { + $this->deleteLdapUser($user); + } else { + $this->deleteTheUserUsingTheProvisioningApi($user); + } + } + $this->userShouldNotExist($user); + } + + /** + * @Given the following users have been deleted + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUsersHaveBeenDeleted(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->userHasBeenDeleted($username["username"]); + } + } + + /** + * @Given these users have been initialized: + * expects a table of users with the heading + * "|username|password|" + * + * @param TableNode $table + * + * @return void + */ + public function theseUsersHaveBeenInitialized(TableNode $table):void { + foreach ($table as $row) { + if (!isset($row ['password'])) { + $password = $this->getPasswordForUser($row ['username']); + } else { + $password = $row ['password']; + } + $this->initializeUser( + $row ['username'], + $password + ); + } + } + + /** + * @When the administrator gets all the members of group :group using the provisioning API + * + * @param string $group + * + * @return void + */ + public function theAdministratorGetsAllTheMembersOfGroupUsingTheProvisioningApi(string $group):void { + $this->userGetsAllTheMembersOfGroupUsingTheProvisioningApi( + $this->getAdminUsername(), + $group + ); + } + + /** + * @When /^user "([^"]*)" gets all the members of group "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $group + * + * @return void + */ + public function userGetsAllTheMembersOfGroupUsingTheProvisioningApi(string $user, string $group):void { + $fullUrl = $this->getBaseUrl() . "/ocs/v{$this->ocsApiVersion}.php/cloud/groups/$group"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getActualUsername($user), + $this->getPasswordForUser($user) + ); + } + + /** + * get all the existing groups + * + * @return ResponseInterface + */ + public function getAllGroups():ResponseInterface { + $fullUrl = $this->getBaseUrl() . "/ocs/v{$this->ocsApiVersion}.php/cloud/groups"; + return HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + } + + /** + * @When the administrator gets all the groups using the provisioning API + * + * @return void + */ + public function theAdministratorGetsAllTheGroupsUsingTheProvisioningApi():void { + $this->response = $this->getAllGroups(); + } + + /** + * @When /^user "([^"]*)" tries to get all the groups using the provisioning API$/ + * @When /^user "([^"]*)" gets all the groups using the provisioning API$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userTriesToGetAllTheGroupsUsingTheProvisioningApi(string $user):void { + $fullUrl = $this->getBaseUrl() . "/ocs/v{$this->ocsApiVersion}.php/cloud/groups"; + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword + ); + } + + /** + * @When the administrator gets all the groups of user :user using the provisioning API + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theAdministratorGetsAllTheGroupsOfUser(string $user):void { + $this->userGetsAllTheGroupsOfUser($this->getAdminUsername(), $user); + } + + /** + * @When user :user gets all the groups of user :otherUser using the provisioning API + * + * @param string $user + * @param string $otherUser + * + * @return void + * @throws Exception + */ + public function userGetsAllTheGroupsOfUser(string $user, string $otherUser):void { + $actualOtherUser = $this->getActualUsername($otherUser); + $fullUrl = $this->getBaseUrl() . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$actualOtherUser/groups"; + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword + ); + } + + /** + * @When the administrator gets the list of all users using the provisioning API + * + * @return void + */ + public function theAdministratorGetsTheListOfAllUsersUsingTheProvisioningApi():void { + $this->userGetsTheListOfAllUsersUsingTheProvisioningApi($this->getAdminUsername()); + } + + /** + * @When user :user gets the list of all users using the provisioning API + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userGetsTheListOfAllUsersUsingTheProvisioningApi(string $user):void { + $fullUrl = $this->getBaseUrl() . "/ocs/v{$this->ocsApiVersion}.php/cloud/users"; + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword + ); + } + + /** + * Make a request about the user. That will force the server to fully + * initialize the user, including their skeleton files. + * + * @param string $user + * @param string $password + * + * @return void + */ + public function initializeUser(string $user, string $password):void { + $url = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$user"; + HttpRequestHelper::get( + $url, + $this->getStepLineRef(), + $user, + $password + ); + $this->lastUploadTime = \time(); + } + + /** + * Touch an API end-point for each user so that their file-system gets setup + * + * @param array $users + * + * @return void + * @throws Exception + */ + public function initializeUsers(array $users):void { + $url = "/cloud/users/%s"; + foreach ($users as $user) { + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + 'GET', + \sprintf($url, $user), + $this->getStepLineRef() + ); + $this->setResponse($response); + $this->theHTTPStatusCodeShouldBe(200); + } + } + + /** + * adds a user to the list of users that were created during test runs + * makes it possible to use this list in other test steps + * or to delete them at the end of the test + * + * @param string|null $user + * @param string|null $password + * @param string|null $displayName + * @param string|null $email + * @param string|null $userId only set for the users created using the Graph API + * @param bool $shouldExist + * + * @return void + * @throws JsonException + */ + public function addUserToCreatedUsersList( + ?string $user, + ?string $password, + ?string $displayName = null, + ?string $email = null, + ?string $userId = null, + bool $shouldExist = true + ):void { + $user = $this->getActualUsername($user); + $normalizedUsername = $this->normalizeUsername($user); + $userData = [ + "password" => $password, + "displayname" => $displayName, + "email" => $email, + "shouldExist" => $shouldExist, + "actualUsername" => $user, + "id" => $userId + ]; + + if ($this->currentServer === 'LOCAL') { + // Only remember this user creation if it was expected to have been successful + // or the user has not been processed before. Some tests create a user the + // first time (successfully) and then purposely try to create the user again. + // The 2nd user creation is expected to fail, and in that case we want to + // still remember the details of the first user creation. + if ($shouldExist || !\array_key_exists($normalizedUsername, $this->createdUsers)) { + $this->createdUsers[$normalizedUsername] = $userData; + } + } elseif ($this->currentServer === 'REMOTE') { + // See comment above about the LOCAL case. The logic is the same for the remote case. + if ($shouldExist || !\array_key_exists($normalizedUsername, $this->createdRemoteUsers)) { + $this->createdRemoteUsers[$normalizedUsername] = $userData; + } + } + } + + /** + * remember the password of a user that already exists so that you can use + * ordinary test steps after changing their password. + * + * @param string $user + * @param string $password + * + * @return void + */ + public function rememberUserPassword( + string $user, + string $password + ):void { + $normalizedUsername = $this->normalizeUsername($user); + if ($this->currentServer === 'LOCAL') { + if (\array_key_exists($normalizedUsername, $this->createdUsers)) { + $this->createdUsers[$normalizedUsername]['password'] = $password; + } + } elseif ($this->currentServer === 'REMOTE') { + if (\array_key_exists($normalizedUsername, $this->createdRemoteUsers)) { + $this->createdRemoteUsers[$user]['password'] = $password; + } + } + } + + /** + * Remembers that a user from the list of users that were created during + * test runs is no longer expected to exist. Useful if a user was created + * during the setup phase but was deleted in a test run. We don't expect + * this user to exist in the tear-down phase, so remember that fact. + * + * @param string $user + * + * @return void + */ + public function rememberThatUserIsNotExpectedToExist(string $user):void { + $user = $this->getActualUsername($user); + $normalizedUsername = $this->normalizeUsername($user); + if (\array_key_exists($normalizedUsername, $this->createdUsers)) { + $this->createdUsers[$normalizedUsername]['shouldExist'] = false; + } + } + + /** + * creates a single user + * + * @param string|null $user + * @param string|null $password if null, then select a password + * @param string|null $displayName + * @param string|null $email + * @param bool $initialize initialize the user skeleton files etc + * @param string|null $method how to create the user api|occ, default api + * @param bool $setDefault sets the missing values to some default + * @param bool $skeleton + * + * @return void + * @throws Exception + */ + public function createUser( + ?string $user, + ?string $password = null, + ?string $displayName = null, + ?string $email = null, + bool $initialize = true, + ?string $method = null, + bool $setDefault = true, + bool $skeleton = true + ):void { + $userId = null; + if ($password === null) { + $password = $this->getPasswordForUser($user); + } + + if ($displayName === null && $setDefault === true) { + $displayName = $this->getDisplayNameForUser($user); + if ($displayName === null) { + $displayName = $this->getDisplayNameForUser('regularuser'); + } + } + + if ($email === null && $setDefault === true) { + $email = $this->getEmailAddressForUser($user); + + if ($email === null) { + // escape @ & space if present in userId + $email = \str_replace(["@", " "], "", $user) . '@owncloud.com'; + } + } + + $user = $this->getActualUsername($user); + + if ($method === null && $this->isTestingWithLdap()) { + //guess yourself + $method = "ldap"; + } elseif (OcisHelper::isTestingWithGraphApi()) { + $method = "graph"; + } elseif ($method === null) { + $method = "api"; + } + $user = \trim($user); + $method = \trim(\strtolower($method)); + switch ($method) { + case "occ": + $result = SetupHelper::createUser( + $user, + $password, + $this->getStepLineRef(), + $displayName, + $email + ); + if ($result["code"] !== "0") { + throw new Exception( + __METHOD__ . " could not create user. {$result['stdOut']} {$result['stdErr']}" + ); + } + break; + case "api": + case "ldap": + $settings = []; + $setting["userid"] = $user; + $setting["displayName"] = $displayName; + $setting["password"] = $password; + $setting["email"] = $email; + \array_push($settings, $setting); + try { + $this->usersHaveBeenCreated( + $initialize, + $settings, + $method, + $skeleton + ); + } catch (LdapException $exception) { + throw new Exception( + __METHOD__ . " cannot create a LDAP user with provided data. Error: {$exception}" + ); + } + break; + case "graph": + $this->graphContext->theAdminHasCreatedUser( + $user, + $password, + $email, + $displayName, + ); + $newUser = $this->getJsonDecodedResponse(); + $userId = $newUser['id']; + break; + default: + throw new InvalidArgumentException( + __METHOD__ . " Invalid method to create a user" + ); + } + + $this->addUserToCreatedUsersList($user, $password, $displayName, $email, $userId); + if ($initialize) { + $this->initializeUser($user, $password); + } + } + + /** + * @param string $user + * + * @return void + * @throws Exception + */ + public function deleteUser(string $user):void { + $this->deleteTheUserUsingTheProvisioningApi($user); + $this->userShouldNotExist($user); + } + + /** + * Try to delete the group, catching anything bad that might happen. + * Use this method only in places where you want to try as best you + * can to delete the group, but do not want to error if there is a problem. + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function cleanupGroup(string $group):void { + try { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminHasDeletedGroupUsingTheGraphApi($group); + } else { + $this->deleteTheGroupUsingTheProvisioningApi($group); + } + } catch (Exception $e) { + \error_log( + "INFORMATION: There was an unexpected problem trying to delete group " . + "'$group' message '" . $e->getMessage() . "'" + ); + } + + if ($this->theGroupShouldBeAbleToBeDeleted($group) + && $this->groupExists($group) + ) { + \error_log( + "INFORMATION: tried to delete group '$group'" . + " at the end of the scenario but it seems to still exist. " . + "There might be problems with later scenarios." + ); + } + } + + /** + * @param string|null $user + * + * @return bool + * @throws JsonException + */ + public function userExists(?string $user):bool { + // in OCIS there is no admin user and in oC10 there are issues when + // sending the username in lowercase in the auth but in uppercase in + // the URL see https://github.com/owncloud/core/issues/36822 + $user = $this->getActualUsername($user); + if (OcisHelper::isTestingOnOcisOrReva()) { + // In OCIS an intermittent issue restricts users to list their own account + // So use admin account to list the user + // https://github.com/owncloud/ocis/issues/820 + // The special code can be reverted once the issue is fixed + if (OcisHelper::isTestingParallelDeployment()) { + $requestingUser = $this->getActualUsername($user); + $requestingPassword = $this->getPasswordForUser($user); + } elseif (OcisHelper::isTestingWithGraphApi()) { + $requestingUser = $this->getAdminUsername(); + $requestingPassword = $this->getAdminPassword(); + } elseif (OcisHelper::isTestingOnOcis()) { + $requestingUser = 'moss'; + $requestingPassword = 'vista'; + } else { + $requestingUser = $this->getActualUsername($user); + $requestingPassword = $this->getPasswordForUser($requestingUser); + } + } else { + $requestingUser = $this->getAdminUsername(); + $requestingPassword = $this->getAdminPassword(); + } + $path = (OcisHelper::isTestingWithGraphApi()) + ? "/graph/v1.0" + : "/ocs/v2.php/cloud"; + $fullUrl = $this->getBaseUrl() . $path . "/users/$user"; + + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $requestingUser, + $requestingPassword + ); + if ($this->response->getStatusCode() >= 400) { + return false; + } + return true; + } + + /** + * @Then /^user "([^"]*)" should belong to group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userShouldBelongToGroup(string $user, string $group):void { + $user = $this->getActualUsername($user); + $respondedArray = []; + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userShouldBeMemberInGroupUsingTheGraphApi( + $user, + $group + ); + } else { + $this->theAdministratorGetsAllTheGroupsOfUser($user); + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + \sort($respondedArray); + Assert::assertContains( + $group, + $respondedArray, + __METHOD__ . " Group '$group' does not exist in '" + . \implode(', ', $respondedArray) + . "'" + ); + Assert::assertEquals( + 200, + $this->response->getStatusCode(), + __METHOD__ + . " Expected status code is '200' but got '" + . $this->response->getStatusCode() + . "'" + ); + } + } + + /** + * @Then the following users should belong to the following groups + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theTheFollowingUserShouldBelongToTheFollowingGroup(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username", "groupname"]); + $rows = $table->getHash(); + foreach ($rows as $row) { + $this->userShouldBelongToGroup($row["username"], $row["groupname"]); + } + } + + /** + * @param string $group + * + * @return array + */ + public function getUsersOfLdapGroup(string $group):array { + $ou = $this->getLdapGroupsOU(); + $entry = 'cn=' . $group . ',ou=' . $ou . ',' . $this->ldapBaseDN; + $ldapResponse = $this->ldap->getEntry($entry); + return $ldapResponse["memberuid"]; + } + + /** + * @Then /^user "([^"]*)" should not belong to group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws JsonException + */ + public function userShouldNotBelongToGroup(string $user, string $group):void { + $user = $this->getActualUsername($user); + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->userShouldNotBeMemberInGroupUsingTheGraphApi($user, $group); + } else { + $fullUrl = $this->getBaseUrl() . "/ocs/v2.php/cloud/users/$user/groups"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + \sort($respondedArray); + Assert::assertNotContains($group, $respondedArray); + Assert::assertEquals( + 200, + $this->response->getStatusCode() + ); + } + } + + /** + * @Then the following users should not belong to the following groups + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theTheFollowingUserShouldNotBelongToTheFollowingGroup(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username", "groupname"]); + $rows = $table->getHash(); + foreach ($rows as $row) { + $this->userShouldNotBelongToGroup($row["username"], $row["groupname"]); + } + } + + /** + * @Then group :group should not contain user :username + * + * @param string $group + * @param string $username + * + * @return void + */ + public function groupShouldNotContainUser(string $group, string $username):void { + $username = $this->getActualUsername($username); + $fullUrl = $this->getBaseUrl() . "/ocs/v2.php/cloud/groups/$group"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + $this->theUsersReturnedByTheApiShouldNotInclude($username); + } + + /** + * @param string $user + * @param string $group + * + * @return bool + */ + public function userBelongsToGroup(string $user, string $group):bool { + $fullUrl = $this->getBaseUrl() . "/ocs/v2.php/cloud/users/$user/groups"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + + if (\in_array($group, $respondedArray)) { + return true; + } else { + return false; + } + } + + /** + * @When /^the administrator adds user "([^"]*)" to group "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function adminAddsUserToGroupUsingTheProvisioningApi(string $user, string $group):void { + $this->addUserToGroup($user, $group, "api"); + } + + /** + * @When the administrator adds the following users to the following groups using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorAddsUserToTheFollowingGroupsUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username", "groupname"], ["comment"]); + $rows = $table->getHash(); + foreach ($rows as $row) { + $this->adminAddsUserToGroupUsingTheProvisioningApi($row["username"], $row["groupname"]); + } + } + + /** + * @When user :user tries to add user :otherUser to group :group using the provisioning API + * + * @param string $user + * @param string $otherUser + * @param string $group + * + * @return void + * @throws Exception + */ + public function userTriesToAddUserToGroupUsingTheProvisioningApi(string $user, string $otherUser, string $group):void { + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $actualOtherUser = $this->getActualUsername($otherUser); + $result = UserHelper::addUserToGroup( + $this->getBaseUrl(), + $actualOtherUser, + $group, + $actualUser, + $actualPassword, + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->response = $result; + } + + /** + * @When user :user tries to add himself to group :group using the provisioning API + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userTriesToAddHimselfToGroupUsingTheProvisioningApi(string $user, string $group):void { + $this->userTriesToAddUserToGroupUsingTheProvisioningApi($user, $user, $group); + } + + /** + * @When the administrator tries to add user :user to group :group using the provisioning API + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function theAdministratorTriesToAddUserToGroupUsingTheProvisioningApi( + string $user, + string $group + ):void { + $this->userTriesToAddUserToGroupUsingTheProvisioningApi( + $this->getAdminUsername(), + $user, + $group + ); + } + + /** + * @Given /^user "([^"]*)" has been added to group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userHasBeenAddedToGroup(string $user, string $group):void { + $user = $this->getActualUsername($user); + $this->addUserToGroup($user, $group, null, true); + } + + /** + * @Given the following users have been added to the following groups + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUserHaveBeenAddedToTheFollowingGroup(TableNode $table):void { + $this->verifyTableNodeColumns($table, ['username', 'groupname']); + foreach ($table as $row) { + $this->userHasBeenAddedToGroup($row['username'], $row['groupname']); + } + } + + /** + * @Given /^user "([^"]*)" has been added to database backend group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userHasBeenAddedToDatabaseBackendGroup(string $user, string $group):void { + $this->addUserToGroup($user, $group, 'api', true); + } + + /** + * @param string $user + * @param string $group + * @param string|null $method how to add the user to the group api|occ + * @param bool $checkResult if true, then check the status of the operation. default false. + * for given step checkResult is expected to be set as true + * for when step checkResult is expected to be set as false + * + * @return void + * @throws Exception + */ + public function addUserToGroup(string $user, string $group, ?string $method = null, bool $checkResult = false):void { + $user = $this->getActualUsername($user); + if ($method === null + && $this->isTestingWithLdap() + && !$this->isLocalAdminGroup($group) + ) { + //guess yourself + $method = "ldap"; + } elseif ($method === null && OcisHelper::isTestingWithGraphApi()) { + $method = "graph"; + } elseif ($method === null) { + $method = "api"; + } + $method = \trim(\strtolower($method)); + switch ($method) { + case "api": + $result = UserHelper::addUserToGroup( + $this->getBaseUrl(), + $user, + $group, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + if ($checkResult && ($result->getStatusCode() !== 200)) { + throw new Exception( + "could not add user to group. " + . $result->getStatusCode() . " " . $result->getBody() + ); + } + $this->response = $result; + if (!$checkResult) { + // for when step only + $this->pushToLastStatusCodesArrays(); + } + break; + case "occ": + $result = SetupHelper::addUserToGroup( + $group, + $user, + $this->getStepLineRef() + ); + if ($checkResult && ($result["code"] !== "0")) { + throw new Exception( + "could not add user to group. {$result['stdOut']} {$result['stdErr']}" + ); + } + break; + case "ldap": + try { + $this->addUserToLdapGroup( + $user, + $group + ); + } catch (LdapException $exception) { + throw new Exception( + "User " . $user . " cannot be added to " . $group . " . Error: {$exception}" + ); + }; + break; + case "graph": + $this->graphContext->adminHasAddedUserToGroupUsingTheGraphApi( + $user, + $group + ); + break; + default: + throw new InvalidArgumentException( + "Invalid method to add a user to a group" + ); + } + } + + /** + * @Given the administrator has been added to group :group + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function theAdministratorHasBeenAddedToGroup(string $group):void { + $admin = $this->getAdminUsername(); + $this->addUserToGroup($admin, $group, null, true); + } + + /** + * @param string $group + * @param bool $shouldExist - true if the group should exist + * @param bool $possibleToDelete - true if it is possible to delete the group + * @param string|null $id - id of the group, only required for the groups created using the Graph API + * + * @return void + */ + public function addGroupToCreatedGroupsList( + string $group, + bool $shouldExist = true, + bool $possibleToDelete = true, + ?string $id = null + ):void { + $groupData = [ + "shouldExist" => $shouldExist, + "possibleToDelete" => $possibleToDelete + ]; + if ($id !== null) { + $groupData["id"] = $id; + } + + if ($this->currentServer === 'LOCAL') { + $this->createdGroups[$group] = $groupData; + } elseif ($this->currentServer === 'REMOTE') { + $this->createdRemoteGroups[$group] = $groupData; + } + } + + /** + * Remembers that a group from the list of groups that were created during + * test runs is no longer expected to exist. Useful if a group was created + * during the setup phase but was deleted in a test run. We don't expect + * this group to exist in the tear-down phase, so remember that fact. + * + * @param string $group + * + * @return void + */ + public function rememberThatGroupIsNotExpectedToExist(string $group):void { + if (\array_key_exists($group, $this->createdGroups)) { + $this->createdGroups[$group]['shouldExist'] = false; + } + } + + /** + * @When /^the administrator creates group "([^"]*)" using the provisioning API$/ + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function adminCreatesGroupUsingTheProvisioningApi(string $group):void { + if (!$this->groupExists($group)) { + $this->createTheGroup($group, 'api'); + } + $this->groupShouldExist($group); + } + + /** + * @Given /^group "([^"]*)" has been created$/ + * + * @param string $group + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function groupHasBeenCreated(string $group):void { + $this->createTheGroup($group); + $this->groupShouldExist($group); + } + + /** + * @Given /^group "([^"]*)" has been created in the database user backend$/ + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function groupHasBeenCreatedOnDatabaseBackend(string $group):void { + $this->adminCreatesGroupUsingTheProvisioningApi($group); + } + + /** + * @Given these groups have been created: + * expects a table of groups with the heading "groupname" + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theseGroupsHaveBeenCreated(TableNode $table):void { + $this->verifyTableNodeColumns($table, ['groupname'], ['comment']); + foreach ($table as $row) { + $this->createTheGroup($row['groupname']); + } + } + + /** + * @When /^the administrator sends a group creation request for group "([^"]*)" using the provisioning API$/ + * + * @param string $group + * @param string|null $user + * + * @return void + */ + public function adminSendsGroupCreationRequestUsingTheProvisioningApi( + string $group, + ?string $user = null + ):void { + $bodyTable = new TableNode([['groupid', $group]]); + $user = $user === null ? $this->getAdminUsername() : $user; + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "POST", + "/cloud/groups", + $bodyTable + ); + $this->pushToLastStatusCodesArrays(); + $this->addGroupToCreatedGroupsList($group); + } + + /** + * @When the administrator sends a group creation request for the following groups using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorSendsAGroupCreationRequestForTheFollowingGroupUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["groupname"], ["comment"]); + $groups = $table->getHash(); + foreach ($groups as $group) { + $this->adminSendsGroupCreationRequestUsingTheProvisioningApi($group["groupname"]); + } + } + + /** + * @When /^the administrator tries to send a group creation request for group "([^"]*)" using the provisioning API$/ + * + * @param string $group + * + * @return void + */ + public function adminTriesToSendGroupCreationRequestUsingTheAPI(string $group):void { + $this->adminSendsGroupCreationRequestUsingTheProvisioningApi($group); + $this->rememberThatGroupIsNotExpectedToExist($group); + } + + /** + * @When /^user "([^"]*)" tries to send a group creation request for group "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $group + * + * @return void + */ + public function userTriesToSendGroupCreationRequestUsingTheAPI(string $user, string $group):void { + $this->adminSendsGroupCreationRequestUsingTheProvisioningApi($group, $user); + $this->rememberThatGroupIsNotExpectedToExist($group); + } + + /** + * creates a single group + * + * @param string $group + * @param string|null $method how to create the group api|occ + * + * @return void + * @throws Exception + */ + public function createTheGroup(string $group, ?string $method = null):void { + //guess yourself + if ($method === null) { + if ($this->isTestingWithLdap()) { + $method = "ldap"; + } elseif (OcisHelper::isTestingWithGraphApi()) { + $method = "graph"; + } else { + $method = "api"; + } + } + $group = \trim($group); + $method = \trim(\strtolower($method)); + $groupCanBeDeleted = false; + $groupId = null; + switch ($method) { + case "api": + $result = UserHelper::createGroup( + $this->getBaseUrl(), + $group, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef() + ); + if ($result->getStatusCode() === 200) { + $groupCanBeDeleted = true; + } else { + throw new Exception( + "could not create group '$group'. " + . $result->getStatusCode() . " " . $result->getBody() + ); + } + break; + case "occ": + $result = SetupHelper::createGroup( + $group, + $this->getStepLineRef() + ); + if ($result["code"] == 0) { + $groupCanBeDeleted = true; + } else { + throw new Exception( + "could not create group '$group'. {$result['stdOut']} {$result['stdErr']}" + ); + } + break; + case "ldap": + try { + $this->createLdapGroup($group); + } catch (LdapException $e) { + throw new Exception( + "could not create group '$group'. Error: {$e}" + ); + } + break; + case "graph": + $newGroup = $this->graphContext->adminHasCreatedGroupUsingTheGraphApi($group); + $groupCanBeDeleted = true; + $groupId = $newGroup["id"]; + break; + default: + throw new InvalidArgumentException( + "Invalid method to create group '$group'" + ); + } + + $this->addGroupToCreatedGroupsList($group, true, $groupCanBeDeleted, $groupId); + } + + /** + * @param string $attribute + * @param string $entry + * @param string $value + * @param bool $append + * + * @return void + * @throws Exception + */ + public function setTheLdapAttributeOfTheEntryTo( + string $attribute, + string $entry, + string $value, + bool $append = false + ):void { + $ldapEntry = $this->ldap->getEntry($entry . "," . $this->ldapBaseDN); + Laminas\Ldap\Attribute::setAttribute($ldapEntry, $attribute, $value, $append); + $this->ldap->update($entry . "," . $this->ldapBaseDN, $ldapEntry); + $this->theLdapUsersHaveBeenReSynced(); + } + + /** + * @param string $user + * @param string $group + * @param string|null $ou + * + * @return void + * @throws Exception + */ + public function addUserToLdapGroup(string $user, string $group, ?string $ou = null):void { + if ($ou === null) { + $ou = $this->getLdapGroupsOU(); + } + $memberAttr = ""; + $memberValue = ""; + if ($this->ldapGroupSchema == "rfc2307") { + $memberAttr = "memberUID"; + $memberValue = "$user"; + } else { + $memberAttr = "member"; + $userbase = "ou=" . $this->getLdapUsersOU() . "," . $this->ldapBaseDN; + $memberValue = "uid=$user" . "," . "$userbase"; + } + $this->setTheLdapAttributeOfTheEntryTo( + $memberAttr, + "cn=$group,ou=$ou", + $memberValue, + true + ); + } + + /** + * @param string $value + * @param string $attribute + * @param string $entry + * + * @return void + */ + public function deleteValueFromLdapAttribute(string $value, string $attribute, string $entry):void { + $this->ldap->deleteAttributes( + $entry . "," . $this->ldapBaseDN, + [$attribute => [$value]] + ); + } + + /** + * @param string $user + * @param string $group + * @param string|null $ou + * + * @return void + * @throws Exception + */ + public function removeUserFromLdapGroup(string $user, string $group, ?string $ou = null):void { + if ($ou === null) { + $ou = $this->getLdapGroupsOU(); + } + $memberAttr = ""; + $memberValue = ""; + if ($this->ldapGroupSchema == "rfc2307") { + $memberAttr = "memberUID"; + $memberValue = "$user"; + } else { + $memberAttr = "member"; + $userbase = "ou=" . $this->getLdapUsersOU() . "," . $this->ldapBaseDN; + $memberValue = "uid=$user" . "," . "$userbase"; + } + $this->deleteValueFromLdapAttribute( + $memberValue, + $memberAttr, + "cn=$group,ou=$ou" + ); + $this->theLdapUsersHaveBeenReSynced(); + } + + /** + * @param string $entry + * + * @return void + * @throws Exception + */ + public function deleteTheLdapEntry(string $entry):void { + $this->ldap->delete($entry . "," . $this->ldapBaseDN); + $this->theLdapUsersHaveBeenReSynced(); + } + + /** + * @param string $group + * @param string|null $ou + * + * @return void + * @throws LdapException + * @throws Exception + */ + public function deleteLdapGroup(string $group, ?string $ou = null):void { + if ($ou === null) { + $ou = $this->getLdapGroupsOU(); + } + $this->deleteTheLdapEntry("cn=$group,ou=$ou"); + $this->theLdapUsersHaveBeenReSynced(); + $key = \array_search($group, $this->ldapCreatedGroups); + if ($key !== false) { + unset($this->ldapCreatedGroups[$key]); + } + $this->rememberThatGroupIsNotExpectedToExist($group); + } + + /** + * @param string|null $username + * @param string|null $ou + * + * @return void + * @throws Exception + */ + public function deleteLdapUser(?string $username, ?string $ou = null):void { + if (!\in_array($username, $this->ldapCreatedUsers)) { + throw new Error( + "User " . $username . " was not created using Ldap and does not exist as an Ldap User" + ); + } + if ($ou === null) { + $ou = $this->getLdapUsersOU(); + } + $entry = "uid=$username,ou=$ou"; + $this->deleteTheLdapEntry($entry); + $key = \array_search($username, $this->ldapCreatedUsers); + if ($key !== false) { + unset($this->ldapCreatedUsers[$key]); + } + $this->rememberThatUserIsNotExpectedToExist($username); + } + + /** + * @param string|null $user + * @param string|null $displayName + * + * @return void + * @throws Exception + */ + public function editLdapUserDisplayName(?string $user, ?string $displayName):void { + $entry = "uid=" . $user . ",ou=" . $this->getLdapUsersOU(); + $this->setTheLdapAttributeOfTheEntryTo( + 'displayname', + $entry, + $displayName + ); + $this->theLdapUsersHaveBeenReSynced(); + } + + /** + * @When /^the administrator disables user "([^"]*)" using the provisioning API$/ + * + * @param string|null $user + * + * @return void + */ + public function adminDisablesUserUsingTheProvisioningApi(?string $user):void { + $user = $this->getActualUsername($user); + $this->disableOrEnableUser($this->getAdminUsername(), $user, 'disable'); + } + + /** + * @When the administrator disables the following users using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorDisablesTheFollowingUsersUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->adminDisablesUserUsingTheProvisioningApi($username["username"]); + } + } + + /** + * @Given /^user "([^"]*)" has been disabled$/ + * + * @param string|null $user + * + * @return void + * @throws Exception + */ + public function adminHasDisabledUserUsingTheProvisioningApi(?string $user):void { + $user = $this->getActualUsername($user); + $this->disableOrEnableUser($this->getAdminUsername(), $user, 'disable'); + $this->theHTTPStatusCodeShouldBeSuccess(); + $this->ocsContext->assertOCSResponseIndicatesSuccess(); + } + + /** + * @Given the following users have been disabled + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUsersHaveBeenDisabled(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->adminHasDisabledUserUsingTheProvisioningApi($username["username"]); + } + } + + /** + * @When user :user disables user :otherUser using the provisioning API + * + * @param string $user + * @param string $otherUser + * + * @return void + */ + public function userDisablesUserUsingTheProvisioningApi(string $user, string $otherUser):void { + $user = $this->getActualUsername($user); + $actualOtherUser = $this->getActualUsername($otherUser); + $this->disableOrEnableUser($user, $actualOtherUser, 'disable'); + } + + /** + * @When the administrator enables user :user using the provisioning API + * + * @param string $user + * + * @return void + */ + public function theAdministratorEnablesUserUsingTheProvisioningApi(string $user):void { + $this->disableOrEnableUser($this->getAdminUsername(), $user, 'enable'); + } + + /** + * @When the administrator enables the following users using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorEnablesTheFollowingUsersUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->theAdministratorEnablesUserUsingTheProvisioningApi($username["username"]); + } + } + + /** + * @When /^user "([^"]*)" enables user "([^"]*)" using the provisioning API$/ + * @When /^user "([^"]*)" tries to enable user "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $otherUser + * + * @return void + */ + public function userTriesToEnableUserUsingTheProvisioningApi( + string $user, + string $otherUser + ):void { + $this->disableOrEnableUser($user, $otherUser, 'enable'); + } + + /** + * @param string $user + * + * @return void + * @throws Exception + */ + public function deleteTheUserUsingTheProvisioningApi(string $user):void { + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + // Always try to delete the user + if (OcisHelper::isTestingWithGraphApi()) { + // users can be deleted using the username in the GraphApi too + $this->graphContext->adminDeletesUserUsingTheGraphApi($user); + } else { + $this->response = UserHelper::deleteUser( + $this->getBaseUrl(), + $user, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + } + $this->pushToLastStatusCodesArrays(); + + // Only log a message if the test really expected the user to have been + // successfully created (i.e. the delete is expected to work) and + // there was a problem deleting the user. Because in this case there + // might be an effect on later tests. + if ($this->theUserShouldExist($user) + && (!\in_array($this->response->getStatusCode(), [200, 204])) + ) { + \error_log( + "INFORMATION: could not delete user '$user' " + . $this->response->getStatusCode() . " " . $this->response->getBody() + ); + } + + $this->rememberThatUserIsNotExpectedToExist($user); + } + + /** + * @param string $group group name + * + * @return void + * @throws Exception + * @throws LdapException + */ + public function deleteGroup(string $group):void { + if ($this->groupExists($group)) { + if ($this->isTestingWithLdap() && \in_array($group, $this->ldapCreatedGroups)) { + $this->deleteLdapGroup($group); + } else { + $this->deleteTheGroupUsingTheProvisioningApi($group); + } + } + } + + /** + * @Given /^group "([^"]*)" has been deleted$/ + * + * @param string $group + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function groupHasBeenDeleted(string $group):void { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminHasDeletedGroupUsingTheGraphApi($group); + } else { + $this->deleteGroup($group); + } + $this->groupShouldNotExist($group); + } + + /** + * @When /^the administrator deletes group "([^"]*)" from the default user backend$/ + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function adminDeletesGroup(string $group):void { + $this->deleteGroup($group); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^the administrator deletes group "([^"]*)" using the provisioning API$/ + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function deleteTheGroupUsingTheProvisioningApi(string $group):void { + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + $this->response = UserHelper::deleteGroup( + $this->getBaseUrl(), + $group, + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + $this->pushToLastStatusCodesArrays(); + if ($this->theGroupShouldExist($group) + && $this->theGroupShouldBeAbleToBeDeleted($group) + && ($this->response->getStatusCode() !== 200) + ) { + \error_log( + "INFORMATION: could not delete group '$group'" + . $this->response->getStatusCode() . " " . $this->response->getBody() + ); + } + + $this->rememberThatGroupIsNotExpectedToExist($group); + } + + /** + * @When the administrator deletes the following groups using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorDeletesTheFollowingGroupsUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["groupname"]); + $groups = $table->getHash(); + foreach ($groups as $group) { + $this->deleteTheGroupUsingTheProvisioningApi($group["groupname"]); + } + } + + /** + * @When user :user tries to delete group :group using the provisioning API + * + * @param string $user + * @param string $group + * + * @return void + * @throws JsonException + */ + public function userTriesToDeleteGroupUsingTheProvisioningApi(string $user, string $group):void { + $this->response = UserHelper::deleteGroup( + $this->getBaseUrl(), + $group, + $this->getActualUsername($user), + $this->getActualPassword($user), + $this->getStepLineRef(), + $this->ocsApiVersion + ); + } + + /** + * @param string $group + * + * @return bool + * @throws Exception + * @throws GuzzleException + */ + public function groupExists(string $group):bool { + if ($this->isTestingWithLdap() && OcisHelper::isTestingOnOcisOrReva()) { + $baseDN = $this->getLdapBaseDN(); + $newDN = 'cn=' . $group . ',ou=' . $this->ldapGroupsOU . ',' . $baseDN; + if ($this->ldap->getEntry($newDN) !== null) { + return true; + } + return false; + } + if (OcisHelper::isTestingWithGraphApi()) { + $base = '/graph/v1.0'; + } else { + $base = '/ocs/v2.php/cloud'; + } + $group = \rawurlencode($group); + $fullUrl = $this->getBaseUrl() . "$base/groups/$group"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + if ($this->response->getStatusCode() >= 400) { + return false; + } + return true; + } + + /** + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function removeUserFromGroupAsAdminUsingTheProvisioningApi(string $user, string $group):void { + $this->userRemovesUserFromGroupUsingTheProvisioningApi( + $this->getAdminUsername(), + $user, + $group + ); + } + + /** + * @When the administrator removes user :user from group :group using the provisioning API + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function adminRemovesUserFromGroupUsingTheProvisioningApi(string $user, string $group):void { + $user = $this->getActualUsername($user); + $this->removeUserFromGroupAsAdminUsingTheProvisioningApi( + $user, + $group + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When the administrator removes the following users from the following groups using the provisioning API + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorRemovesTheFollowingUserFromTheFollowingGroupUsingTheProvisioningApi(TableNode $table):void { + $this->verifyTableNodeColumns($table, ['username', 'groupname']); + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + foreach ($table as $row) { + $this->adminRemovesUserFromGroupUsingTheProvisioningApi($row['username'], $row['groupname']); + $this->pushToLastStatusCodesArrays(); + } + } + + /** + * @Given user :user has been removed from group :group + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function adminHasRemovedUserFromGroup(string $user, string $group):void { + if ($this->isTestingWithLdap() + && !$this->isLocalAdminGroup($group) + && \in_array($group, $this->ldapCreatedGroups) + ) { + $this->removeUserFromLdapGroup($user, $group); + } elseif (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminHasRemovedUserFromGroupUsingTheGraphApi($user, $group); + } else { + $this->removeUserFromGroupAsAdminUsingTheProvisioningApi( + $user, + $group + ); + } + $this->userShouldNotBelongToGroup($user, $group); + } + + /** + * @When user :user removes user :otherUser from group :group using the provisioning API + * + * @param string $user + * @param string $otherUser + * @param string $group + * + * @return void + * @throws Exception + */ + public function userRemovesUserFromGroupUsingTheProvisioningApi( + string $user, + string $otherUser, + string $group + ):void { + $this->userTriesToRemoveUserFromGroupUsingTheProvisioningApi( + $user, + $otherUser, + $group + ); + + if ($this->response->getStatusCode() !== 200) { + \error_log( + "INFORMATION: could not remove user '$user' from group '$group'" + . $this->response->getStatusCode() . " " . $this->response->getBody() + ); + } + } + + /** + * @When user :user tries to remove user :otherUser from group :group using the provisioning API + * + * @param string $user + * @param string $otherUser + * @param string $group + * + * @return void + * @throws Exception + */ + public function userTriesToRemoveUserFromGroupUsingTheProvisioningApi( + string $user, + string $otherUser, + string $group + ):void { + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $actualOtherUser = $this->getActualUsername($otherUser); + $this->response = UserHelper::removeUserFromGroup( + $this->getBaseUrl(), + $actualOtherUser, + $group, + $actualUser, + $actualPassword, + $this->getStepLineRef(), + $this->ocsApiVersion + ); + } + + /** + * @When /^the administrator makes user "([^"]*)" a subadmin of group "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $group + * + * @return void + */ + public function adminMakesUserSubadminOfGroupUsingTheProvisioningApi( + string $user, + string $group + ):void { + $user = $this->getActualUsername($user); + $this->userMakesUserASubadminOfGroupUsingTheProvisioningApi( + $this->getAdminUsername(), + $user, + $group + ); + } + + /** + * @When user :user makes user :otherUser a subadmin of group :group using the provisioning API + * + * @param string $user + * @param string $otherUser + * @param string $group + * + * @return void + * @throws Exception + */ + public function userMakesUserASubadminOfGroupUsingTheProvisioningApi( + string $user, + string $otherUser, + string $group + ):void { + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $actualSubadminUsername = $this->getActualUsername($otherUser); + + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$actualSubadminUsername/subadmins"; + $body = ['groupid' => $group]; + $this->response = HttpRequestHelper::post( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword, + null, + $body + ); + } + + /** + * @When the administrator gets all the groups where user :user is subadmin using the provisioning API + * + * @param string $user + * + * @return void + */ + public function theAdministratorGetsAllTheGroupsWhereUserIsSubadminUsingTheProvisioningApi(string $user):void { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$user/subadmins"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + } + + /** + * @When /^user "([^"]*)" gets all the groups where user "([^"]*)" is subadmin using the provisioning API$/ + * @When /^user "([^"]*)" tries to get all the groups where user "([^"]*)" is subadmin using the provisioning API$/ + * + * @param string $user + * @param string $otherUser + * + * @return void + * @throws Exception + */ + public function userTriesToGetAllTheGroupsWhereUserIsSubadminUsingTheProvisioningApi(string $user, string $otherUser):void { + $actualOtherUser = $this->getActualUsername($otherUser); + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$actualOtherUser/subadmins"; + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword + ); + } + + /** + * @Given /^user "([^"]*)" has been made a subadmin of group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + */ + public function userHasBeenMadeSubadminOfGroup( + string $user, + string $group + ):void { + $this->adminMakesUserSubadminOfGroupUsingTheProvisioningApi( + $user, + $group + ); + Assert::assertEquals( + 200, + $this->response->getStatusCode() + ); + } + + /** + * @When the administrator gets all the subadmins of group :group using the provisioning API + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function theAdministratorGetsAllTheSubadminsOfGroupUsingTheProvisioningApi(string $group):void { + $this->userGetsAllTheSubadminsOfGroupUsingTheProvisioningApi( + $this->getAdminUsername(), + $group + ); + } + + /** + * @When user :user gets all the subadmins of group :group using the provisioning API + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userGetsAllTheSubadminsOfGroupUsingTheProvisioningApi(string $user, string $group):void { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/groups/$group/subadmins"; + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword + ); + } + + /** + * @When the administrator removes user :user from being a subadmin of group :group using the provisioning API + * + * @param string $user + * @param string $group + * + * @return void + */ + public function theAdministratorRemovesUserFromBeingASubadminOfGroupUsingTheProvisioningApi( + string $user, + string $group + ):void { + $this->userRemovesUserFromBeingASubadminOfGroupUsingTheProvisioningApi( + $this->getAdminUsername(), + $user, + $group + ); + } + + /** + * @When user :user removes user :otherUser from being a subadmin of group :group using the provisioning API + * + * @param string $user + * @param string $otherUser + * @param string $group + * + * @return void + * @throws Exception + */ + public function userRemovesUserFromBeingASubadminOfGroupUsingTheProvisioningApi( + string $user, + string $otherUser, + string $group + ):void { + $actualOtherUser = $this->getActualUsername($otherUser); + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$actualOtherUser/subadmins"; + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getUserPassword($actualUser); + $this->response = HttpRequestHelper::delete( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword, + null, + ['groupid' => $group] + ); + } + + /** + * @Then /^the users returned by the API should be$/ + * + * @param TableNode $usersList + * + * @return void + * @throws Exception + */ + public function theUsersShouldBe(TableNode $usersList):void { + $this->verifyTableNodeColumnsCount($usersList, 1); + $users = $usersList->getRows(); + $usersSimplified = \array_map( + function ($user) { + return $this->getActualUsername($user); + }, + $this->simplifyArray($users) + ); + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->theseUsersShouldBeInTheResponse($usersSimplified); + } else { + $respondedArray = $this->getArrayOfUsersResponded($this->response); + Assert::assertEqualsCanonicalizing( + $usersSimplified, + $respondedArray, + __METHOD__ + . " Provided users do not match the users returned in the response." + ); + } + } + + /** + * @Then /^the users returned by the API should include$/ + * + * @param TableNode $usersList + * + * @return void + * @throws Exception + */ + public function theUsersShouldInclude(TableNode $usersList):void { + $this->verifyTableNodeColumnsCount($usersList, 1); + $users = $usersList->getRows(); + $usersSimplified = \array_map( + function ($user) { + return $this->getActualUsername($user); + }, + $this->simplifyArray($users) + ); + $respondedArray = $this->getArrayOfUsersResponded($this->response); + foreach ($usersSimplified as $userElement) { + Assert::assertContains( + $userElement, + $respondedArray, + __METHOD__ + . " user $userElement is not present in the users list: \n" + . \join("\n", $respondedArray) + ); + } + } + + /** + * @Then /^the groups returned by the API should be$/ + * + * @param TableNode $groupsList + * + * @return void + * @throws Exception + */ + public function theGroupsShouldBe(TableNode $groupsList):void { + $this->verifyTableNodeColumnsCount($groupsList, 1); + $groups = $groupsList->getRows(); + $groupsSimplified = $this->simplifyArray($groups); + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->theseGroupsShouldBeInTheResponse($groupsSimplified); + } else { + Assert::assertEqualsCanonicalizing( + $groupsSimplified, + $respondedArray, + __METHOD__ + . " Provided groups do not match the groups returned in the response." + ); + } + } + + /** + * @Then /^the extra groups returned by the API should be$/ + * + * @param TableNode $groupsList + * + * @return void + * @throws Exception + */ + public function theExtraGroupsShouldBe(TableNode $groupsList):void { + $this->verifyTableNodeColumnsCount($groupsList, 1); + $groups = $groupsList->getRows(); + $groupsSimplified = $this->simplifyArray($groups); + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->theseGroupsShouldBeInTheResponse($groupsSimplified); + } else { + $expectedGroups = \array_merge($this->startingGroups, $groupsSimplified); + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + Assert::assertEqualsCanonicalizing( + $expectedGroups, + $respondedArray, + __METHOD__ + . " Provided groups do not match the groups returned in the response." + ); + } + } + + /** + * @Then /^the groups returned by the API should include "([^"]*)"$/ + * + * @param string $group + * + * @return void + */ + public function theGroupsReturnedByTheApiShouldInclude(string $group):void { + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + Assert::assertContains($group, $respondedArray); + } + + /** + * @Then /^the groups returned by the API should not include "([^"]*)"$/ + * + * @param string $group + * + * @return void + */ + public function theGroupsReturnedByTheApiShouldNotInclude(string $group):void { + $respondedArray = $this->getArrayOfGroupsResponded($this->response); + Assert::assertNotContains($group, $respondedArray); + } + + /** + * @Then /^the users returned by the API should not include "([^"]*)"$/ + * + * @param string $user + * + * @return void + */ + public function theUsersReturnedByTheApiShouldNotInclude(string $user):void { + $respondedArray = $this->getArrayOfUsersResponded($this->response); + Assert::assertNotContains($user, $respondedArray); + } + + /** + * @param TableNode|null $groupsOrUsersList + * + * @return void + * @throws Exception + */ + public function checkSubadminGroupsOrUsersTable(?TableNode $groupsOrUsersList):void { + $this->verifyTableNodeColumnsCount($groupsOrUsersList, 1); + $tableRows = $groupsOrUsersList->getRows(); + $simplifiedTableRows = $this->simplifyArray($tableRows); + $respondedArray = $this->getArrayOfSubadminsResponded($this->response); + Assert::assertEqualsCanonicalizing( + $simplifiedTableRows, + $respondedArray + ); + } + + /** + * @Then /^the subadmin groups returned by the API should be$/ + * + * @param TableNode|null $groupsList + * + * @return void + * @throws Exception + */ + public function theSubadminGroupsShouldBe(?TableNode $groupsList):void { + $this->checkSubadminGroupsOrUsersTable($groupsList); + } + + /** + * @Then /^the subadmin users returned by the API should be$/ + * + * @param TableNode|null $usersList + * + * @return void + * @throws Exception + */ + public function theSubadminUsersShouldBe(?TableNode $usersList):void { + $this->checkSubadminGroupsOrUsersTable($usersList); + } + + /** + * @Then /^the apps returned by the API should include$/ + * + * @param TableNode|null $appList + * + * @return void + * @throws Exception + */ + public function theAppsShouldInclude(?TableNode $appList):void { + $this->verifyTableNodeColumnsCount($appList, 1); + $apps = $appList->getRows(); + $appsSimplified = $this->simplifyArray($apps); + $respondedArray = $this->getArrayOfAppsResponded($this->response); + foreach ($appsSimplified as $app) { + Assert::assertContains( + $app, + $respondedArray, + "The apps returned by the API should include $app but $app was not included" + ); + } + } + + /** + * @Then /^the apps returned by the API should not include$/ + * + * @param TableNode|null $appList + * + * @return void + * @throws Exception + */ + public function theAppsShouldNotInclude(?TableNode $appList):void { + $this->verifyTableNodeColumnsCount($appList, 1); + $apps = $appList->getRows(); + $appsSimplified = $this->simplifyArray($apps); + $respondedArray = $this->getArrayOfAppsResponded($this->response); + foreach ($appsSimplified as $app) { + Assert::assertNotContains( + $app, + $respondedArray, + "The apps returned by the API should not include $app but $app was included" + ); + } + } + + /** + * @Then /^app "([^"]*)" should not be in the apps list$/ + * + * @param string $appName + * + * @return void + */ + public function appShouldNotBeInTheAppsList(string $appName):void { + $fullUrl = $this->getBaseUrl() . "/ocs/v2.php/cloud/apps"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + $respondedArray = $this->getArrayOfAppsResponded($this->response); + Assert::assertNotContains($appName, $respondedArray); + } + + /** + * @Then /^user "([^"]*)" should be a subadmin of group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userShouldBeASubadminOfGroup(string $user, string $group):void { + $this->theAdministratorGetsAllTheSubadminsOfGroupUsingTheProvisioningApi($group); + Assert::assertEquals( + 200, + $this->response->getStatusCode() + ); + $listOfSubadmins = $this->getArrayOfSubadminsResponded($this->response); + Assert::assertContains( + $user, + $listOfSubadmins + ); + } + + /** + * @Then /^user "([^"]*)" should not be a subadmin of group "([^"]*)"$/ + * + * @param string $user + * @param string $group + * + * @return void + * @throws Exception + */ + public function userShouldNotBeASubadminOfGroup(string $user, string $group):void { + $this->theAdministratorGetsAllTheSubadminsOfGroupUsingTheProvisioningApi($group); + $listOfSubadmins = $this->getArrayOfSubadminsResponded($this->response); + Assert::assertNotContains( + $user, + $listOfSubadmins + ); + } + + /** + * @Then /^the display name returned by the API should be "([^"]*)"$/ + * + * @param string $expectedDisplayName + * + * @return void + * @throws Exception + */ + public function theDisplayNameReturnedByTheApiShouldBe(string $expectedDisplayName):void { + $responseDisplayName = (string) $this->getResponseXml(null, __METHOD__)->data[0]->displayname; + Assert::assertEquals( + $expectedDisplayName, + $responseDisplayName + ); + } + + /** + * @Then /^the display name of user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $user + * @param string $displayname + * + * @return void + * @throws Exception + */ + public function theDisplayNameOfUserShouldBe(string $user, string $displayname):void { + $actualUser = $this->getActualUsername($user); + $this->retrieveUserInformationAsAdminUsingProvisioningApi($actualUser); + $this->theDisplayNameReturnedByTheApiShouldBe($displayname); + } + + /** + * @Then the display name of the following users should be + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theDisplayNameOfTheFollowingUsersShouldBe(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["name", "display-name"]); + $users = $table->getHash(); + foreach ($users as $user) { + $this->theDisplayNameOfUserShouldBe($user["name"], $user["display-name"]); + } + } + + /** + * @Then /^the display name of user "([^"]*)" should not have changed$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theDisplayNameOfUserShouldNotHaveChanged(string $user):void { + $actualUser = $this->getActualUsername($user); + $expectedDisplayName = $this->getDisplayNameForUser($user); + $this->retrieveUserInformationAsAdminUsingProvisioningApi($actualUser); + $this->theDisplayNameReturnedByTheApiShouldBe($expectedDisplayName); + } + + /** + * @Then /^the email address returned by the API should be "([^"]*)"$/ + * + * @param string $expectedEmailAddress + * + * @return void + * @throws Exception + */ + public function theEmailAddressReturnedByTheApiShouldBe(string $expectedEmailAddress):void { + $responseEmailAddress = (string) $this->getResponseXml(null, __METHOD__)->data[0]->email; + Assert::assertEquals( + $expectedEmailAddress, + $responseEmailAddress + ); + } + + /** + * @Then /^the email address of user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $user + * @param string $expectedEmailAddress + * + * @return void + * @throws Exception + */ + public function theEmailAddressOfUserShouldBe(string $user, string $expectedEmailAddress):void { + $user = $this->getActualUsername($user); + $this->retrieveUserInformationAsAdminUsingProvisioningApi($user); + $this->theEmailAddressReturnedByTheApiShouldBe($expectedEmailAddress); + } + + /** + * @Then /^the email address of user "([^"]*)" should not have changed$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function theEmailAddressOfUserShouldNotHaveChanged(string $user):void { + $user = $this->getActualUsername($user); + $expectedEmailAddress = $this->getEmailAddressForUser($user); + $this->retrieveUserInformationAsAdminUsingProvisioningApi($user); + $this->theEmailAddressReturnedByTheApiShouldBe($expectedEmailAddress); + } + + /** + * @Then /^the free, used, total and relative quota returned by the API should exist and be valid numbers$/ + * + * @return void + * @throws Exception + */ + public function theQuotaFieldsReturnedByTheApiShouldBValid():void { + $quotaData = $this->getResponseXml(null, __METHOD__)->data[0]->quota; + $missingQuotaDataString = ""; + if (!isset($quotaData->free)) { + $missingQuotaDataString .= "free "; + } + if (!isset($quotaData->used)) { + $missingQuotaDataString .= "used "; + } + if (!isset($quotaData->total)) { + $missingQuotaDataString .= "total "; + } + if (!isset($quotaData->relative)) { + $missingQuotaDataString .= "relative "; + } + Assert::assertSame( + "", + $missingQuotaDataString, + "These quota data items are missing: $missingQuotaDataString" + ); + $freeQuota = (string) $quotaData->free; + Assert::assertIsNumeric($freeQuota, "free quota '$freeQuota' is not numeric"); + $usedQuota = (string) $quotaData->used; + Assert::assertIsNumeric($usedQuota, "used quota '$usedQuota' is not numeric"); + $totalQuota = (string) $quotaData->total; + Assert::assertIsNumeric($totalQuota, "total quota '$totalQuota' is not numeric"); + $relativeQuota = (string) $quotaData->relative; + Assert::assertIsNumeric($relativeQuota, "free quota '$relativeQuota' is not numeric"); + Assert::assertSame( + (int) $freeQuota + (int) $usedQuota, + (int) $totalQuota, + "free $freeQuota plus used $usedQuota quota is not equal to total quota $totalQuota" + ); + } + + /** + * @Then /^the quota definition returned by the API should be "([^"]*)"$/ + * + * @param string $expectedQuotaDefinition a string that describes the quota + * + * @return void + * @throws Exception + */ + public function theQuotaDefinitionReturnedByTheApiShouldBe(string $expectedQuotaDefinition):void { + $responseQuotaDefinition = (string) $this->getResponseXml(null, __METHOD__)->data[0]->quota->definition; + Assert::assertEquals( + $expectedQuotaDefinition, + $responseQuotaDefinition + ); + } + + /** + * @Then /^the quota definition of user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $user + * @param string $expectedQuotaDefinition + * + * @return void + * @throws Exception + */ + public function theQuotaDefinitionOfUserShouldBe(string $user, string $expectedQuotaDefinition):void { + $this->retrieveUserInformationAsAdminUsingProvisioningApi($user); + $this->theQuotaDefinitionReturnedByTheApiShouldBe($expectedQuotaDefinition); + } + + /** + * @Then /^the last login returned by the API should be a current Unix timestamp$/ + * + * @return void + * @throws Exception + */ + public function theLastLoginReturnedByTheApiShouldBe():void { + $responseLastLogin = (string) $this->getResponseXml(null, __METHOD__)->data[0]->last_login; + Assert::assertIsNumeric($responseLastLogin); + Assert::assertGreaterThan($this->scenarioStartTime, (int) $responseLastLogin); + } + + /** + * Parses the xml answer to get the array of users returned. + * + * @param ResponseInterface $resp + * + * @return array + * @throws Exception + */ + public function getArrayOfUsersResponded(ResponseInterface $resp):array { + $listCheckedElements + = $this->getResponseXml($resp, __METHOD__)->data[0]->users[0]->element; + $extractedElementsArray + = \json_decode(\json_encode($listCheckedElements), true); + return $extractedElementsArray; + } + + /** + * Parses the xml answer to get the array of groups returned. + * + * @param ResponseInterface $resp + * + * @return array + * @throws Exception + */ + public function getArrayOfGroupsResponded(ResponseInterface $resp):array { + $listCheckedElements + = $this->getResponseXml($resp, __METHOD__)->data[0]->groups[0]->element; + $extractedElementsArray + = \json_decode(\json_encode($listCheckedElements), true); + return $extractedElementsArray; + } + + /** + * Parses the xml answer to get the array of apps returned. + * + * @param ResponseInterface $resp + * + * @return array + * @throws Exception + */ + public function getArrayOfAppsResponded(ResponseInterface $resp):array { + $listCheckedElements + = $this->getResponseXml($resp, __METHOD__)->data[0]->apps[0]->element; + $extractedElementsArray + = \json_decode(\json_encode($listCheckedElements), true); + return $extractedElementsArray; + } + + /** + * Parses the xml answer to get the array of subadmins returned. + * + * @param ResponseInterface $resp + * + * @return array + * @throws Exception + */ + public function getArrayOfSubadminsResponded(ResponseInterface $resp):array { + $listCheckedElements + = $this->getResponseXml($resp, __METHOD__)->data[0]->element; + $extractedElementsArray + = \json_decode(\json_encode($listCheckedElements), true); + return $extractedElementsArray; + } + + /** + * Parses the xml answer to get the array of app info returned for an app. + * + * @param ResponseInterface $resp + * + * @return array + * @throws Exception + */ + public function getArrayOfAppInfoResponded(ResponseInterface $resp):array { + $listCheckedElements + = $this->getResponseXml($resp, __METHOD__)->data[0]; + $extractedElementsArray + = \json_decode(\json_encode($listCheckedElements), true); + return $extractedElementsArray; + } + + /** + * @Then /^app "([^"]*)" should be disabled$/ + * + * @param string $app + * + * @return void + * @throws Exception + */ + public function appShouldBeDisabled(string $app):void { + $fullUrl = $this->getBaseUrl() + . "/ocs/v2.php/cloud/apps?filter=disabled"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + $respondedArray = $this->getArrayOfAppsResponded($this->response); + Assert::assertContains($app, $respondedArray); + Assert::assertEquals( + 200, + $this->response->getStatusCode() + ); + } + + /** + * @Then /^app "([^"]*)" should be enabled$/ + * + * @param string $app + * + * @return void + * @throws Exception + */ + public function appShouldBeEnabled(string $app):void { + $fullUrl = $this->getBaseUrl() . "/ocs/v2.php/cloud/apps?filter=enabled"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + $respondedArray = $this->getArrayOfAppsResponded($this->response); + Assert::assertContains($app, $respondedArray); + Assert::assertEquals( + 200, + $this->response->getStatusCode() + ); + } + + /** + * @Then /^the information for app "([^"]*)" should have a valid version$/ + * + * @param string $app + * + * @return void + * @throws Exception + */ + public function theInformationForAppShouldHaveAValidVersion(string $app):void { + $fullUrl = $this->getBaseUrl() . "/ocs/v2.php/cloud/apps/$app"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + Assert::assertEquals( + 200, + $this->response->getStatusCode() + ); + $respondedArray = $this->getArrayOfAppInfoResponded($this->response); + Assert::assertArrayHasKey( + 'version', + $respondedArray, + "app info returned for $app app does not have a version" + ); + $appVersion = $respondedArray['version']; + Assert::assertTrue( + \substr_count($appVersion, '.') > 1, + "app version '$appVersion' returned in app info is not a valid version string" + ); + } + + /** + * @Then /^user "([^"]*)" should be disabled$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userShouldBeDisabled(string $user):void { + $user = $this->getActualUsername($user); + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$user"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + Assert::assertEquals( + "false", + $this->getResponseXml(null, __METHOD__)->data[0]->enabled + ); + } + + /** + * @Then the following users should be disabled + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUsersShouldBeDisabled(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->userShouldBeDisabled($username["username"]); + } + } + + /** + * @Then /^user "([^"]*)" should be enabled$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userShouldBeEnabled(string $user):void { + $user = $this->getActualUsername($user); + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$user"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + Assert::assertEquals( + "true", + $this->getResponseXml(null, __METHOD__)->data[0]->enabled + ); + } + + /** + * @Then the following users should be enabled + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingUsersShouldBeEnabled(TableNode $table):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $this->userShouldBeEnabled($username["username"]); + } + } + + /** + * @When the administrator sets the quota of user :user to :quota using the provisioning API + * + * @param string $user + * @param string $quota + * + * @return void + */ + public function adminSetsUserQuotaToUsingTheProvisioningApi(string $user, string $quota):void { + $user = $this->getActualUsername($user); + $body + = [ + 'key' => 'quota', + 'value' => $quota, + ]; + + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + "PUT", + "/cloud/users/$user", + $this->getStepLineRef(), + $body, + 2 + ); + } + + /** + * @Given the quota of user :user has been set to :quota + * + * @param string $user + * @param string $quota + * + * @return void + */ + public function theQuotaOfUserHasBeenSetTo(string $user, string $quota):void { + $this->adminSetsUserQuotaToUsingTheProvisioningApi($user, $quota); + $this->theHTTPStatusCodeShouldBe(200); + } + + /** + * @When the administrator gives unlimited quota to user :user using the provisioning API + * + * @param string $user + * + * @return void + */ + public function adminGivesUnlimitedQuotaToUserUsingTheProvisioningApi(string $user):void { + $this->adminSetsUserQuotaToUsingTheProvisioningApi($user, 'none'); + } + + /** + * @Given user :user has been given unlimited quota + * + * @param string $user + * + * @return void + */ + public function userHasBeenGivenUnlimitedQuota(string $user):void { + $this->theQuotaOfUserHasBeenSetTo($user, 'none'); + } + + /** + * Returns home path of the given user + * + * @param string $user + * + * @return string + * @throws Exception + */ + public function getUserHome(string $user):string { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$user"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + return $this->getResponseXml(null, __METHOD__)->data[0]->home; + } + + /** + * @Then /^the user attributes returned by the API should include$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function checkUserAttributes(?TableNode $body):void { + $this->verifyTableNodeRows($body, [], $this->userResponseFields); + $bodyRows = $body->getRowsHash(); + foreach ($bodyRows as $field => $value) { + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + $field_array = \explode(' ', $field); + foreach ($field_array as $field_name) { + $data = $data->$field_name; + } + if ($data != $value) { + Assert::fail( + "$field has value $data" + ); + } + } + } + + /** + * @Then the attributes of user :user returned by the API should include + * + * @param string $user + * @param TableNode $body + * + * @return void + * @throws Exception + */ + public function checkAttributesForUser(string $user, TableNode $body):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumnsCount($body, 2); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $this->getAdminUsername(), + "GET", + "/cloud/users/$user", + null + ); + $this->checkUserAttributes($body); + } + + /** + * @Then /^the API should not return any data$/ + * + * @return void + * @throws Exception + */ + public function theApiShouldNotReturnAnyData():void { + $responseData = $this->getResponseXml(null, __METHOD__)->data[0]; + Assert::assertEmpty( + $responseData, + "Response data is not empty but it should be empty" + ); + } + + /** + * @Then /^the list of users returned by the API should be empty$/ + * + * @return void + * @throws Exception + */ + public function theListOfUsersReturnedByTheApiShouldBeEmpty():void { + $usersList = $this->getResponseXml(null, __METHOD__)->data[0]->users[0]; + Assert::assertEmpty( + $usersList, + "Users list is not empty but it should be empty" + ); + } + + /** + * @Then /^the list of groups returned by the API should be empty$/ + * + * @return void + * @throws Exception + */ + public function theListOfGroupsReturnedByTheApiShouldBeEmpty():void { + $groupsList = $this->getResponseXml(null, __METHOD__)->data[0]->groups[0]; + Assert::assertEmpty( + $groupsList, + "Groups list is not empty but it should be empty" + ); + } + + /** + * @AfterScenario + * + * @return void + * @throws Exception + */ + public function afterScenario():void { + $this->waitForDavRequestsToFinish(); + $this->restoreParametersAfterScenario(); + + if (OcisHelper::isTestingOnOcisOrReva() && $this->someUsersHaveBeenCreated()) { + foreach ($this->getCreatedUsers() as $user) { + OcisHelper::deleteRevaUserData($user["actualUsername"]); + } + } elseif (OcisHelper::isTestingOnOc10()) { + $this->resetAdminUserAttributes(); + } + if ($this->isTestingWithLdap()) { + $this->deleteLdapUsersAndGroups(); + } + $this->cleanupDatabaseUsers(); + $this->cleanupDatabaseGroups(); + } + + /** + * + * @return void + * @throws Exception + */ + public function resetAdminUserAttributes():void { + if ($this->adminDisplayName !== '') { + $this->adminChangesTheDisplayNameOfUserUsingTheProvisioningApi( + $this->getAdminUsername(), + '' + ); + } + if ($this->adminEmailAddress !== '') { + $this->adminChangesTheEmailOfUserToUsingTheProvisioningApi( + $this->getAdminUsername(), + '' + ); + } + } + + /** + * + * @return void + * @throws Exception + */ + public function cleanupDatabaseUsers():void { + $this->authContext->deleteTokenAuthEnforcedAfterScenario(); + $previousServer = $this->currentServer; + $this->usingServer('LOCAL'); + foreach ($this->createdUsers as $user => $userData) { + $this->deleteUser($userData['actualUsername']); + } + $this->usingServer('REMOTE'); + foreach ($this->createdRemoteUsers as $remoteUser => $userData) { + $this->deleteUser($userData['actualUsername']); + } + $this->usingServer($previousServer); + } + + /** + * + * @return void + * @throws Exception + */ + public function cleanupDatabaseGroups():void { + $this->authContext->deleteTokenAuthEnforcedAfterScenario(); + $previousServer = $this->currentServer; + $this->usingServer('LOCAL'); + foreach ($this->createdGroups as $group => $groupData) { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminDeletesGroupWithGroupId( + $groupData['id'] + ); + } else { + $this->cleanupGroup((string)$group); + } + } + $this->usingServer('REMOTE'); + foreach ($this->createdRemoteGroups as $remoteGroup => $groupData) { + if (OcisHelper::isTestingWithGraphApi()) { + $this->graphContext->adminDeletesGroupWithGroupId( + $groupData['id'] + ); + } else { + $this->cleanupGroup((string)$remoteGroup); + } + } + $this->usingServer($previousServer); + } + + /** + * @BeforeScenario + * + * @return void + * @throws Exception + */ + public function rememberAppEnabledDisabledState():void { + if (!OcisHelper::isTestingOnOcisOrReva()) { + SetupHelper::init( + $this->getAdminUsername(), + $this->getAdminPassword(), + $this->getBaseUrl(), + $this->getOcPath() + ); + $this->runOcc(['app:list', '--output json']); + $apps = \json_decode($this->getStdOutOfOccCommand(), true); + $this->enabledApps = \array_keys($apps["enabled"]); + $this->disabledApps = \array_keys($apps["disabled"]); + } + } + + /** + * @BeforeScenario @rememberGroupsThatExist + * + * @return void + * @throws Exception + */ + public function rememberGroupsThatExistAtTheStartOfTheScenario():void { + $this->startingGroups = $this->getArrayOfGroupsResponded($this->getAllGroups()); + } + + /** + * @AfterScenario + * + * @return void + * @throws Exception + */ + public function restoreAppEnabledDisabledState():void { + if (!OcisHelper::isTestingOnOcisOrReva() && !$this->isRunningForDbConversion()) { + $this->runOcc(['app:list', '--output json']); + + $apps = \json_decode($this->getStdOutOfOccCommand(), true); + $currentlyEnabledApps = \array_keys($apps["enabled"]); + $currentlyDisabledApps = \array_keys($apps["disabled"]); + + foreach ($currentlyDisabledApps as $disabledApp) { + if (\in_array($disabledApp, $this->enabledApps)) { + $this->adminEnablesOrDisablesApp('enables', $disabledApp); + } + } + + foreach ($currentlyEnabledApps as $enabledApp) { + if (\in_array($enabledApp, $this->disabledApps)) { + $this->adminEnablesOrDisablesApp('disables', $enabledApp); + } + } + } + } + + /** + * disable or enable user + * + * @param string $user + * @param string $otherUser + * @param string $action + * + * @return void + */ + public function disableOrEnableUser(string $user, string $otherUser, string $action):void { + $actualUser = $this->getActualUsername($user); + $actualPassword = $this->getPasswordForUser($actualUser); + $actualOtherUser = $this->getActualUsername($otherUser); + + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/users/$actualOtherUser/$action"; + $this->response = HttpRequestHelper::put( + $fullUrl, + $this->getStepLineRef(), + $actualUser, + $actualPassword + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * Returns an array of all apps reported by the cloud/apps endpoint + * + * @return array + * @throws Exception + */ + public function getAllApps():array { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/apps"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + return ($this->getArrayOfAppsResponded($this->response)); + } + + /** + * Returns array of enabled apps reported by the cloud/apps endpoint + * + * @return array + * @throws Exception + */ + public function getEnabledApps():array { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/apps?filter=enabled"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + return ($this->getArrayOfAppsResponded($this->response)); + } + + /** + * Returns array of disabled apps reported by the cloud/apps endpoint + * + * @return array + * @throws Exception + */ + public function getDisabledApps():array { + $fullUrl = $this->getBaseUrl() + . "/ocs/v{$this->ocsApiVersion}.php/cloud/apps?filter=disabled"; + $this->response = HttpRequestHelper::get( + $fullUrl, + $this->getStepLineRef(), + $this->getAdminUsername(), + $this->getAdminPassword() + ); + return ($this->getArrayOfAppsResponded($this->response)); + } + + /** + * Removes skeleton directory config from config.php and returns the config value + * + * @param string|null $baseUrl + * + * @return string + * @throws Exception + */ + public function popSkeletonDirectoryConfig(?string $baseUrl = null):string { + $path = $this->getSkeletonDirectory($baseUrl); + $this->runOcc( + ["config:system:delete skeletondirectory"], + null, + null, + $baseUrl + ); + return $path; + } + + /** + * @param string|null $baseUrl + * + * @return string + * @throws Exception + */ + private function getSkeletonDirectory(?string $baseUrl = null):string { + $this->runOcc( + ["config:system:get skeletondirectory"], + null, + null, + $baseUrl + ); + return \trim($this->getStdOutOfOccCommand()); + } + + /** + * Get the name of the smallest available skeleton, to "simulate" without skeleton. + * + * In ownCloud 10 there is always a skeleton directory. If none is specified + * then whatever is in core/skeleton is used. That contains different files + * and folders depending on the build that is being tested. So for testing + * we have "empty" skeleton that is created on-the-fly by the testing app. + * That provides a consistent skeleton for test scenarios that specify + * "without skeleton files" + * + * @return string name of the smallest skeleton folder + */ + private function getSmallestSkeletonDirName(): string { + return "empty"; + } + + /** + * @return bool + */ + private function isEmptySkeleton(): bool { + $skeletonDir = \getenv("SKELETON_DIR"); + if (($skeletonDir !== false) && (\basename($skeletonDir) === $this->getSmallestSkeletonDirName() . "Skeleton")) { + return true; + } + return false; + } + + /** + * sets the skeletondirectory according to the type + * + * @param string $skeletonType can be "tiny", "small", "large" OR empty. + * If an empty string is given, the current + * setting will not be changed + * + * @return string skeleton folder before the change + * @throws Exception + */ + private function setSkeletonDirByType(string $skeletonType): string { + if (OcisHelper::isTestingOnOcisOrReva()) { + $originalSkeletonPath = \getenv("SKELETON_DIR"); + if ($originalSkeletonPath === false) { + $originalSkeletonPath = ''; + } + if ($skeletonType !== '') { + $skeletonDirName = $skeletonType . "Skeleton"; + $newSkeletonPath = \dirname($originalSkeletonPath) . '/' . $skeletonDirName; + \putenv( + "SKELETON_DIR=" . $newSkeletonPath + ); + } + } else { + $baseUrl = $this->getBaseUrl(); + $originalSkeletonPath = $this->getSkeletonDirectory($baseUrl); + + if ($skeletonType !== '') { + OcsApiHelper::sendRequest( + $baseUrl, + $this->getAdminUsername(), + $this->getAdminPassword(), + 'POST', + "/apps/testing/api/v1/testingskeletondirectory", + $this->getStepLineRef(), + [ + 'directory' => $skeletonType . "Skeleton" + ], + $this->getOcsApiVersion() + ); + } + } + return $originalSkeletonPath; + } + + /** + * sets the skeletondirectory + * + * @param string $skeletonDir Full path of the skeleton directory + * If an empty string is given, the current + * setting will not be changed + * + * @return string skeleton folder before the change + * @throws Exception + */ + private function setSkeletonDir(string $skeletonDir): string { + if (OcisHelper::isTestingOnOcisOrReva()) { + $originalSkeletonPath = \getenv("SKELETON_DIR"); + if ($skeletonDir !== '') { + \putenv("SKELETON_DIR=" . $skeletonDir); + } + } else { + $baseUrl = $this->getBaseUrl(); + $originalSkeletonPath = $this->getSkeletonDirectory($baseUrl); + if ($skeletonDir !== '') { + $this->runOcc( + ["config:system:set skeletondirectory --value $skeletonDir"], + null, + null, + $baseUrl + ); + } + } + return $originalSkeletonPath; + } +} diff --git a/tests/acceptance/features/bootstrap/PublicWebDavContext.php b/tests/acceptance/features/bootstrap/PublicWebDavContext.php new file mode 100644 index 000000000..bdcb48f67 --- /dev/null +++ b/tests/acceptance/features/bootstrap/PublicWebDavContext.php @@ -0,0 +1,1661 @@ + + * @copyright Copyright (c) 2018, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use PHPUnit\Framework\Assert; +use TestHelpers\HttpRequestHelper; +use TestHelpers\OcisHelper; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * context file for steps that execute actions as "the public". + */ +class PublicWebDavContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * + * @var OccContext + */ + private $occContext; + + /** + * @When /^the public downloads the last public link shared file with range "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $range ignore if empty + * @param string $publicWebDAVAPIVersion + * @param string|null $password + * + * @return void + */ + public function downloadPublicFileWithRange(string $range, string $publicWebDAVAPIVersion, ?string $password = ""):void { + if ($publicWebDAVAPIVersion === "new") { + // In this case a single file has been shared as a public link. + // Even if that file is somewhere down inside a folder(s), when + // accessing it as a public link using the "new" public webDAV API + // the client needs to provide the public link share token followed + // by just the name of the file - not the full path. + $fullPath = $this->featureContext->getLastPublicSharePath(); + $fullPathParts = \explode("/", $fullPath); + $path = \end($fullPathParts); + } else { + $path = ""; + } + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + $password, + $range, + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public downloads the last public link shared file with range "([^"]*)" and password "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $range ignore if empty + * @param string $password + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function downloadPublicFileWithRangeAndPassword(string $range, string $password, string $publicWebDAVAPIVersion):void { + if ($publicWebDAVAPIVersion === "new") { + $path = $this->featureContext->getLastPublicShareData()->data->file_target; + } else { + $path = ""; + } + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + $password, + $range, + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public downloads the last public link shared file using the (old|new) public WebDAV API$/ + * + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function downloadPublicFile(string $publicWebDAVAPIVersion):void { + $this->downloadPublicFileWithRange("", $publicWebDAVAPIVersion); + } + + /** + * @When /^the public downloads the last public link shared file with password "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $password + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function downloadPublicFileWithPassword(string $password, string $publicWebDAVAPIVersion):void { + $this->downloadPublicFileWithRange("", $publicWebDAVAPIVersion, $password); + } + + /** + * @When /^the public deletes (?:file|folder|entry) "([^"]*)" from the last public link share using the (old|new) public WebDAV API$/ + * + * @param string $fileName + * @param string $publicWebDAVAPIVersion + * @param string $password + * + * @return void + */ + public function deleteFileFromPublicShare(string $fileName, string $publicWebDAVAPIVersion, string $password = ""):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } + $token = $this->featureContext->getLastPublicShareToken(); + $davPath = WebDavHelper::getDavPath( + $token, + 0, + "public-files-$publicWebDAVAPIVersion" + ); + $fullUrl = $this->featureContext->getBaseUrl() . "/$davPath$fileName"; + $userName = $this->getUsernameForPublicWebdavApi( + $token, + $password, + $publicWebDAVAPIVersion + ); + $headers = [ + 'X-Requested-With' => 'XMLHttpRequest' + ]; + $this->featureContext->setResponse( + HttpRequestHelper::delete( + $fullUrl, + $this->featureContext->getStepLineRef(), + $userName, + $password, + $headers + ) + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + + /** + * @When /^the public deletes file "([^"]*)" from the last public link share using the password "([^"]*)" and (old|new) public WebDAV API$/ + * + * @param string $file + * @param string $password + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicDeletesFileFromTheLastPublicShareUsingThePasswordPasswordAndOldPublicWebdavApi(string $file, string $password, string $publicWebDAVAPIVersion):void { + $this->deleteFileFromPublicShare($file, $publicWebDAVAPIVersion, $password); + } + + /** + * @When /^the public renames (?:file|folder|entry) "([^"]*)" to "([^"]*)" from the last public link share using the (old|new) public WebDAV API$/ + * + * @param string $fileName + * @param string $toFileName + * @param string $publicWebDAVAPIVersion + * @param string|null $password + * + * @return void + */ + public function renameFileFromPublicShare(string $fileName, string $toFileName, string $publicWebDAVAPIVersion, ?string $password = ""):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } + $token = $this->featureContext->getLastPublicShareToken(); + $davPath = WebDavHelper::getDavPath( + $token, + 0, + "public-files-$publicWebDAVAPIVersion" + ); + $fullUrl = $this->featureContext->getBaseUrl() . "/$davPath$fileName"; + $destination = $this->featureContext->getBaseUrl() . "/$davPath$toFileName"; + $userName = $this->getUsernameForPublicWebdavApi( + $token, + $password, + $publicWebDAVAPIVersion + ); + $headers = [ + 'X-Requested-With' => 'XMLHttpRequest', + 'Destination' => $destination + ]; + $this->featureContext->setResponse( + HttpRequestHelper::sendRequest( + $fullUrl, + $this->featureContext->getStepLineRef(), + "MOVE", + $userName, + $password, + $headers + ) + ); + } + + /** + * @When /^the public renames file "([^"]*)" to "([^"]*)" from the last public link share using the password "([^"]*)" and (old|new) public WebDAV API$/ + * + * @param string $fileName + * @param string $toName + * @param string $password + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicRenamesFileFromTheLastPublicShareUsingThePasswordPasswordAndOldPublicWebdavApi(string $fileName, string $toName, string $password, string $publicWebDAVAPIVersion):void { + $this->renameFileFromPublicShare($fileName, $toName, $publicWebDAVAPIVersion, $password); + } + + /** + * @When /^the public downloads file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function downloadPublicFileInsideAFolder(string $path, string $publicWebDAVAPIVersion = "old"):void { + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + "", + "", + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public downloads file "([^"]*)" from inside the last public link shared folder with password "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $path + * @param string|null $password + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publicDownloadsTheFileInsideThePublicSharedFolderWithPassword( + string $path, + ?string $password = "", + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + $password, + "", + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public downloads file "([^"]*)" from inside the last public link shared folder with range "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $path + * @param string $range + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function downloadPublicFileInsideAFolderWithRange(string $path, string $range, string $publicWebDAVAPIVersion):void { + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + "", + $range, + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public downloads file "([^"]*)" from inside the last public link shared folder with password "([^"]*)" with range "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $path + * @param string $password + * @param string $range ignored when empty + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + string $path, + string $password, + string $range, + string $publicWebDAVAPIVersion = "old" + ):void { + $path = \ltrim($path, "/"); + $password = $this->featureContext->getActualPassword($password); + $token = $this->featureContext->getLastPublicShareToken(); + $davPath = WebDavHelper::getDavPath( + $token, + 0, + "public-files-$publicWebDAVAPIVersion" + ); + $fullUrl = $this->featureContext->getBaseUrl() . "/$davPath$path"; + $userName = $this->getUsernameForPublicWebdavApi( + $token, + $password, + $publicWebDAVAPIVersion + ); + + $headers = [ + 'X-Requested-With' => 'XMLHttpRequest' + ]; + if ($range !== "") { + $headers['Range'] = $range; + } + $response = HttpRequestHelper::get( + $fullUrl, + $this->featureContext->getStepLineRef(), + $userName, + $password, + $headers + ); + $this->featureContext->setResponse($response); + } + + /** + * @param string $source target file name + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publiclyUploadingFile(string $source, string $publicWebDAVAPIVersion):void { + $this->publicUploadContent( + \basename($source), + '', + \file_get_contents($source), + false, + [], + $publicWebDAVAPIVersion + ); + } + + /** + * @When the public uploads file :filename using the :publicWebDAVAPIVersion WebDAV API + * + * @param string $source target file name + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicUploadsFileUsingTheWebDAVApi(string $source, string $publicWebDAVAPIVersion):void { + $this->publiclyUploadingFile( + $source, + $publicWebDAVAPIVersion + ); + } + + /** + * + * @param string $baseUrl URL of owncloud + * e.g. http://localhost:8080 + * should include the subfolder + * if owncloud runs in a subfolder + * e.g. http://localhost:8080/owncloud-core + * @param string $source + * @param string $destination + * + * @return void + */ + public function publiclyCopyingFile( + string $baseUrl, + string $source, + string $destination + ):void { + $fullSourceUrl = $baseUrl . $source; + $fullDestUrl = WebDavHelper::sanitizeUrl( + $baseUrl . $destination + ); + + $headers["Destination"] = $fullDestUrl; + $response = HttpRequestHelper::sendRequest( + $fullSourceUrl, + $this->featureContext->getStepLineRef(), + "COPY", + null, + null, + $headers, + null, + null, + null, + false, + 0, + null + ); + $this->featureContext->setResponse($response); + } + + /** + * @When /^the public copies (?:file|folder) "([^"]*)" to "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $source + * @param string $destination + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicCopiesFileUsingTheWebDAVApi(string $source, string $destination, string $publicWebDAVAPIVersion):void { + $token = $this->featureContext->getLastPublicShareToken(); + $davPath = WebDavHelper::getDavPath( + $token, + 0, + "public-files-$publicWebDAVAPIVersion" + ); + $baseUrl = $this->featureContext->getLocalBaseUrl() . '/' . $davPath; + + $this->publiclyCopyingFile( + $baseUrl, + $source, + $destination + ); + } + + /** + * @Given the public has uploaded file :filename + * + * @param string $source target file name + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicHasUploadedFileUsingTheWebDAVApi(string $source, string $publicWebDAVAPIVersion):void { + $this->publiclyUploadingFile( + $source, + $publicWebDAVAPIVersion + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * This only works with the old API, auto-rename is not supported in the new API + * auto renaming is handled on files drop folders implicitly + * + * @param string $filename target file name + * @param string $body content to upload + * + * @return void + */ + public function publiclyUploadingContentAutoRename(string $filename, string $body = 'test'):void { + $this->publicUploadContent($filename, '', $body, true); + } + + /** + * @When the public uploads file :filename with content :body with auto-rename mode using the old public WebDAV API + * + * @param string $filename target file name + * @param string $body content to upload + * + * @return void + */ + public function thePublicUploadsFileWithContentWithAutoRenameMode(string $filename, string $body = 'test'):void { + $this->publiclyUploadingContentAutoRename($filename, $body); + } + + /** + * @Given the public has uploaded file :filename with content :body with auto-rename mode + * + * @param string $filename target file name + * @param string $body content to upload + * + * @return void + */ + public function thePublicHasUploadedFileWithContentWithAutoRenameMode(string $filename, string $body = 'test'):void { + $this->publiclyUploadingContentAutoRename($filename, $body); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $filename target file name + * @param string $password + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publiclyUploadingContentWithPassword( + string $filename, + string $password = '', + string $body = 'test', + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publicUploadContent( + $filename, + $password, + $body, + false, + [], + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public uploads file "([^"]*)" with password "([^"]*)" and content "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $filename target file name + * @param string|null $password + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicUploadsFileWithPasswordAndContentUsingPublicWebDAVApi( + string $filename, + ?string $password = '', + string $body = 'test', + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publiclyUploadingContentWithPassword( + $filename, + $password, + $body, + $publicWebDAVAPIVersion + ); + } + + /** + * @Given the public has uploaded file :filename" with password :password and content :body + * + * @param string $filename target file name + * @param string|null $password + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicHasUploadedFileWithPasswordAndContentUsingPublicWebDAVApi( + string $filename, + ?string $password = '', + string $body = 'test', + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publiclyUploadingContentWithPassword( + $filename, + $password, + $body, + $publicWebDAVAPIVersion + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $filename target file name + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publiclyOverwritingContent(string $filename, string $body = 'test', string $publicWebDAVAPIVersion = 'old'):void { + $this->publicUploadContent($filename, '', $body, false, [], $publicWebDAVAPIVersion); + } + + /** + * @When the public overwrites file :filename with content :body using the :type WebDAV API + * + * @param string $filename target file name + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicOverwritesFileWithContentUsingWebDavApi(string $filename, string $body = 'test', string $publicWebDAVAPIVersion = 'old'):void { + $this->publiclyOverwritingContent( + $filename, + $body, + $publicWebDAVAPIVersion + ); + } + + /** + * @Given the public has overwritten file :filename with content :body + * + * @param string $filename target file name + * @param string $body content to upload + * + * @return void + */ + public function thePublicHasOverwrittenFileWithContentUsingOldWebDavApi(string $filename, string $body = 'test'):void { + $this->publiclyOverwritingContent( + $filename, + $body + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $filename target file name + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publiclyUploadingContent( + string $filename, + string $body = 'test', + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publicUploadContent( + $filename, + '', + $body, + false, + [], + $publicWebDAVAPIVersion + ); + } + + /** + * @When /^the public uploads file "([^"]*)" with content "([^"]*)" using the (old|new) public WebDAV API$/ + * + * @param string $filename target file name + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicUploadsFileWithContentUsingThePublicWebDavApi( + string $filename, + string $body = 'test', + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publiclyUploadingContent( + $filename, + $body, + $publicWebDAVAPIVersion + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + + /** + * @Given the public has uploaded file :filename with content :body + * + * @param string $filename target file name + * @param string $body content to upload + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function thePublicHasUploadedFileWithContentUsingThePublicWebDavApi( + string $filename, + string $body = 'test', + string $publicWebDAVAPIVersion = "old" + ):void { + $this->publiclyUploadingContent( + $filename, + $body, + $publicWebDAVAPIVersion + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public should be able to download the last publicly shared file using the (old|new) public WebDAV API without a password and the content should be "([^"]*)"$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $expectedContent + * + * @return void + * @throws Exception + */ + public function checkLastPublicSharedFileDownload( + string $publicWebDAVAPIVersion, + string $expectedContent + ):void { + $this->checkLastPublicSharedFileWithPasswordDownload( + $publicWebDAVAPIVersion, + "", + $expectedContent + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public should be able to download the last publicly shared file using the (old|new) public WebDAV API without a password and the content should be "([^"]*)" plus end-of-line$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $expectedContent + * + * @return void + * @throws Exception + */ + public function checkLastPublicSharedFileDownloadPlusEndOfLine( + string $publicWebDAVAPIVersion, + string $expectedContent + ):void { + $this->checkLastPublicSharedFileWithPasswordDownload( + $publicWebDAVAPIVersion, + "", + "$expectedContent\n" + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public should be able to download the last publicly shared file using the (old|new) public WebDAV API with password "([^"]*)" and the content should be "([^"]*)"$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $expectedContent + * + * @return void + * @throws Exception + */ + public function checkLastPublicSharedFileWithPasswordDownload( + string $publicWebDAVAPIVersion, + string $password, + string $expectedContent + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->downloadPublicFileWithRange( + "", + $publicWebDAVAPIVersion, + $password + ); + + $this->featureContext->checkDownloadedContentMatches( + $expectedContent, + "Checking the content of the last public shared file after downloading with the $publicWebDAVAPIVersion public WebDAV API" + ); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public download of the last publicly shared file using the (old|new) public WebDAV API with password "([^"]*)" should fail with HTTP status code "([^"]*)"$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $expectedHttpCode + * + * @return void + */ + public function theLastPublicSharedFileShouldNotBeAbleToBeDownloadedWithPassword( + string $publicWebDAVAPIVersion, + string $password, + string $expectedHttpCode + ):void { + $this->downloadPublicFileWithRange( + "", + $publicWebDAVAPIVersion, + $password + ); + $responseContent = $this->featureContext->getResponse()->getBody()->getContents(); + \libxml_use_internal_errors(true); + Assert::assertNotFalse( + \simplexml_load_string($responseContent), + "response body is not valid XML, maybe download did work\n" . + "response body: \n$responseContent\n" + ); + $this->featureContext->theHTTPStatusCodeShouldBe($expectedHttpCode); + } + + /** + * @Then /^the public download of the last publicly shared file using the (old|new) public WebDAV API without a password should fail with HTTP status code "([^"]*)"$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $expectedHttpCode + * + * @return void + */ + public function theLastPublicSharedFileShouldNotBeAbleToBeDownloadedWithoutAPassword( + string $publicWebDAVAPIVersion, + string $expectedHttpCode + ):void { + $this->theLastPublicSharedFileShouldNotBeAbleToBeDownloadedWithPassword( + $publicWebDAVAPIVersion, + "", + $expectedHttpCode + ); + } + + /** + * @Then /^the public should be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function shouldBeAbleToDownloadFileInsidePublicSharedFolder( + string $path, + string $publicWebDAVAPIVersion + ):void { + $this->shouldBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + "", + $path, + $publicWebDAVAPIVersion, + "" + ); + } + + /** + * @Then /^the public should not be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API without a password$/ + * @Then /^the public download of file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API should fail with HTTP status code "([^"]*)"$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $expectedHttpCode + * + * @return void + */ + public function shouldNotBeAbleToDownloadFileInsidePublicSharedFolder( + string $path, + string $publicWebDAVAPIVersion, + string $expectedHttpCode = "401" + ):void { + $this->shouldNotBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + "", + $path, + $publicWebDAVAPIVersion, + "", + $expectedHttpCode + ); + } + + /** + * @Then /^the public should be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)"$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $password + * + * @return void + */ + public function shouldBeAbleToDownloadFileInsidePublicSharedFolderWithPassword( + string $path, + string $publicWebDAVAPIVersion, + string $password + ):void { + $this->shouldBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + "", + $path, + $publicWebDAVAPIVersion, + $password + ); + } + + /** + * @Then /^the public should be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)" and the content should be "([^"]*)" plus end-of-line$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $content + * + * @return void + * @throws Exception + */ + public function shouldBeAbleToDownloadFileInsidePublicSharedFolderWithPasswordAndEOL( + string $path, + string $publicWebDAVAPIVersion, + string $password, + string $content + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPassword( + $path, + $password, + $publicWebDAVAPIVersion + ); + + $this->featureContext->downloadedContentShouldBePlusEndOfLine($content); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public should be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)" and the content should be "([^"]*)"$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $content + * + * @return void + * @throws Exception + */ + public function shouldBeAbleToDownloadFileInsidePublicSharedFolderWithPasswordAndContentShouldBe( + string $path, + string $publicWebDAVAPIVersion, + string $password, + string $content + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPassword( + $path, + $password, + $publicWebDAVAPIVersion + ); + + $this->featureContext->downloadedContentShouldBe($content); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public should be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API without password and the content should be "([^"]*)"$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $content + * + * @return void + * @throws Exception + */ + public function shouldBeAbleToDownloadFileInsidePublicSharedFolderWithoutPassword( + string $path, + string $publicWebDAVAPIVersion, + string $content + ):void { + $this->shouldBeAbleToDownloadFileInsidePublicSharedFolderWithPasswordAndContentShouldBe($path, $publicWebDAVAPIVersion, "", $content); + } + + /** + * @Then /^the public should not be able to download file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)"$/ + * @Then /^the public download of file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)" should fail with HTTP status code "([^"]*)"$/ + * + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $expectedHttpCode + * + * @return void + */ + public function shouldNotBeAbleToDownloadFileInsidePublicSharedFolderWithPassword( + string $path, + string $publicWebDAVAPIVersion, + string $password, + string $expectedHttpCode = "401" + ):void { + $this->shouldNotBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + "", + $path, + $publicWebDAVAPIVersion, + $password, + $expectedHttpCode + ); + } + + /** + * @Then /^the public should be able to download the range "([^"]*)" of file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)""$/ + * + * @param string $range + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $password + * + * @return void + * @throws Exception + */ + public function shouldBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + string $range, + string $path, + string $publicWebDAVAPIVersion, + string $password + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + $password, + $range, + $publicWebDAVAPIVersion + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public should not be able to download the range "([^"]*)" of file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)"$/ + * + * @param string $range + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $expectedHttpCode + * + * @return void + * @throws Exception + */ + public function shouldNotBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + string $range, + string $path, + string $publicWebDAVAPIVersion, + string $password, + string $expectedHttpCode = "401" + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicDownloadsTheFileInsideThePublicSharedFolderWithPasswordAndRange( + $path, + $password, + $range, + $publicWebDAVAPIVersion + ); + + $responseContent = $this->featureContext->getResponse()->getBody()->getContents(); + \libxml_use_internal_errors(true); + Assert::assertNotFalse( + \simplexml_load_string($responseContent), + "response body is not valid XML, maybe download did work\n" . + "response body: \n$responseContent\n" + ); + $this->featureContext->theHTTPStatusCodeShouldBe($expectedHttpCode); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + } + + /** + * @Then /^the public should be able to download the range "([^"]*)" of file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API and the content should be "([^"]*)"$/ + * + * @param string $range + * @param string $path + * @param string $publicWebDAVAPIVersion + * @param string $content + * + * @return void + * @throws Exception + */ + public function shouldBeAbleToDownloadRangeOfFileInsidePublicSharedFolder( + string $range, + string $path, + string $publicWebDAVAPIVersion, + string $content + ):void { + $this->shouldBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + $range, + $path, + $publicWebDAVAPIVersion, + "", + $content + ); + } + + /** + * @Then /^the public should not be able to download the range "([^"]*)" of file "([^"]*)" from inside the last public link shared folder using the (old|new) public WebDAV API without a password$/ + * + * @param string $range + * @param string $path + * @param string $publicWebDAVAPIVersion + * + * @return void + * @throws Exception + */ + public function shouldNotBeAbleToDownloadRangeOfFileInsidePublicSharedFolder( + string $range, + string $path, + string $publicWebDAVAPIVersion + ):void { + $this->shouldNotBeAbleToDownloadRangeOfFileInsidePublicSharedFolderWithPassword( + $range, + $path, + $publicWebDAVAPIVersion, + "" + ); + } + + /** + * @Then /^the public upload to the last publicly shared file using the (old|new) public WebDAV API should (?:fail|pass) with HTTP status code "([^"]*)"$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $expectedHttpCode + * + * @return void + * @throws Exception + */ + public function publiclyUploadingShouldToSharedFileShouldFail( + string $publicWebDAVAPIVersion, + string $expectedHttpCode + ):void { + $filename = ""; + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $filename = (string)$this->featureContext->getLastPublicShareData()->data[0]->file_target; + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicUploadContent( + $filename, + '', + 'test', + false, + [], + $publicWebDAVAPIVersion + ); + + $this->featureContext->theHTTPStatusCodeShouldBe($expectedHttpCode); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + } + + /** + * @Then /^uploading a file should not work using the (old|new) public WebDAV API$/ + * @Then /^the public upload to the last publicly shared folder using the (old|new) public WebDAV API should fail with HTTP status code "([^"]*)"$/ + * + * @param string $publicWebDAVAPIVersion + * @param string $expectedHttpCode + * + * @return void + * @throws Exception + */ + public function publiclyUploadingShouldNotWork( + string $publicWebDAVAPIVersion, + string $expectedHttpCode = null + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicUploadContent( + 'whateverfilefortesting.txt', + '', + 'test', + false, + [], + $publicWebDAVAPIVersion + ); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + + $response = $this->featureContext->getResponse(); + if ($expectedHttpCode === null) { + $expectedHttpCode = [507, 400, 401, 403, 404, 423]; + } + $this->featureContext->theHTTPStatusCodeShouldBe( + $expectedHttpCode, + "upload should have failed but passed with code " . + $response->getStatusCode() + ); + } + + /** + * @Then /^the public should be able to upload file "([^"]*)" into the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)"$/ + * + * @param string $filename + * @param string $publicWebDAVAPIVersion + * @param string $password + * + * @return void + */ + public function publiclyUploadingIntoFolderWithPasswordShouldWork( + string $filename, + string $publicWebDAVAPIVersion, + string $password + ):void { + $this->publicUploadContent( + $filename, + $password, + 'test', + false, + [], + $publicWebDAVAPIVersion + ); + + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public upload of file "([^"]*)" into the last public link shared folder using the (old|new) public WebDAV API with password "([^"]*)" should fail with HTTP status code "([^"]*)"$/ + * + * @param string $filename + * @param string $publicWebDAVAPIVersion + * @param string $password + * @param string $expectedHttpCode + * + * @return void + */ + public function publiclyUploadingIntoFolderWithPasswordShouldFail( + string $filename, + string $publicWebDAVAPIVersion, + string $password, + string $expectedHttpCode + ):void { + $this->publicUploadContent( + $filename, + $password, + 'test', + false, + [], + $publicWebDAVAPIVersion + ); + + $response = $this->featureContext->getResponse(); + $this->featureContext->theHTTPStatusCodeShouldBe( + $expectedHttpCode, + "upload of $filename into the last publicly shared folder should have failed with code " . + $expectedHttpCode . " but the code was " . $response->getStatusCode() + ); + } + + /** + * @Then /^uploading a file should work using the (old|new) public WebDAV API$/ + * + * @param string $publicWebDAVAPIVersion + * + * @return void + * @throws Exception + */ + public function publiclyUploadingShouldWork(string $publicWebDAVAPIVersion):void { + $path = "whateverfilefortesting-$publicWebDAVAPIVersion-publicWebDAVAPI.txt"; + $content = "test $publicWebDAVAPIVersion"; + + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + } else { + $techPreviewHadToBeEnabled = false; + } + + $this->publicUploadContent( + $path, + '', + $content, + false, + [], + $publicWebDAVAPIVersion + ); + $response = $this->featureContext->getResponse(); + Assert::assertTrue( + ($response->getStatusCode() == 201), + "upload should have passed but failed with code " . + $response->getStatusCode() + ); + $this->shouldBeAbleToDownloadFileInsidePublicSharedFolder( + $path, + $publicWebDAVAPIVersion + ); + $this->featureContext->checkDownloadedContentMatches($content); + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + } + + /** + * @Then /^uploading content to a public link shared file should (not|)\s?work using the (old|new) public WebDAV API$/ + * + * @param string $shouldOrNot (not|) + * @param string $publicWebDAVAPIVersion + * + * @return void + * @throws Exception + */ + public function publiclyUploadingToPublicLinkSharedFileShouldWork( + string $shouldOrNot, + string $publicWebDAVAPIVersion + ):void { + $content = "test $publicWebDAVAPIVersion"; + $should = ($shouldOrNot !== "not"); + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } elseif ($publicWebDAVAPIVersion === "new") { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + $path = $this->featureContext->getLastPublicSharePath(); + } else { + $techPreviewHadToBeEnabled = false; + $path = ""; + } + + $this->publicUploadContent( + $path, + '', + $content, + false, + [], + $publicWebDAVAPIVersion + ); + $response = $this->featureContext->getResponse(); + if ($should) { + Assert::assertTrue( + ($response->getStatusCode() == 204), + "upload should have passed but failed with code " . + $response->getStatusCode() + ); + + $this->downloadPublicFileWithRange( + "", + $publicWebDAVAPIVersion, + "" + ); + + $this->featureContext->checkDownloadedContentMatches( + $content, + "Checking the content of the last public shared file after downloading with the $publicWebDAVAPIVersion public WebDAV API" + ); + } else { + $expectedCode = 403; + Assert::assertTrue( + ($response->getStatusCode() == $expectedCode), + "upload should have failed with HTTP status $expectedCode but passed with code " . + $response->getStatusCode() + ); + } + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + } + + /** + * @When the public uploads file :fileName to the last public link shared folder with mtime :mtime using the :davVersion public WebDAV API + * + * @param String $fileName + * @param String $mtime + * @param String $davVersion + * + * @return void + * @throws Exception + */ + public function thePublicUploadsFileToLastSharedFolderWithMtimeUsingTheWebdavApi( + string $fileName, + string $mtime, + string $davVersion = "old" + ):void { + $mtime = new DateTime($mtime); + $mtime = $mtime->format('U'); + + $this->publicUploadContent( + $fileName, + '', + 'test', + false, + ["X-OC-Mtime" => $mtime], + $davVersion + ); + } + + /** + * @param string $destination + * @param string $password + * + * @return void + */ + public function publicCreatesFolderUsingPassword( + string $destination, + string $password + ):void { + $token = $this->featureContext->getLastPublicShareToken(); + $davPath = WebDavHelper::getDavPath( + $token, + 0, + "public-files-new" + ); + $url = $this->featureContext->getBaseUrl() . "/$davPath"; + $password = $this->featureContext->getActualPassword($password); + $userName = $this->getUsernameForPublicWebdavApi( + $token, + $password, + "new" + ); + $foldername = \implode( + '/', + \array_map('rawurlencode', \explode('/', $destination)) + ); + $url .= \ltrim($foldername, '/'); + + $this->featureContext->setResponse( + HttpRequestHelper::sendRequest( + $url, + $this->featureContext->getStepLineRef(), + 'MKCOL', + $userName, + $password + ) + ); + } + + /** + * @When the public creates folder :destination using the new public WebDAV API + * + * @param String $destination + * + * @return void + */ + public function publicCreatesFolder(string $destination):void { + $this->publicCreatesFolderUsingPassword($destination, ''); + } + + /** + * @Then /^the public should be able to create folder "([^"]*)" in the last public link shared folder using the new public WebDAV API with password "([^"]*)"$/ + * + * @param string $foldername + * @param string $password + * + * @return void + */ + public function publicShouldBeAbleToCreateFolderWithPassword( + string $foldername, + string $password + ):void { + $this->publicCreatesFolderUsingPassword($foldername, $password); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the public creation of folder "([^"]*)" in the last public link shared folder using the new public WebDAV API with password "([^"]*)" should fail with HTTP status code "([^"]*)"$/ + * + * @param string $foldername + * @param string $password + * @param string $expectedHttpCode + * + * @return void + */ + public function publicCreationOfFolderWithPasswordShouldFail( + string $foldername, + string $password, + string $expectedHttpCode + ):void { + $this->publicCreatesFolderUsingPassword($foldername, $password); + $this->featureContext->theHTTPStatusCodeShouldBe( + $expectedHttpCode, + "creation of $foldername in the last publicly shared folder should have failed with code " . + $expectedHttpCode + ); + } + + /** + * @Then the mtime of file :fileName in the last shared public link using the WebDAV API should be :mtime + * + * @param string $fileName + * @param string $mtime + * + * @return void + * @throws Exception + */ + public function theMtimeOfFileInTheLastSharedPublicLinkUsingTheWebdavApiShouldBe( + string $fileName, + string $mtime + ):void { + $token = $this->featureContext->getLastPublicShareToken(); + $baseUrl = $this->featureContext->getBaseUrl(); + if (\TestHelpers\OcisHelper::isTestingOnOcisOrReva()) { + $mtime = \explode(" ", $mtime); + \array_pop($mtime); + $mtime = \implode(" ", $mtime); + Assert::assertStringContainsString( + $mtime, + WebDavHelper::getMtimeOfFileinPublicLinkShare( + $baseUrl, + $fileName, + $token, + $this->featureContext->getStepLineRef() + ) + ); + } else { + Assert::assertEquals( + $mtime, + WebDavHelper::getMtimeOfFileinPublicLinkShare( + $baseUrl, + $fileName, + $token, + $this->featureContext->getStepLineRef() + ) + ); + } + } + + /** + * @Then the mtime of file :fileName in the last shared public link using the WebDAV API should not be :mtime + * + * @param string $fileName + * @param string $mtime + * + * @return void + * @throws Exception + */ + public function theMtimeOfFileInTheLastSharedPublicLinkUsingTheWebdavApiShouldNotBe( + string $fileName, + string $mtime + ):void { + $token = $this->featureContext->getLastPublicShareToken(); + $baseUrl = $this->featureContext->getBaseUrl(); + Assert::assertNotEquals( + $mtime, + WebDavHelper::getMtimeOfFileinPublicLinkShare( + $baseUrl, + $fileName, + $token, + $this->featureContext->getStepLineRef() + ) + ); + } + + /** + * Uploads a file through the public WebDAV API and sets the response in FeatureContext + * + * @param string $filename + * @param string|null $password + * @param string $body + * @param bool $autoRename + * @param array $additionalHeaders + * @param string $publicWebDAVAPIVersion + * + * @return void + */ + public function publicUploadContent( + string $filename, + ?string $password = '', + string $body = 'test', + bool $autoRename = false, + array $additionalHeaders = [], + string $publicWebDAVAPIVersion = "old" + ):void { + if (OcisHelper::isTestingOnOcisOrReva() && $publicWebDAVAPIVersion === "old") { + return; + } + $password = $this->featureContext->getActualPassword($password); + $token = $this->featureContext->getLastPublicShareToken(); + $davPath = WebDavHelper::getDavPath( + $token, + 0, + "public-files-$publicWebDAVAPIVersion" + ); + $url = $this->featureContext->getBaseUrl() . "/$davPath"; + $userName = $this->getUsernameForPublicWebdavApi( + $token, + $password, + $publicWebDAVAPIVersion + ); + + $filename = \implode( + '/', + \array_map('rawurlencode', \explode('/', $filename)) + ); + $url .= \ltrim($filename, '/'); + // Trim any "/" from the end. For example, if we are putting content to a + // single file that has been shared with a link, then the URL should end + // with the link token and no "/" at the end. + $url = \rtrim($url, "/"); + $headers = ['X-Requested-With' => 'XMLHttpRequest']; + + if ($autoRename) { + $headers['OC-Autorename'] = 1; + } + $headers = \array_merge($headers, $additionalHeaders); + $response = HttpRequestHelper::put( + $url, + $this->featureContext->getStepLineRef(), + $userName, + $password, + $headers, + $body + ); + $this->featureContext->setResponse($response); + } + + /** + * @param string $token + * @param string $password + * @param string $publicWebDAVAPIVersion + * + * @return string|null + */ + private function getUsernameForPublicWebdavApi( + string $token, + string $password, + string $publicWebDAVAPIVersion + ):?string { + if ($publicWebDAVAPIVersion === "old") { + $userName = $token; + } else { + if ($password !== '') { + $userName = 'public'; + } else { + $userName = null; + } + } + return $userName; + } + + /** + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function setUpScenario(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + $this->occContext = $environment->getContext('OccContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/SearchContext.php b/tests/acceptance/features/bootstrap/SearchContext.php new file mode 100644 index 000000000..173c91758 --- /dev/null +++ b/tests/acceptance/features/bootstrap/SearchContext.php @@ -0,0 +1,192 @@ + + * @copyright Copyright (c) 2018 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; +use TestHelpers\OcisHelper; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * context containing search related API steps + */ +class SearchContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * @When user :user searches for :pattern using the WebDAV API + * @When user :user searches for :pattern and limits the results to :limit items using the WebDAV API + * @When user :user searches for :pattern using the WebDAV API requesting these properties: + * @When user :user searches for :pattern and limits the results to :limit items using the WebDAV API requesting these properties: + * + * @param string $user + * @param string $pattern + * @param string|null $limit + * @param TableNode|null $properties + * + * @return void + */ + public function userSearchesUsingWebDavAPI( + string $user, + string $pattern, + ?string $limit = null, + TableNode $properties = null + ):void { + // Because indexing of newly uploaded files or directories with ocis is decoupled and occurs asynchronously, a short wait is necessary before searching files or folders. + if (OcisHelper::isTestingOnOcis()) { + sleep(4); + } + $user = $this->featureContext->getActualUsername($user); + $baseUrl = $this->featureContext->getBaseUrl(); + $password = $this->featureContext->getPasswordForUser($user); + $body + = "\n" . + " \n" . + " \n" . + " $pattern\n"; + if ($limit !== null) { + $body .= " $limit\n"; + } + + $body .= " \n"; + if ($properties !== null) { + $propertiesRows = $properties->getRows(); + $body .= " "; + foreach ($propertiesRows as $property) { + $body .= "<$property[0]/>"; + } + $body .= " "; + } + $body .= " "; + $response = WebDavHelper::makeDavRequest( + $baseUrl, + $user, + $password, + "REPORT", + "/", + null, + $this->featureContext->getStepLineRef(), + $body, + $this->featureContext->getDavPathVersion() + ); + $this->featureContext->setResponse($response); + } + + /** + * @Then file/folder :path in the search result of user :user should contain these properties: + * + * @param string $path + * @param string $user + * @param TableNode $properties + * + * @return void + * @throws Exception + */ + public function fileOrFolderInTheSearchResultShouldContainProperties( + string $path, + string $user, + TableNode $properties + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->featureContext->verifyTableNodeColumns($properties, ['name', 'value']); + $properties = $properties->getHash(); + $fileResult = $this->featureContext->findEntryFromPropfindResponse( + $path, + $user, + "REPORT", + ); + Assert::assertNotFalse( + $fileResult, + "could not find file/folder '$path'" + ); + $fileProperties = $fileResult['value'][1]['value'][0]['value']; + foreach ($properties as $property) { + $foundProperty = false; + $property['value'] = $this->featureContext->substituteInLineCodes( + $property['value'], + $user + ); + foreach ($fileProperties as $fileProperty) { + if ($fileProperty['name'] === $property['name']) { + Assert::assertMatchesRegularExpression( + "/" . $property['value'] . "/", + $fileProperty['value'] + ); + $foundProperty = true; + break; + } + } + Assert::assertTrue( + $foundProperty, + "could not find property '" . $property['name'] . "'" + ); + } + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } + + /** + * @Then the search result by tags for user :user should contain these entries: + * + * @param string|null $user + * @param TableNode $expectedEntries + * + * @return void + * @throws Exception + */ + public function theSearchResultByTagsForUserShouldContainTheseEntries( + ?string $user, + TableNode $expectedEntries + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->featureContext->verifyTableNodeColumnsCount($expectedEntries, 1); + $expectedEntries = $expectedEntries->getRows(); + $expectedEntriesArray = []; + $responseResourcesArray = $this->featureContext->findEntryFromReportResponse($user); + foreach ($expectedEntries as $item) { + \array_push($expectedEntriesArray, $item[0]); + } + Assert::assertEqualsCanonicalizing($expectedEntriesArray, $responseResourcesArray); + } +} diff --git a/tests/acceptance/features/bootstrap/Sharing.php b/tests/acceptance/features/bootstrap/Sharing.php new file mode 100644 index 000000000..918976782 --- /dev/null +++ b/tests/acceptance/features/bootstrap/Sharing.php @@ -0,0 +1,4391 @@ + + * @author Sergio Bertolin + * @author Phillip Davis + * @copyright Copyright (c) 2018, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\Assert; +use TestHelpers\OcsApiHelper; +use TestHelpers\OcisHelper; +use TestHelpers\SharingHelper; +use TestHelpers\HttpRequestHelper; +use TestHelpers\SetupHelper; +use TestHelpers\TranslationHelper; + +/** + * Sharing trait + */ +trait Sharing { + /** + * @var int + */ + private $sharingApiVersion = 1; + + /** + * Contains the API response to the last share that was created by each user + * using the Sharing API. Shares created on the webUI do not have an entry. + * + * @var SimpleXMLElement[] + */ + private $lastShareDataByUser = []; + + /** + * Contains the share id of the last share that was created by each user, + * either using the Sharing API or on the web UI. + * + * @var string[] + */ + private $lastShareIdByUser = []; + + /** + * @var string + */ + private $userWhoCreatedLastShare = null; + + /** + * @var string + */ + private $userWhoCreatedLastPublicShare = null; + + /** + * Contains the API response to the last public link share that was created + * by the test-runner using the Sharing API. + * Shares created on the webUI do not have an entry. + * + * @var SimpleXMLElement + */ + private $lastPublicShareData = null; + + /** + * Contains the share id of the last public link share that was created by + * the test-runner, either using the Sharing API or on the web UI. + * + * @var string + */ + private $lastPublicShareId = null; + + /** + * @var int + */ + private $savedShareId = null; + + /** + * @var int + */ + private $localLastShareTime = null; + + /** + * Defines the fields that can be provided in a share request. + * + * @var array + */ + private $shareFields = [ + 'path', 'name', 'publicUpload', 'password', 'expireDate', + 'expireDateAsString', 'permissions', 'shareWith', 'shareType' + ]; + + /** + * Defines the fields that are known and can be tested in a share response. + * Note that ownCloud10 also provides file_parent in responses. + * file_parent is not provided by OCIS/reva. + * There are no known clients that use file_parent. + * The acceptance tests do not test for file_parent. + * + * @var array fields that are possible in a share response + */ + private $shareResponseFields = [ + 'id', 'share_type', 'uid_owner', 'displayname_owner', 'stime', 'parent', + 'expiration', 'token', 'uid_file_owner', 'displayname_file_owner', 'path', + 'item_type', 'mimetype', 'storage_id', 'storage', 'item_source', + 'file_source', 'file_target', 'name', 'url', 'mail_send', + 'attributes', 'permissions', 'share_with', 'share_with_displayname', 'share_with_additional_info' + ]; + + /* + * Contains information about the public links that have been created with the webUI. + * Each entry in the array has a "name", "url" and "path". + */ + private $createdPublicLinks = []; + + /** + * @return array + */ + public function getCreatedPublicLinks():array { + return $this->createdPublicLinks; + } + + /** + * The end (last) entry will itself be an array with keys "name", "url" and "path" + * + * @return array + */ + public function getLastCreatedPublicLink():array { + return \end($this->createdPublicLinks); + } + + /** + * @return string + */ + public function getLastCreatedPublicLinkUrl():string { + $lastCreatedLink = $this->getLastCreatedPublicLink(); + return $lastCreatedLink["url"]; + } + + /** + * @return string + */ + public function getLastCreatedPublicLinkPath():string { + $lastCreatedLink = $this->getLastCreatedPublicLink(); + return $lastCreatedLink["path"]; + } + + /** + * @return string + */ + public function getLastCreatedPublicLinkToken():string { + $lastCreatedLinkUrl = $this->getLastCreatedPublicLinkUrl(); + // The token is the last part of the URL, delimited by "/" + $urlParts = \explode("/", $lastCreatedLinkUrl); + return \end($urlParts); + } + + /** + * @return SimpleXMLElement|null + */ + public function getLastPublicShareData():?SimpleXMLElement { + return $this->lastPublicShareData; + } + + /** + * @param SimpleXMLElement $responseXml + * + * @return void + */ + public function setLastPublicShareData(SimpleXMLElement $responseXml): void { + $this->lastPublicShareData = $responseXml; + } + + /** + * @return SimpleXMLElement + * @throws Exception + */ + public function getLastShareData():SimpleXMLElement { + return $this->getLastShareDataForUser($this->userWhoCreatedLastShare); + } + + /** + * @param string|null $user + * + * @return SimpleXMLElement + * @throws Exception + */ + public function getLastShareDataForUser(?string $user):SimpleXMLElement { + if ($user === null) { + throw new Exception( + __METHOD__ . " user not specified. Probably no user or group shares have been created yet in the test scenario." + ); + } + if (isset($this->lastShareDataByUser[$user])) { + return $this->lastShareDataByUser[$user]; + } else { + throw new Exception(__METHOD__ . " last share data for user '$user' was not found"); + } + } + + /** + * @return int|null + */ + public function getSavedShareId():?int { + return $this->savedShareId; + } + + /** + * @return void + */ + public function resetLastPublicShareData():void { + $this->lastPublicShareData = null; + $this->lastPublicShareId = null; + $this->userWhoCreatedLastPublicShare = null; + } + + /** + * @param string $user + * + * @return void + */ + public function resetLastShareInfoForUser(string $user):void { + if (isset($this->lastShareDataByUser[$user])) { + unset($this->lastShareDataByUser[$user]); + } + if (isset($this->lastShareIdByUser[$user])) { + unset($this->lastShareIdByUser[$user]); + } + } + + /** + * @return int + */ + public function getLocalLastShareTime():int { + return $this->localLastShareTime; + } + + /** + * @return int + */ + public function getServerLastShareTime():int { + return (int) $this->getLastShareData()->data->stime; + } + + /** + * @param string|null $postfix string to append to the end of the path + * + * @return string + */ + public function getSharesEndpointPath(?string $postfix = ''):string { + return "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares$postfix"; + } + + /** + * Split given permissions string each separated with "," into array of strings + * + * @param string $str + * + * @return string[] + */ + private function splitPermissionsString(string $str):array { + $str = \trim($str); + $permissions = \array_map('trim', \explode(',', $str)); + + /* We use 'all', 'uploadwriteonly' and 'change' in feature files + for readability. Parse into appropriate permissions and return them + without any duplications.*/ + if (\in_array('all', $permissions, true)) { + $permissions = \array_keys(SharingHelper::PERMISSION_TYPES); + } + if (\in_array('uploadwriteonly', $permissions, true)) { + // remove 'uploadwriteonly' from $permissions + $permissions = \array_diff($permissions, ['uploadwriteonly']); + $permissions = \array_merge($permissions, ['create']); + } + if (\in_array('change', $permissions, true)) { + // remove 'change' from $permissions + $permissions = \array_diff($permissions, ['change']); + $permissions = \array_merge( + $permissions, + ['create', 'delete', 'read', 'update'] + ); + } + + return \array_unique($permissions); + } + + /** + * + * @return int + * + * @throws Exception + */ + public function getServerShareTimeFromLastResponse():int { + $stime = $this->getResponseXml(null, __METHOD__)->xpath("//stime"); + if ((bool) $stime) { + return (int) $stime[0]; + } + throw new Exception("Last share time (i.e. 'stime') could not be found in the response."); + } + + /** + * @return void + */ + private function waitToCreateShare():void { + if (($this->localLastShareTime !== null) + && ((\microtime(true) - $this->localLastShareTime) < 1) + ) { + // prevent creating two shares with the same "stime" which is + // based on seconds, this affects share merging order and could + // affect expected test result order + \sleep(1); + } + } + + /** + * @param string $user + * @param TableNode|null $body + * TableNode $body should not have any heading and can have following rows | + * | path | The folder or file path to be shared | + * | name | A (human-readable) name for the share, | + * | | which can be up to 64 characters in length. | + * | publicUpload | Whether to allow public upload to a public | + * | | shared folder. Write true for allowing. | + * | password | The password to protect the public link share with. | + * | expireDate | An expire date for public link shares. | + * | | This argument takes a date string in any format | + * | | that can be passed to strtotime(), for example: | + * | | 'YYYY-MM-DD' or '+ x days'. It will be converted to | + * | | 'YYYY-MM-DD' format before sending | + * | expireDateAsString | An expire date string for public link shares. | + * | | Whatever string is provided will be sent as the | + * | | expire date. For example, use this to test sending | + * | | invalid date strings. | + * | permissions | The permissions to set on the share. | + * | | 1 = read; 2 = update; 4 = create; | + * | | 8 = delete; 16 = share; 31 = all | + * | | 15 = change | + * | | 4 = uploadwriteonly | + * | | (default: 31, for public shares: 1) | + * | | Pass either the (total) number, | + * | | or the keyword, | + * | | or an comma separated list of keywords | + * | shareWith | The user or group id with which the file should | + * | | be shared. | + * | shareType | The type of the share. This can be one of: | + * | | 0 = user, 1 = group, 3 = public_link, | + * | | 6 = federated (cloud share). | + * | | Pass either the number or the keyword. | + * + * @return void + * @throws Exception + */ + public function createShareWithSettings(string $user, ?TableNode $body):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeRows( + $body, + ['path'], + $this->shareFields + ); + $bodyRows = $body->getRowsHash(); + $bodyRows['name'] = \array_key_exists('name', $bodyRows) ? $bodyRows['name'] : null; + $bodyRows['shareWith'] = \array_key_exists('shareWith', $bodyRows) ? $bodyRows['shareWith'] : null; + $bodyRows['shareWith'] = $this->getActualUsername($bodyRows['shareWith']); + $bodyRows['publicUpload'] = \array_key_exists('publicUpload', $bodyRows) ? $bodyRows['publicUpload'] === 'true' : null; + $bodyRows['password'] = \array_key_exists('password', $bodyRows) ? $this->getActualPassword($bodyRows['password']) : null; + + if (\array_key_exists('permissions', $bodyRows)) { + if (\is_numeric($bodyRows['permissions'])) { + $bodyRows['permissions'] = (int) $bodyRows['permissions']; + } else { + $bodyRows['permissions'] = $this->splitPermissionsString($bodyRows['permissions']); + } + } else { + $bodyRows['permissions'] = null; + } + if (\array_key_exists('shareType', $bodyRows)) { + if (\is_numeric($bodyRows['shareType'])) { + $bodyRows['shareType'] = (int) $bodyRows['shareType']; + } + } else { + $bodyRows['shareType'] = null; + } + + Assert::assertFalse( + isset($bodyRows['expireDate'], $bodyRows['expireDateAsString']), + 'expireDate and expireDateAsString cannot be set at the same time.' + ); + $needToParse = \array_key_exists('expireDate', $bodyRows); + $expireDate = $bodyRows['expireDate'] ?? $bodyRows['expireDateAsString'] ?? null; + $bodyRows['expireDate'] = $needToParse ? \date('Y-m-d', \strtotime($expireDate)) : $expireDate; + $this->createShare( + $user, + $bodyRows['path'], + $bodyRows['shareType'], + $bodyRows['shareWith'], + $bodyRows['publicUpload'], + $bodyRows['password'], + $bodyRows['permissions'], + $bodyRows['name'], + $bodyRows['expireDate'] + ); + } + + /** + * @Given auto-accept shares has been disabled + * + * @return void + */ + public function autoAcceptSharesHasBeenDisabled():void { + if (OcisHelper::isTestingOnOcisOrReva()) { + // auto-accept shares is disabled by default on OCIS. + // so there is nothing to do, just return + return; + } + + $this->appConfigurationContext->serverParameterHasBeenSetTo( + "shareapi_auto_accept_share", + "core", + "no" + ); + } + + /** + * @When /^user "([^"]*)" creates a share using the sharing API with settings$/ + * + * @param string $user + * @param TableNode|null $body {@link createShareWithSettings} + * + * @return void + * @throws Exception + */ + public function userCreatesAShareWithSettings(string $user, ?TableNode $body):void { + $user = $this->getActualUsername($user); + $this->createShareWithSettings( + $user, + $body + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has created a share with settings$/ + * + * @param string $user + * @param TableNode|null $body {@link createShareWithSettings} + * + * @return void + * @throws Exception + */ + public function userHasCreatedAShareWithSettings(string $user, ?TableNode $body) { + $this->createShareWithSettings( + $user, + $body + ); + $this->theHTTPStatusCodeShouldBe( + 200, + "Failed HTTP status code for last share for user $user" . ", Reason: " . $this->getResponse()->getReasonPhrase() + ); + } + + /** + * @When /^the user creates a share using the sharing API with settings$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theUserCreatesAShareWithSettings(?TableNode $body):void { + $this->createShareWithSettings($this->currentUser, $body); + } + + /** + * @When /^user "([^"]*)" creates a public link share using the sharing API with settings$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userCreatesAPublicLinkShareWithSettings(string $user, ?TableNode $body):void { + $rows = $body->getRows(); + // A public link share is shareType 3 + $rows[] = ['shareType', 'public_link']; + $newBody = new TableNode($rows); + $this->createShareWithSettings($user, $newBody); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has created a public link share with settings$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userHasCreatedAPublicLinkShareWithSettings(string $user, ?TableNode $body):void { + $this->userCreatesAPublicLinkShareWithSettings($user, $body); + $this->ocsContext->theOCSStatusCodeShouldBe("100,200"); + $this->theHTTPStatusCodeShouldBe(200); + $this->clearStatusCodeArrays(); + } + + /** + * @When /^the user creates a public link share using the sharing API with settings$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theUserCreatesAPublicLinkShareWithSettings(?TableNode $body):void { + $this->userCreatesAPublicLinkShareWithSettings($this->currentUser, $body); + } + + /** + * @Given /^the user has created a share with settings$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theUserHasCreatedAShareWithSettings(?TableNode $body):void { + $this->createShareWithSettings($this->currentUser, $body); + $this->ocsContext->theOCSStatusCodeShouldBe("100,200"); + $this->theHTTPStatusCodeShouldBe(200); + } + + /** + * @Given /^the user has created a public link share with settings$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theUserHasCreatedAPublicLinkShareWithSettings(?TableNode $body):void { + $this->theUserCreatesAPublicLinkShareWithSettings($body); + $this->ocsContext->theOCSStatusCodeShouldBe("100,200"); + $this->theHTTPStatusCodeShouldBe(200); + } + + /** + * @param string $user + * @param string $path + * @param boolean $publicUpload + * @param string|null $sharePassword + * @param string|int|string[]|int[]|null $permissions + * @param string|null $linkName + * @param string|null $expireDate + * + * @return void + */ + public function createAPublicShare( + string $user, + string $path, + bool $publicUpload = false, + string $sharePassword = null, + $permissions = null, + ?string $linkName = null, + ?string $expireDate = null + ):void { + $user = $this->getActualUsername($user); + $this->createShare( + $user, + $path, + 'public_link', + null, // shareWith + $publicUpload, + $sharePassword, + $permissions, + $linkName, + $expireDate + ); + } + + /** + * @When /^user "([^"]*)" creates a public link share of (?:file|folder) "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userCreatesAPublicLinkShareOf(string $user, string $path):void { + $this->createAPublicShare($user, $path); + } + + /** + * @Given /^user "([^"]*)" has created a public link share of (?:file|folder) "([^"]*)"$/ + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userHasCreatedAPublicLinkShareOf(string $user, string $path):void { + $this->createAPublicShare($user, $path); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $path + * + * @return void + */ + public function createPublicLinkShareOfResourceAsCurrentUser(string $path):void { + $this->createAPublicShare($this->currentUser, $path); + } + + /** + * @When /^the user creates a public link share of (?:file|folder) "([^"]*)" using the sharing API$/ + * + * @param string $path + * + * @return void + */ + public function aPublicLinkShareOfIsCreated(string $path):void { + $this->createPublicLinkShareOfResourceAsCurrentUser($path); + } + + /** + * @Given /^the user has created a public link share of (?:file|folder) "([^"]*)"$/ + * + * @param string $path + * + * @return void + */ + public function aPublicLinkShareOfHasCreated(string $path):void { + $this->createPublicLinkShareOfResourceAsCurrentUser($path); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $user + * @param string $path + * @param string|int|string[]|int[]|null $permissions + * + * @return void + */ + public function createPublicLinkShareOfResourceWithPermission( + string $user, + string $path, + $permissions + ):void { + $this->createAPublicShare($user, $path, true, null, $permissions); + } + + /** + * @When /^user "([^"]*)" creates a public link share of (?:file|folder) "([^"]*)" using the sharing API with (read|update|create|delete|change|uploadwriteonly|share|all) permission(?:s|)$/ + * + * @param string $user + * @param string $path + * @param string|int|string[]|int[]|null $permissions + * + * @return void + */ + public function userCreatesAPublicLinkShareOfWithPermission( + string $user, + string $path, + $permissions + ):void { + $this->createPublicLinkShareOfResourceWithPermission( + $user, + $path, + $permissions + ); + } + + /** + * @Given /^user "([^"]*)" has created a public link share of (?:file|folder) "([^"]*)" with (read|update|create|delete|change|uploadwriteonly|share|all) permission(?:s|)$/ + * + * @param string $user + * @param string $path + * @param string|int|string[]|int[]|null $permissions + * + * @return void + */ + public function userHasCreatedAPublicLinkShareOfWithPermission( + string $user, + string $path, + $permissions + ):void { + $this->createPublicLinkShareOfResourceWithPermission( + $user, + $path, + $permissions + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $path + * @param string|int|string[]|int[]|null $permissions + * + * @return void + */ + public function createPublicLinkShareWithPermissionByCurrentUser(string $path, $permissions):void { + $this->createAPublicShare( + $this->currentUser, + $path, + true, + null, + $permissions + ); + } + + /** + * @When /^the user creates a public link share of (?:file|folder) "([^"]*)" using the sharing API with (read|update|create|delete|change|uploadwriteonly|share|all) permission(?:s|)$/ + * + * @param string $path + * @param string|int|string[]|int[]|null $permissions + * + * @return void + */ + public function aPublicLinkShareOfIsCreatedWithPermission(string $path, $permissions):void { + $this->createPublicLinkShareWithPermissionByCurrentUser($path, $permissions); + } + + /** + * @Given /^the user has created a public link share of (?:file|folder) "([^"]*)" with (read|update|create|delete|change|uploadwriteonly|share|all) permission(?:s|)$/ + * + * @param string $path + * @param string|int|string[]|int[]|null $permissions + * + * @return void + */ + public function aPublicLinkShareOfHasCreatedWithPermission(string $path, $permissions):void { + $this->createPublicLinkShareWithPermissionByCurrentUser($path, $permissions); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $user + * @param string $path + * @param string $expiryDate in a valid date format, e.g. "+30 days" + * + * @return void + */ + public function createPublicLinkShareOfResourceWithExpiry( + string $user, + string $path, + string $expiryDate + ):void { + $this->createAPublicShare( + $user, + $path, + true, + null, + null, + null, + $expiryDate + ); + } + + /** + * @When /^user "([^"]*)" creates a public link share of (?:file|folder) "([^"]*)" using the sharing API with expiry "([^"]*)"$/ + * + * @param string $user + * @param string $path + * @param string $expiryDate in a valid date format, e.g. "+30 days" + * + * @return void + */ + public function userCreatesAPublicLinkShareOfWithExpiry( + string $user, + string $path, + string $expiryDate + ):void { + $this->createPublicLinkShareOfResourceWithExpiry( + $user, + $path, + $expiryDate + ); + } + + /** + * @Given /^user "([^"]*)" has created a public link share of (?:file|folder) "([^"]*)" with expiry "([^"]*)"$/ + * + * @param string $user + * @param string $path + * @param string $expiryDate in a valid date format, e.g. "+30 days" + * + * @return void + */ + public function userHasCreatedAPublicLinkShareOfWithExpiry( + string $user, + string $path, + string $expiryDate + ):void { + $this->createPublicLinkShareOfResourceWithExpiry( + $user, + $path, + $expiryDate + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $path + * @param string $expiryDate in a valid date format, e.g. "+30 days" + * + * @return void + */ + public function createPublicLinkShareOfResourceWithExpiryByCurrentUser( + string $path, + string $expiryDate + ):void { + $this->createAPublicShare( + $this->currentUser, + $path, + true, + null, + null, + null, + $expiryDate + ); + } + + /** + * @When /^the user creates a public link share of (?:file|folder) "([^"]*)" using the sharing API with expiry "([^"]*)$"/ + * + * @param string $path + * @param string $expiryDate in a valid date format, e.g. "+30 days" + * + * @return void + */ + public function aPublicLinkShareOfIsCreatedWithExpiry( + string $path, + string $expiryDate + ):void { + $this->createPublicLinkShareOfResourceWithExpiryByCurrentUser( + $path, + $expiryDate + ); + } + + /** + * @Given /^the user has created a public link share of (?:file|folder) "([^"]*)" with expiry "([^"]*)$/ + * + * @param string $path + * @param string $expiryDate in a valid date format, e.g. "+30 days" + * + * @return void + */ + public function aPublicLinkShareOfHasCreatedWithExpiry( + string $path, + string $expiryDate + ):void { + $this->createPublicLinkShareOfResourceWithExpiryByCurrentUser( + $path, + $expiryDate + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * Give the mimetype of the last shared file + * + * @return string + */ + public function getMimeTypeOfLastSharedFile():string { + return \json_decode(\json_encode($this->getLastShareData()->data->mimetype), true)[0]; + } + + /** + * @param string $url + * @param string|null $user + * @param string|null $password + * @param string|null $mimeType + * + * @return void + */ + private function checkDownload( + string $url, + ?string $user = null, + ?string $password = null, + ?string $mimeType = null + ) { + $password = $this->getActualPassword($password); + $headers = ['X-Requested-With' => 'XMLHttpRequest']; + $this->response = HttpRequestHelper::get( + $url, + $this->getStepLineRef(), + $user, + $password, + $headers + ); + Assert::assertEquals( + 200, + $this->response->getStatusCode(), + __METHOD__ + . " Expected status code is '200' but got '" + . $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), + __METHOD__ + . " Expected mimeType '$mimeType' but got '" + . $finfo->buffer($buf, FILEINFO_MIME_TYPE) + ); + } + } + + /** + * @Then /^user "([^"]*)" should not be able to create a public link share of (file|folder) "([^"]*)" using the sharing API$/ + * + * @param string $sharer + * @param string $entry + * @param string $filepath + * + * @return void + * @throws Exception + */ + public function shouldNotBeAbleToCreatePublicLinkShare(string $sharer, string $entry, string $filepath):void { + $this->asFileOrFolderShouldExist( + $this->getActualUsername($sharer), + $entry, + $filepath + ); + $this->createAPublicShare($sharer, $filepath); + Assert::assertEquals( + 404, + $this->ocsContext->getOCSResponseStatusCode($this->response), + __METHOD__ + . " Expected response status code is '404' but got '" + . $this->ocsContext->getOCSResponseStatusCode($this->response) + . "'" + ); + } + + /** + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function updateLastShareByCurrentUser(?TableNode $body):void { + $this->updateLastShareWithSettings($this->currentUser, $body); + } + + /** + * @When /^the user updates the last share using the sharing API with$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theUserUpdatesTheLastShareWith(?TableNode $body):void { + $this->updateLastShareByCurrentUser($body); + } + + /** + * @Given /^the user has updated the last share with$/ + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theUserHasUpdatedTheLastShareWith(?TableNode $body):void { + $this->updateLastShareByCurrentUser($body); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $user + * @param TableNode|null $body + * @param string|null $shareOwner + * @param bool $updateLastPublicLink + * + * @return void + * @throws Exception + */ + public function updateLastShareWithSettings( + string $user, + ?TableNode $body, + ?string $shareOwner = null, + ?bool $updateLastPublicLink = false + ):void { + $user = $this->getActualUsername($user); + + if ($updateLastPublicLink) { + $share_id = $this->getLastPublicLinkShareId(); + } else { + if ($shareOwner === null) { + $share_id = $this->getLastShareId(); + } else { + $share_id = $this->getLastShareIdForUser($shareOwner); + } + } + + $this->verifyTableNodeRows( + $body, + [], + $this->shareFields + ); + $bodyRows = $body->getRowsHash(); + if (\array_key_exists('expireDate', $bodyRows)) { + $dateModification = $bodyRows['expireDate']; + if (!empty($bodyRows['expireDate'])) { + $bodyRows['expireDate'] = \date('Y-m-d', \strtotime($dateModification)); + } else { + $bodyRows['expireDate'] = ''; + } + } + if (\array_key_exists('password', $bodyRows)) { + $bodyRows['password'] = $this->getActualPassword($bodyRows['password']); + } + if (\array_key_exists('permissions', $bodyRows)) { + if (\is_numeric($bodyRows['permissions'])) { + $bodyRows['permissions'] = (int) $bodyRows['permissions']; + } else { + $bodyRows['permissions'] = $this->splitPermissionsString($bodyRows['permissions']); + $bodyRows['permissions'] = SharingHelper::getPermissionSum($bodyRows['permissions']); + } + } + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "PUT", + $this->getSharesEndpointPath("/$share_id"), + $this->getStepLineRef(), + $bodyRows, + $this->ocsApiVersion + ); + } + + /** + * @When /^user "([^"]*)" updates the last share using the sharing API with$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userUpdatesTheLastShareWith(string $user, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" updates the last public link share using the sharing API with$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userUpdatesTheLastPublicLinkShareWith(string $user, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body, null, true); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has updated the last share with$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userHasUpdatedTheLastShareWith(string $user, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Given /^user "([^"]*)" has updated the last public link share with$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userHasUpdatedTheLastPublicLinkShareWith(string $user, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body, null, true); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Given /^user "([^"]*)" has updated the last share of "([^"]*)" with$/ + * + * @param string $user + * @param string $shareOwner + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userHasUpdatedTheLastShareOfWith(string $user, string $shareOwner, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body, $shareOwner); + $this->theHTTPStatusCodeShouldBeSuccess(); + if ($this->ocsApiVersion == 1) { + $this->ocsContext->theOCSStatusCodeShouldBe("100"); + } elseif ($this->ocsApiVersion === 2) { + $this->ocsContext->theOCSStatusCodeShouldBe("200"); + } else { + throw new Exception('Invalid ocs api version used'); + } + } + + /** + * @param string $name + * @param string $url + * @param string $path + * + * @return void + */ + public function addToListOfCreatedPublicLinks(string $name, string $url, string $path = ""):void { + $this->createdPublicLinks[] = ["name" => $name, "url" => $url, "path" => $path]; + } + + /** + * @param string $user + * @param string|null $path + * @param string|null $shareType + * @param string|null $shareWith + * @param bool|null $publicUpload + * @param string|null $sharePassword + * @param string|int|string[]|int[]|null $permissions + * @param string|null $linkName + * @param string|null $expireDate + * @param string $sharingApp + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function createShare( + string $user, + ?string $path = null, + ?string $shareType = null, + ?string $shareWith = null, + ?bool $publicUpload = null, + ?string $sharePassword = null, + $permissions = null, + ?string $linkName = null, + ?string $expireDate = null, + string $sharingApp = 'files_sharing' + ):void { + $userActual = $this->getActualUsername($user); + if (\is_string($permissions) && !\is_numeric($permissions)) { + $permissions = $this->splitPermissionsString($permissions); + } + $this->waitToCreateShare(); + $this->response = SharingHelper::createShare( + $this->getBaseUrl(), + $userActual, + $this->getPasswordForUser($user), + $path, + $shareType, + $this->getStepLineRef(), + $shareWith, + $publicUpload, + $sharePassword, + $permissions, + $linkName, + $expireDate, + $this->ocsApiVersion, + $this->sharingApiVersion, + $sharingApp + ); + $httpStatusCode = $this->response->getStatusCode(); + // In case of HTTP status code 204 "no content", or a failure code like 4xx + // in the HTTP or OCS status there is no useful content in response payload body. + // Clear the test-runner's memory of "last share data" to avoid later steps + // accidentally using some previous share data. + if (($httpStatusCode === 204) + || !$this->theHTTPStatusCodeWasSuccess() + || (($httpStatusCode === 200) && ($this->ocsContext->getOCSResponseStatusCode($this->response) > 299)) + ) { + if ($shareType === 'public_link') { + $this->resetLastPublicShareData(); + } else { + $this->resetLastShareInfoForUser($user); + } + } else { + if ($shareType === 'public_link') { + $this->setLastPublicShareData($this->getResponseXml(null, __METHOD__)); + $this->setLastPublicLinkShareId((string) $this->lastPublicShareData->data[0]->id); + $this->setUserWhoCreatedLastPublicShare($user); + if (isset($this->lastPublicShareData->data)) { + $linkName = (string) $this->lastPublicShareData->data[0]->name; + $linkUrl = (string) $this->lastPublicShareData->data[0]->url; + $this->addToListOfCreatedPublicLinks($linkName, $linkUrl, $path); + } + } else { + $shareData = $this->getResponseXml(null, __METHOD__); + $this->lastShareDataByUser[$user] = $shareData; + $shareId = (string) $shareData->data[0]->id; + $this->setLastShareIdOf($user, $shareId); + } + } + $this->localLastShareTime = \microtime(true); + } + + /** + * @param string $field + * @param string $value + * @param string $contentExpected + * @param bool $expectSuccess if true then the caller expects that the field + * has the expected content + * emit debugging information if the field is not as expected + * + * @return bool + */ + public function doesFieldValueMatchExpectedContent( + string $field, + string $value, + string $contentExpected, + bool $expectSuccess = true + ):bool { + if (($contentExpected === "ANY_VALUE") + || (($contentExpected === "A_TOKEN") && (\strlen($value) === 15)) + || (($contentExpected === "A_NUMBER") && \is_numeric($value)) + || (($contentExpected === "A_STRING") && \is_string($value) && $value !== "") + || (($contentExpected === "AN_URL") && $this->isAPublicLinkUrl($value)) + || (($field === 'remote') && (\rtrim($value, "/") === $contentExpected)) + || ($contentExpected === $value) + ) { + if (!$expectSuccess) { + echo $field . " is unexpectedly set with value '" . $value . "'\n"; + } + return true; + } + return false; + } + + /** + * @param string $field + * @param string|null $contentExpected + * @param bool $expectSuccess if true then the caller expects that the field + * is in the response with the expected content + * so emit debugging information if the field is not correct + * @param SimpleXMLElement|null $data + * + * @return bool + * @throws Exception + */ + public function isFieldInResponse(string $field, ?string $contentExpected, bool $expectSuccess = true, ?SimpleXMLElement $data = null):bool { + if ($data === null) { + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + } + Assert::assertIsObject($data, __METHOD__ . " data not found in response XML"); + + $dateFieldsArrayToConvert = ['expiration', 'original_date', 'new_date']; + //do not try to convert empty date + if ((string) \in_array($field, \array_merge($dateFieldsArrayToConvert)) && !empty($contentExpected)) { + $timestamp = \strtotime($contentExpected, $this->getServerShareTimeFromLastResponse()); + // strtotime returns false if it failed to parse, just leave it as it is in that condition + if ($timestamp !== false) { + $contentExpected + = \date( + 'Y-m-d', + $timestamp + ) . " 00:00:00"; + } + } + $contentExpected = (string) $contentExpected; + + if (\count($data->element) > 0) { + $fieldIsSet = false; + $value = ""; + foreach ($data as $element) { + if (isset($element->$field)) { + $fieldIsSet = true; + $value = (string) $element->$field; + if ($this->doesFieldValueMatchExpectedContent( + $field, + $value, + $contentExpected, + $expectSuccess + ) + ) { + return true; + } + } + } + } else { + $fieldIsSet = isset($data->$field); + if ($fieldIsSet) { + $value = (string) $data->$field; + if ($this->doesFieldValueMatchExpectedContent( + $field, + $value, + $contentExpected, + $expectSuccess + ) + ) { + return true; + } + } + } + if ($expectSuccess) { + if ($fieldIsSet) { + echo $field . " has unexpected value '" . $value . "'\n"; + } else { + echo $field . " is not set in response\n"; + } + } + return false; + } + + /** + * @Then no files or folders should be included in the response + * + * @return void + */ + public function checkNoFilesFoldersInResponse():void { + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + Assert::assertIsObject($data, __METHOD__ . " data not found in response XML"); + Assert::assertCount(0, $data); + } + + /** + * @Then exactly :count file/files or folder/folders should be included in the response + * + * @param string $count + * + * @return void + */ + public function checkCountFilesFoldersInResponse(string $count):void { + $count = (int) $count; + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + Assert::assertIsObject($data, __METHOD__ . " data not found in response XML"); + Assert::assertCount($count, $data, __METHOD__ . " the response does not contain $count entries"); + } + + /** + * @Then /^(?:file|folder|entry) "([^"]*)" should be included in the response$/ + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function checkSharedFileInResponse(string $filename):void { + $filename = "/" . \ltrim($filename, '/'); + Assert::assertTrue( + $this->isFieldInResponse('file_target', "$filename"), + "'file_target' value '$filename' was not found in response" + ); + } + + /** + * @Then /^(?:file|folder|entry) "([^"]*)" should not be included in the response$/ + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function checkSharedFileNotInResponse(string $filename):void { + $filename = "/" . \ltrim($filename, '/'); + Assert::assertFalse( + $this->isFieldInResponse('file_target', "$filename", false), + "'file_target' value '$filename' was unexpectedly found in response" + ); + } + + /** + * @Then /^(?:file|folder|entry) "([^"]*)" should be included as path in the response$/ + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function checkSharedFileAsPathInResponse(string $filename):void { + $filename = "/" . \ltrim($filename, '/'); + Assert::assertTrue( + $this->isFieldInResponse('path', "$filename"), + "'path' value '$filename' was not found in response" + ); + } + + /** + * @Then /^(?:file|folder|entry) "([^"]*)" should not be included as path in the response$/ + * + * @param string $filename + * + * @return void + * @throws Exception + */ + public function checkSharedFileAsPathNotInResponse(string $filename):void { + $filename = "/" . \ltrim($filename, '/'); + Assert::assertFalse( + $this->isFieldInResponse('path', "$filename", false), + "'path' value '$filename' was unexpectedly found in response" + ); + } + + /** + * @Then /^(user|group) "([^"]*)" should be included in the response$/ + * + * @param string $type + * @param string $user + * + * @return void + * @throws Exception + */ + public function checkSharedUserOrGroupInResponse(string $type, string $user):void { + if ($type === 'user') { + $user = $this->getActualUsername($user); + } + Assert::assertTrue( + $this->isFieldInResponse('share_with', "$user"), + "'share_with' value '$user' was not found in response" + ); + } + + /** + * @Then /^user "([^"]*)" should not be included in the response$/ + * @Then /^group "([^"]*)" should not be included in the response$/ + * + * @param string $userOrGroup + * + * @return void + * @throws Exception + */ + public function checkSharedUserOrGroupNotInResponse(string $userOrGroup):void { + Assert::assertFalse( + $this->isFieldInResponse('share_with', "$userOrGroup", false), + "'share_with' value '$userOrGroup' was unexpectedly found in response" + ); + } + + /** + * @param string $userOrGroupId + * @param string|int $shareType 0 or "user" for user, 1 or "group" for group + * @param string|int|string[]|int[]|null $permissions + * + * @return bool + */ + public function isUserOrGroupInSharedData(string $userOrGroupId, $shareType, $permissions = null):bool { + $shareType = SharingHelper::getShareType($shareType); + + if ($permissions !== null) { + if (\is_string($permissions) && !\is_numeric($permissions)) { + $permissions = $this->splitPermissionsString($permissions); + } + $permissionSum = SharingHelper::getPermissionSum($permissions); + } + + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + if (\is_iterable($data)) { + foreach ($data as $element) { + if (($element->share_type->__toString() === (string) $shareType) + && ($element->share_with->__toString() === $userOrGroupId) + && ($permissions === null || $permissionSum === (int) $element->permissions->__toString()) + ) { + return true; + } + } + return false; + } + \error_log( + "INFORMATION: isUserOrGroupInSharedData response XML data is " . + \gettype($data) . + " and therefore does not contain share_with information." + ); + return false; + } + + /** + * + * @param string $user1 + * @param string $filepath + * @param string $user2 + * @param string|int|string[]|int[] $permissions + * @param bool|null $getShareData If true then only create the share if it is not + * already existing, and at the end request the + * share information and leave that in $this->response + * Typically used in a "Given" step which verifies + * that the share did get created successfully. + * + * @return void + */ + public function shareFileWithUserUsingTheSharingApi( + string $user1, + string $filepath, + string $user2, + $permissions = null, + ?bool $getShareData = false + ):void { + $user1Actual = $this->getActualUsername($user1); + $user2Actual = $this->getActualUsername($user2); + + $path = $this->getSharesEndpointPath("?path=" . \urlencode($filepath)); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user1Actual, + $this->getPasswordForUser($user1), + "GET", + $path, + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + if ($getShareData && $this->isUserOrGroupInSharedData($user2Actual, "user", $permissions)) { + return; + } else { + $this->createShare( + $user1, + $filepath, + '0', + $user2Actual, + null, + null, + $permissions + ); + } + if ($getShareData) { + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user1Actual, + $this->getPasswordForUser($user1), + "GET", + $path, + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + } + + /** + * @When /^user "([^"]*)" shares (?:file|folder|entry) "([^"]*)" with user "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @When /^user "([^"]*)" shares (?:file|folder|entry) "([^"]*)" with user "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $user1 + * @param string $filepath + * @param string $user2 + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function userSharesFileWithUserUsingTheSharingApi( + string $user1, + string $filepath, + string $user2, + $permissions = null + ):void { + $this->shareFileWithUserUsingTheSharingApi( + $user1, + $filepath, + $user2, + $permissions + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" shares the following (?:files|folders|entries) with user "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @When /^user "([^"]*)" shares the following (?:files|folders|entries) with user "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $sharer + * @param string $sharee + * @param TableNode $table + * @param string|int|string[]|int[] $permissions + * + * @return void + * @throws Exception + */ + public function userSharesTheFollowingFilesWithUserUsingTheSharingApi( + string $sharer, + string $sharee, + TableNode $table, + $permissions = null + ):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $filepath) { + $this->userSharesFileWithUserUsingTheSharingApi( + $sharer, + $filepath["path"], + $sharee, + $permissions + ); + } + } + + /** + * @Given /^user "([^"]*)" has shared (?:file|folder|entry) "([^"]*)" with user "([^"]*)"(?: with permissions (\d+))?$/ + * @Given /^user "([^"]*)" has shared (?:file|folder|entry) "([^"]*)" with user "([^"]*)" with permissions "([^"]*)"$/ + * + * @param string $user1 + * @param string $filepath + * @param string $user2 + * @param string|int|string[]|int[] $permissions + * + * @return void + * @throws Exception + */ + public function userHasSharedFileWithUserUsingTheSharingApi( + string $user1, + string $filepath, + string $user2, + $permissions = null + ):void { + $user1 = $this->getActualUsername($user1); + $user2 = $this->getActualUsername($user2); + $this->shareFileWithUserUsingTheSharingApi( + $user1, + $filepath, + $user2, + $permissions, + true + ); + $this->ocsContext->assertOCSResponseIndicatesSuccess( + 'The ocs share response does not indicate success.', + ); + // this is expected to fail if a file is shared with create and delete permissions, which is not possible + Assert::assertTrue( + $this->isUserOrGroupInSharedData($user2, "user", $permissions), + __METHOD__ . " User $user1 failed to share $filepath with user $user2" + ); + } + + /** + * @Given /^user "([^"]*)" has shared (?:file|folder|entry) "([^"]*)" with the administrator(?: with permissions (\d+))?$/ + * @Given /^user "([^"]*)" has shared (?:file|folder|entry) "([^"]*)" with the administrator with permissions "([^"]*)"$/ + * + * @param string $sharer + * @param string $filepath + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function userHasSharedFileWithTheAdministrator( + string $sharer, + string $filepath, + $permissions = null + ):void { + $admin = $this->getAdminUsername(); + $this->userHasSharedFileWithUserUsingTheSharingApi( + $sharer, + $filepath, + $admin, + $permissions + ); + } + + /** + * @When /^the user shares (?:file|folder|entry) "([^"]*)" with user "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @When /^the user shares (?:file|folder|entry) "([^"]*)" with user "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $filepath + * @param string $user2 + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function theUserSharesFileWithUserUsingTheSharingApi( + string $filepath, + string $user2, + $permissions = null + ) { + $this->userSharesFileWithUserUsingTheSharingApi( + $this->getCurrentUser(), + $filepath, + $user2, + $permissions + ); + } + + /** + * @Given /^the user has shared (?:file|folder|entry) "([^"]*)" with user "([^"]*)"(?: with permissions (\d+))?$/ + * @Given /^the user has shared (?:file|folder|entry) "([^"]*)" with user "([^"]*)" with permissions "([^"]*)"$/ + * + * @param string $filepath + * @param string $user2 + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function theUserHasSharedFileWithUserUsingTheSharingApi( + string $filepath, + string $user2, + $permissions = null + ):void { + $user2 = $this->getActualUsername($user2); + $this->userHasSharedFileWithUserUsingTheSharingApi( + $this->getCurrentUser(), + $filepath, + $user2, + $permissions + ); + } + + /** + * @When /^the user shares (?:file|folder|entry) "([^"]*)" with group "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @When /^the user shares (?:file|folder|entry) "([^"]*)" with group "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $filepath + * @param string $group + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function theUserSharesFileWithGroupUsingTheSharingApi( + string $filepath, + string $group, + $permissions = null + ):void { + $this->userSharesFileWithGroupUsingTheSharingApi( + $this->currentUser, + $filepath, + $group, + $permissions + ); + } + + /** + * @Given /^the user has shared (?:file|folder|entry) "([^"]*)" with group "([^"]*)"(?: with permissions (\d+))?$/ + * @Given /^the user has shared (?:file|folder|entry) "([^"]*)" with group "([^"]*)" with permissions "([^"]*)"$/ + * + * @param string $filepath + * @param string $group + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function theUserHasSharedFileWithGroupUsingTheSharingApi( + string $filepath, + string $group, + $permissions = null + ):void { + $this->userHasSharedFileWithGroupUsingTheSharingApi( + $this->currentUser, + $filepath, + $group, + $permissions + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * + * @param string $user + * @param string $filepath + * @param string $group + * @param string|int|string[]|int[] $permissions + * @param bool $getShareData If true then only create the share if it is not + * already existing, and at the end request the + * share information and leave that in $this->response + * Typically used in a "Given" step which verifies + * that the share did get created successfully. + * + * @return void + */ + public function shareFileWithGroupUsingTheSharingApi( + string $user, + string $filepath, + string$group, + $permissions = null, + bool $getShareData = false + ):void { + $userActual = $this->getActualUsername($user); + $path = $this->getSharesEndpointPath("?path=$filepath"); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $userActual, + $this->getPasswordForUser($user), + "GET", + $path, + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + if ($getShareData && $this->isUserOrGroupInSharedData($group, "group", $permissions)) { + return; + } else { + $this->createShare( + $user, + $filepath, + '1', + $group, + null, + null, + $permissions + ); + } + if ($getShareData) { + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $userActual, + $this->getPasswordForUser($user), + "GET", + $path, + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + } + + /** + * @When /^user "([^"]*)" shares (?:file|folder|entry) "([^"]*)" with group "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * @When /^user "([^"]*)" shares (?:file|folder|entry) "([^"]*)" with group "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * + * @param string $user + * @param string $filepath + * @param string $group + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function userSharesFileWithGroupUsingTheSharingApi( + string $user, + string $filepath, + string $group, + $permissions = null + ) { + $this->shareFileWithGroupUsingTheSharingApi( + $user, + $filepath, + $group, + $permissions + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" shares the following (?:files|folders|entries) with group "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @When /^user "([^"]*)" shares the following (?:files|folders|entries) with group "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $group + * @param TableNode $table + * @param string|int|string[]|int[] $permissions + * + * @return void + * @throws Exception + */ + public function userSharesTheFollowingFilesWithGroupUsingTheSharingApi( + string $user, + string $group, + TableNode $table, + $permissions = null + ) { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $filepath) { + $this->userSharesFileWithGroupUsingTheSharingApi( + $user, + $filepath["path"], + $group, + $permissions + ); + } + } + + /** + * @Given /^user "([^"]*)" has shared (?:file|folder|entry) "([^"]*)" with group "([^"]*)" with permissions "([^"]*)"$/ + * @Given /^user "([^"]*)" has shared (?:file|folder|entry) "([^"]*)" with group "([^"]*)"(?: with permissions (\d+))?$/ + * + * @param string $user + * @param string $filepath + * @param string $group + * @param string|int|string[]|int[] $permissions + * + * @return void + */ + public function userHasSharedFileWithGroupUsingTheSharingApi( + string $user, + string $filepath, + string $group, + $permissions = null + ) { + $this->shareFileWithGroupUsingTheSharingApi( + $user, + $filepath, + $group, + $permissions, + true + ); + + Assert::assertEquals( + true, + $this->isUserOrGroupInSharedData($group, "group", $permissions), + __METHOD__ + . " Could not assert that user '$user' has shared '$filepath' with group '$group' with permissions '$permissions'" + ); + } + + /** + * @When /^user "([^"]*)" tries to update the last share using the sharing API with$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userTriesToUpdateTheLastShareUsingTheSharingApiWith(string $user, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body); + } + + /** + * @When /^user "([^"]*)" tries to update the last public link share using the sharing API with$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function userTriesToUpdateTheLastPublicLinkShareUsingTheSharingApiWith(string $user, ?TableNode $body):void { + $this->updateLastShareWithSettings($user, $body, null, true); + } + + /** + * @Then /^user "([^"]*)" should not be able to share (file|folder|entry) "([^"]*)" with (user|group) "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @Then /^user "([^"]*)" should not be able to share (file|folder|entry) "([^"]*)" with (user|group) "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $sharer + * @param string $entry + * @param string $filepath + * @param string $userOrGroupShareType + * @param string $sharee + * @param string|int|string[]|int[] $permissions + * + * @return void + * @throws Exception + */ + public function userTriesToShareFileUsingTheSharingApi( + string $sharer, + string $entry, + string $filepath, + string $userOrGroupShareType, + string $sharee, + $permissions = null + ):void { + $sharee = $this->getActualUsername($sharee); + $this->asFileOrFolderShouldExist( + $this->getActualUsername($sharer), + $entry, + $filepath + ); + $this->createShare( + $sharer, + $filepath, + $userOrGroupShareType, + $sharee, + null, + null, + $permissions + ); + $statusCode = $this->ocsContext->getOCSResponseStatusCode($this->response); + Assert::assertTrue( + ($statusCode == 404) || ($statusCode == 403), + "Sharing should have failed with status code 403 or 404 but got status code $statusCode" + ); + } + + /** + * @Then /^user "([^"]*)" should be able to share (file|folder|entry) "([^"]*)" with (user|group) "([^"]*)"(?: with permissions (\d+))? using the sharing API$/ + * @Then /^user "([^"]*)" should be able to share (file|folder|entry) "([^"]*)" with (user|group) "([^"]*)" with permissions "([^"]*)" using the sharing API$/ + * + * @param string $sharer + * @param string $entry + * @param string $filepath + * @param string $userOrGroupShareType + * @param string $sharee + * @param string|int|string[]|int[] $permissions + * + * @return void + * @throws Exception + */ + public function userShouldBeAbleToShareUsingTheSharingApi( + string $sharer, + string $entry, + string $filepath, + string $userOrGroupShareType, + string $sharee, + $permissions = null + ):void { + $sharee = $this->getActualUsername($sharee); + $this->asFileOrFolderShouldExist($sharer, $entry, $filepath); + $this->createShare( + $sharer, + $filepath, + $userOrGroupShareType, + $sharee, + null, + null, + $permissions + ); + + //v1.php returns 100 as success code + //v2.php returns 200 in the same case + $this->ocsContext->theOCSStatusCodeShouldBe("100, 200"); + } + + /** + * @When /^the user deletes the last share using the sharing API$/ + * + * @return void + */ + public function theUserDeletesLastShareUsingTheSharingAPI():void { + $this->deleteLastShareUsingSharingApiByCurrentUser(); + } + + /** + * @Given /^the user has deleted the last share$/ + * + * @return void + */ + public function theUserHasDeletedLastShareUsingTheSharingAPI():void { + $this->deleteLastShareUsingSharingApiByCurrentUser(); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @param string $user the user who will do the delete request + * @param string|null $sharer the specific user whose share will be deleted (if specified) + * @param bool $deleteLastPublicLink + * + * @return void + */ + public function deleteLastShareUsingSharingApi(string $user, string $sharer = null, bool $deleteLastPublicLink = false):void { + $user = $this->getActualUsername($user); + if ($deleteLastPublicLink) { + $shareId = $this->getLastPublicLinkShareId(); + } else { + if ($sharer === null) { + $shareId = $this->getLastShareId(); + } else { + $shareId = $this->getLastShareIdForUser($sharer); + } + } + $url = $this->getSharesEndpointPath("/$shareId"); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "DELETE", + $url, + null + ); + } + + /** + * @return void + */ + public function deleteLastShareUsingSharingApiByCurrentUser():void { + $this->deleteLastShareUsingSharingApi($this->currentUser); + } + + /** + * @When /^user "([^"]*)" deletes the last share using the sharing API$/ + * @When /^user "([^"]*)" tries to delete the last share using the sharing API$/ + * + * @param string $user + * + * @return void + */ + public function userDeletesLastShareUsingTheSharingApi(string $user):void { + $this->deleteLastShareUsingSharingApi($user); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" deletes the last public link share using the sharing API$/ + * @When /^user "([^"]*)" tries to delete the last public link share using the sharing API$/ + * + * @param string $user + * + * @return void + */ + public function userDeletesLastPublicLinkShareUsingTheSharingApi(string $user):void { + $this->deleteLastShareUsingSharingApi($user, null, true); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" deletes the last share of user "([^"]*)" using the sharing API$/ + * @When /^user "([^"]*)" tries to delete the last share of user "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $sharer + * + * @return void + */ + public function userDeletesLastShareOfUserUsingTheSharingApi(string $user, string $sharer):void { + $this->deleteLastShareUsingSharingApi($user, $sharer); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has deleted the last share$/ + * + * @param string $user + * + * @return void + */ + public function userHasDeletedLastShareUsingTheSharingApi(string $user):void { + $this->deleteLastShareUsingSharingApi($user); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When /^the user gets the info of the last share using the sharing API$/ + * + * @return void + * @throws Exception + */ + public function theUserGetsInfoOfLastShareUsingTheSharingApi():void { + $this->userGetsInfoOfLastShareUsingTheSharingApi($this->currentUser); + } + + /** + * @When /^user "([^"]*)" gets the info of the last share in language "([^"]*)" using the sharing API$/ + * @When /^user "([^"]*)" gets the info of the last share using the sharing API$/ + * + * @param string $user username that requests the information (might not be the user that has initiated the share) + * @param string|null $language + * + * @return void + * @throws Exception + */ + public function userGetsInfoOfLastShareUsingTheSharingApi(string $user, ?string $language = null):void { + $shareId = $this->getLastShareId(); + $language = TranslationHelper::getLanguage($language); + $this->getShareData($user, $shareId, $language); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^the user gets the info of the last public link share using the sharing API$/ + * + * @return void + * @throws Exception + */ + public function theUserGetsInfoOfLastPublicLinkShareUsingTheSharingApi():void { + $this->userGetsInfoOfLastPublicLinkShareUsingTheSharingApi($this->getUserWhoCreatedLastPublicShare()); + } + + /** + * @When /^user "([^"]*)" gets the info of the last public link share in language "([^"]*)" using the sharing API$/ + * @When /^user "([^"]*)" gets the info of the last public link share using the sharing API$/ + * + * @param string $user username that requests the information (might not be the user that has initiated the share) + * @param string|null $language + * + * @return void + * @throws Exception + */ + public function userGetsInfoOfLastPublicLinkShareUsingTheSharingApi(string $user, ?string $language = null):void { + if ($this->lastPublicShareId !== null) { + $shareId = $this->lastPublicShareId; + } else { + throw new Exception( + __METHOD__ . " last public link share data was not found" + ); + } + $language = TranslationHelper::getLanguage($language); + $this->getShareData($user, $shareId, $language); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Then /^as "([^"]*)" the info about the last share by user "([^"]*)" with user "([^"]*)" should include$/ + * + * @param string $requestor + * @param string $sharer + * @param string $sharee + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function asLastShareInfoAboutUserSharingWithUserShouldInclude( + string $requestor, + string $sharer, + string $sharee, + TableNode $table + ) { + $this->userGetsInfoOfLastShareUsingTheSharingApi($requestor); + $this->ocsContext->assertOCSResponseIndicatesSuccess(); + $this->checkFieldsOfLastResponseToUser($sharer, $sharee, $table); + } + + /** + * @Then /^the info about the last share by user "([^"]*)" with (?:user|group) "([^"]*)" should include$/ + * + * @param string $sharer + * @param string $sharee + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theInfoAboutTheLastShareByUserWithUserShouldInclude( + string $sharer, + string $sharee, + TableNode $table + ):void { + $this->asLastShareInfoAboutUserSharingWithUserShouldInclude($sharer, $sharer, $sharee, $table); + } + + /** + * Sets the id of the last shared file + * + * @param string $user + * @param string $shareId + * + * @return void + */ + public function setLastShareIdOf(string $user, string $shareId):void { + $this->lastShareIdByUser[$user] = $shareId; + $this->userWhoCreatedLastShare = $user; + } + + /** + * Sets the id of the last public link shared file + * + * @param string $shareId + * + * @return void + */ + public function setLastPublicLinkShareId(string $shareId):void { + $this->lastPublicShareId = $shareId; + } + + /** + * Retrieves the id of the last public link shared file + * + * @return string|null + */ + public function getLastPublicLinkShareId():?string { + return $this->lastPublicShareId; + } + + /** + * Sets the user who created the last public link share + * + * @param string $user + * + * @return void + */ + public function setUserWhoCreatedLastPublicShare(string $user):void { + $this->userWhoCreatedLastPublicShare = $user; + } + + /** + * Gets the user who created the last public link share + * + * @return string|null + */ + public function getUserWhoCreatedLastPublicShare():?string { + return $this->userWhoCreatedLastPublicShare; + } + + /** + * Retrieves the id of the last shared file + * + * @return string|null + */ + public function getLastShareId():?string { + return $this->getLastShareIdForUser($this->userWhoCreatedLastShare); + } + + /** + * @param string $user + * + * @return string|null + */ + public function getLastShareIdForUser(string $user):?string { + if ($user === null) { + throw new Exception( + __METHOD__ . " user not specified. Probably no user or group shares have been created yet in the test scenario." + ); + } + if (isset($this->lastShareIdByUser[$user])) { + return $this->lastShareIdByUser[$user]; + } else { + throw new Exception(__METHOD__ . " last share id for user '$user' was not found"); + } + } + + /** + * Retrieves all the shares of the respective user + * + * @param string $user + * + * @return ResponseInterface + */ + public function getListOfShares(string $user):ResponseInterface { + $user = $this->getActualUsername($user); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath(), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + return $this->response; + } + + /** + * Get share data of specific share_id + * + * @param string $user + * @param string $share_id + * @param string|null $language + * + * @return void + */ + public function getShareData(string $user, string $share_id, ?string $language = null):void { + $user = $this->getActualUsername($user); + $url = $this->getSharesEndpointPath("/$share_id"); + $headers = []; + if ($language !== null) { + $headers['Accept-Language'] = $language; + } + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "GET", + $url, + null, + null, + $headers + ); + } + + /** + * @When user :user gets all the shares shared with him using the sharing API + * + * @param string $user + * + * @return void + */ + public function userGetsAllTheSharesSharedWithHimUsingTheSharingApi(string $user):void { + $user = $this->getActualUsername($user); + $url = "/apps/files_sharing/api/v1/shares?shared_with_me=true"; + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + 'GET', + $url, + null + ); + } + + /** + * @When /^user "([^"]*)" gets the (|pending)\s?(user|group|user and group|public link) shares shared with him using the sharing API$/ + * + * @param string $user + * @param string $pending + * @param string $shareType + * + * @return void + */ + public function userGetsFilteredSharesSharedWithHimUsingTheSharingApi(string $user, string $pending, string $shareType):void { + $user = $this->getActualUsername($user); + if ($pending === "pending") { + $pendingClause = "&state=" . SharingHelper::SHARE_STATES['pending']; + } else { + $pendingClause = ""; + } + if ($shareType === 'public link') { + $shareType = 'public_link'; + } + if ($shareType === 'user and group') { + $rawShareTypes = SharingHelper::SHARE_TYPES['user'] . "," . SharingHelper::SHARE_TYPES['group']; + } else { + $rawShareTypes = SharingHelper::SHARE_TYPES[$shareType]; + } + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + 'GET', + $this->getSharesEndpointPath( + "?shared_with_me=true" . $pendingClause . "&share_types=" . $rawShareTypes + ), + null + ); + } + + /** + * @When /^user "([^"]*)" gets all the shares shared with him that are received as (?:file|folder|entry) "([^"]*)" using the provisioning API$/ + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userGetsAllSharesSharedWithHimFromFileOrFolderUsingTheProvisioningApi(string $user, string $path):void { + $user = $this->getActualUsername($user); + $url = "/apps/files_sharing/api/" + . "v{$this->sharingApiVersion}/shares?shared_with_me=true&path=$path"; + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + 'GET', + $url, + null + ); + } + + /** + * @When user :user gets all shares shared by him using the sharing API + * + * @param string $user + * + * @return void + */ + public function userGetsAllSharesSharedByHimUsingTheSharingApi(string $user):void { + $user = $this->getActualUsername($user); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath(), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + + /** + * @When the administrator gets all shares shared by him using the sharing API + * + * @return void + */ + public function theAdministratorGetsAllSharesSharedByHimUsingTheSharingApi():void { + $this->userGetsAllSharesSharedByHimUsingTheSharingApi($this->getAdminUsername()); + } + + /** + * @When /^user "([^"]*)" gets the (user|group|user and group|public link) shares shared by him using the sharing API$/ + * + * @param string $user + * @param string $shareType + * + * @return void + */ + public function userGetsFilteredSharesSharedByHimUsingTheSharingApi(string $user, string $shareType):void { + $user = $this->getActualUsername($user); + if ($shareType === 'public link') { + $shareType = 'public_link'; + } + if ($shareType === 'user and group') { + $rawShareTypes = SharingHelper::SHARE_TYPES['user'] . "," . SharingHelper::SHARE_TYPES['group']; + } else { + $rawShareTypes = SharingHelper::SHARE_TYPES[$shareType]; + } + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath("?share_types=" . $rawShareTypes), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + + /** + * @When user :user gets all the shares from the file :path using the sharing API + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userGetsAllTheSharesFromTheFileUsingTheSharingApi(string $user, string $path):void { + $user = $this->getActualUsername($user); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath("?path=$path"), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + + /** + * @When user :user gets all the shares with reshares from the file :path using the sharing API + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userGetsAllTheSharesWithResharesFromTheFileUsingTheSharingApi( + string $user, + string $path + ):void { + $userActual = $this->getActualUsername($user); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $userActual, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath("?reshares=true&path=$path"), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + + /** + * @When user :user gets all the shares inside the folder :path using the sharing API + * + * @param string $user + * @param string $path + * + * @return void + */ + public function userGetsAllTheSharesInsideTheFolderUsingTheSharingApi(string $user, string $path):void { + $user = $this->getActualUsername($user); + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath("?path=$path&subfiles=true"), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + } + + /** + * @Then /^the response when user "([^"]*)" gets the info of the last share should include$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theResponseWhenUserGetsInfoOfLastShareShouldInclude( + string $user, + ?TableNode $body + ):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeRows($body, [], $this->shareResponseFields); + $this->getShareData($user, (string)$this->getLastShareData()->data[0]->id); + $this->theHTTPStatusCodeShouldBe( + 200, + "Error getting info of last share for user $user" + ); + $this->ocsContext->assertOCSResponseIndicatesSuccess( + __METHOD__ . + ' Error getting info of last share for user $user\n' . + $this->ocsContext->getOCSResponseStatusMessage( + $this->getResponse() + ) . '"' + ); + $this->checkFields($user, $body); + } + + /** + * @Then /^the response when user "([^"]*)" gets the info of the last public link share should include$/ + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function theResponseWhenUserGetsInfoOfLastPublicLinkShareShouldInclude( + string $user, + ?TableNode $body + ):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeRows($body, [], $this->shareResponseFields); + $this->getShareData($user, (string)$this->getLastPublicLinkShareId()); + $this->theHTTPStatusCodeShouldBe( + 200, + "Error getting info of last public link share for user $user" + ); + $this->ocsContext->assertOCSResponseIndicatesSuccess( + __METHOD__ . + ' Error getting info of last public link share for user $user\n' . + $this->ocsContext->getOCSResponseStatusMessage( + $this->getResponse() + ) . '"' + ); + $this->checkFields($user, $body); + } + + /** + * @Then the information of the last share of user :user should include + * + * @param string $user + * @param TableNode $body + * + * @return void + * @throws Exception + */ + public function informationOfLastShareShouldInclude( + string $user, + TableNode $body + ):void { + $user = $this->getActualUsername($user); + $shareId = $this->getLastShareIdForUser($user); + $this->getShareData($user, $shareId); + $this->theHTTPStatusCodeShouldBe( + 200, + "Error getting info of last share for user $user with share id $shareId" + ); + $this->verifyTableNodeRows($body, [], $this->shareResponseFields); + $this->checkFields($user, $body); + } + + /** + * @Then /^the information for user "((?:[^']*)|(?:[^"]*))" about the received share of (file|folder) "((?:[^']*)|(?:[^"]*))" shared with a (user|group) should include$/ + * + * @param string $user + * @param string $fileOrFolder + * @param string $fileName + * @param string $type + * @param TableNode $body should provide share_type + * + * @return void + * @throws Exception + */ + public function theFieldsOfTheResponseForUserForResourceShouldInclude( + string $user, + string $fileOrFolder, + string $fileName, + string $type, + TableNode $body + ):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumnsCount($body, 2); + $fileName = $fileName[0] === "/" ? $fileName : '/' . $fileName; + $data = $this->getAllSharesSharedWithUser($user); + Assert::assertNotEmpty($data, 'No shares found for ' . $user); + + $bodyRows = $body->getRowsHash(); + Assert::assertArrayHasKey('share_type', $bodyRows, 'share_type is not provided'); + $share_id = null; + foreach ($data as $share) { + if ($share['file_target'] === $fileName && $share['item_type'] === $fileOrFolder) { + if (($share['share_type'] === SharingHelper::getShareType($bodyRows['share_type'])) + ) { + $share_id = $share['id']; + } + } + } + + Assert::assertNotNull($share_id, "Could not find share id for " . $user); + + if (\array_key_exists('expiration', $bodyRows) && $bodyRows['expiration'] !== '') { + $bodyRows['expiration'] = \date('d-m-Y', \strtotime($bodyRows['expiration'])); + } + + $this->getShareData($user, $share_id); + foreach ($bodyRows as $field => $value) { + if ($type === "user" && \in_array($field, ["share_with"])) { + $value = $this->getActualUsername($value); + } + if (\in_array($field, ["uid_owner"])) { + $value = $this->getActualUsername($value); + } + $value = $this->replaceValuesFromTable($field, $value); + Assert::assertTrue( + $this->isFieldInResponse($field, $value), + "$field doesn't have value '$value'" + ); + } + } + + /** + * @Then /^the last share_id should be included in the response/ + * + * @return void + * @throws Exception + */ + public function checkingLastShareIDIsIncluded():void { + $shareId = $this->getLastShareId(); + if (!$this->isFieldInResponse('id', $shareId)) { + Assert::fail( + "Share id $shareId not found in response" + ); + } + } + + /** + * @Then /^the last share id should not be included in the response/ + * + * @return void + * @throws Exception + */ + public function checkLastShareIDIsNotIncluded():void { + $shareId = $this->getLastShareId(); + if ($this->isFieldInResponse('id', $shareId, false)) { + Assert::fail( + "Share id $shareId has been found in response" + ); + } + } + + /** + * @Then /^the last public link share id should not be included in the response/ + * + * @return void + * @throws Exception + */ + public function checkLastPublicLinkShareIDIsNotIncluded():void { + $shareId = $this->getLastPublicLinkShareId(); + if ($this->isFieldInResponse('id', $shareId, false)) { + Assert::fail( + "Public link share id $shareId has been found in response" + ); + } + } + + /** + * @Then /^the response should not contain any share ids/ + * + * @return void + */ + public function theResponseShouldNotContainAnyShareIds():void { + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + $fieldIsSet = false; + $receivedShareCount = 0; + + if (\count($data->element) > 0) { + foreach ($data as $element) { + if (isset($element->id)) { + $fieldIsSet = true; + $receivedShareCount += 1; + } + } + } else { + if (isset($data->id)) { + $fieldIsSet = true; + $receivedShareCount += 1; + } + } + Assert::assertFalse( + $fieldIsSet, + "response contains $receivedShareCount share ids but should not contain any share ids" + ); + } + + /** + * @Then user :user should not see the share id of the last share + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function userShouldNotSeeShareIdOfLastShare(string $user):void { + $this->userGetsAllTheSharesSharedWithHimUsingTheSharingApi($user); + $this->checkLastShareIDIsNotIncluded(); + } + + /** + * @Then user :user should not have any received shares + * + * @param string $user + * + * @return void + */ + public function userShouldNotHaveAnyReceivedShares(string $user):void { + $this->userGetsAllTheSharesSharedWithHimUsingTheSharingApi($user); + $this->theResponseShouldNotContainAnyShareIds(); + } + + /** + * @Then /^the response should contain ([0-9]+) entries$/ + * + * @param int $count + * + * @return void + */ + public function checkingTheResponseEntriesCount(int $count):void { + $actualCount = \count($this->getResponseXml(null, __METHOD__)->data[0]); + Assert::assertEquals( + $count, + $actualCount, + "Expected that the response should contain '$count' entries but got '$actualCount' entries" + ); + } + + /** + * @Then the fields of the last response to user :user should include + * + * @param string $user + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function checkFields(string $user, ?TableNode $body):void { + $this->verifyTableNodeColumnsCount($body, 2); + $bodyRows = $body->getRowsHash(); + $userRelatedFieldNames = [ + "owner", + "user", + "uid_owner", + "uid_file_owner", + "share_with", + "displayname_file_owner", + "displayname_owner" + ]; + foreach ($bodyRows as $field => $value) { + if (\in_array($field, $userRelatedFieldNames)) { + $value = $this->substituteInLineCodes($value, $user); + } + $value = $this->getActualUsername($value); + $value = $this->replaceValuesFromTable($field, $value); + Assert::assertTrue( + $this->isFieldInResponse($field, $value), + "$field doesn't have value '$value'" + ); + } + } + + /** + * @Then /^the fields of the last response (?:to|about) user "([^"]*)" sharing with (?:user|group) "([^"]*)" should include$/ + * + * @param string $sharer + * @param string $sharee + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function checkFieldsOfLastResponseToUser(string $sharer, string $sharee, ?TableNode $body):void { + $this->verifyTableNodeColumnsCount($body, 2); + $bodyRows = $body->getRowsHash(); + foreach ($bodyRows as $field => $value) { + if (\in_array($field, ["displayname_owner", "displayname_file_owner", "owner", "uid_owner", "uid_file_owner"])) { + $value = $this->substituteInLineCodes($value, $sharer); + } elseif (\in_array($field, ["share_with", "share_with_displayname", "user"])) { + $value = $this->substituteInLineCodes($value, $sharee); + } + $value = $this->replaceValuesFromTable($field, $value); + Assert::assertTrue( + $this->isFieldInResponse($field, $value), + "$field doesn't have value '$value'" + ); + } + } + + /** + * @Then the last response should be empty + * + * @return void + */ + public function theFieldsOfTheLastResponseShouldBeEmpty():void { + $data = $this->getResponseXml(null, __METHOD__)->data[0]; + Assert::assertEquals( + \count($data->element), + 0, + "last response contains data but was expected to be empty" + ); + } + + /** + * + * @return string + * + * @throws Exception + */ + public function getSharingAttributesFromLastResponse():string { + $responseXml = $this->getResponseXml(null, __METHOD__)->data[0]; + $actualAttributesElement = $responseXml->xpath('//attributes'); + + if ((bool) $actualAttributesElement) { + $actualAttributes = (array) $actualAttributesElement[0]; + if (empty($actualAttributes)) { + throw new Exception( + "No data inside 'attributes' element in the last response." + ); + } + return $actualAttributes[0]; + } + + throw new Exception("No 'attributes' found inside the response of the last share."); + } + + /** + * @Then the additional sharing attributes for the response should include + * + * @param TableNode $attributes + * + * @return void + * @throws Exception + */ + public function checkingAttributesInLastShareResponse(TableNode $attributes):void { + $this->verifyTableNodeColumns($attributes, ['scope', 'key', 'enabled']); + $attributes = $attributes->getHash(); + + // change string "true"/"false" to boolean inside array + \array_walk_recursive( + $attributes, + function (&$value, $key) { + if ($key !== 'enabled') { + return; + } + if ($value === 'true') { + $value = true; + } + if ($value === 'false') { + $value = false; + } + } + ); + + $actualAttributes = $this->getSharingAttributesFromLastResponse(); + + // parse json to array + $actualAttributesArray = \json_decode($actualAttributes, true); + if (\json_last_error() !== JSON_ERROR_NONE) { + $errMsg = \strtolower(\json_last_error_msg()); + throw new Exception( + "JSON decoding failed because of $errMsg in json\n" . + 'Expected data to be json with array of objects. ' . + "\nReceived:\n $actualAttributes" + ); + } + + // check if the expected attributes received from table match actualAttributes + foreach ($attributes as $row) { + $foundRow = false; + foreach ($actualAttributesArray as $item) { + if (($item['scope'] === $row['scope']) + && ($item['key'] === $row['key']) + && ($item['enabled'] === $row['enabled']) + ) { + $foundRow = true; + } + } + Assert::assertTrue( + $foundRow, + "Could not find expected attribute with scope '" . $row['scope'] . "' and key '" . $row['key'] . "'" + ); + } + } + + /** + * @Then the downloading of file :fileName for user :user should fail with error message + * + * @param string $fileName + * @param string $user + * @param PyStringNode $errorMessage + * + * @return void + * @throws Exception + */ + public function userDownloadsFailWithMessage(string $fileName, string $user, PyStringNode $errorMessage):void { + $user = $this->getActualUsername($user); + $this->downloadFileAsUserUsingPassword($user, $fileName); + $receivedErrorMessage = $this->getResponseXml(null, __METHOD__)->xpath('//s:message'); + if ((bool) $errorMessage) { + Assert::assertEquals( + $errorMessage, + (string) $receivedErrorMessage[0], + "Expected error message was '$errorMessage' but got '" + . (string) $receivedErrorMessage[0] + . "'" + ); + return; + } + throw new Exception("No 's:message' element found on the response."); + } + + /** + * @Then the fields of the last response should not include + * + * @param TableNode|null $body + * + * @return void + * @throws Exception + */ + public function checkFieldsNotInResponse(?TableNode $body):void { + $this->verifyTableNodeColumnsCount($body, 2); + $bodyRows = $body->getRowsHash(); + + foreach ($bodyRows as $field => $value) { + $value = $this->replaceValuesFromTable($field, $value); + Assert::assertFalse( + $this->isFieldInResponse($field, $value, false), + "$field has value $value but should not" + ); + } + } + + /** + * @param string $user + * @param string $fileName + * + * @return void + * @throws Exception + */ + public function removeAllSharesFromResource(string $user, string $fileName):void { + $headers = ['Content-Type' => 'application/json']; + $res = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath("?format=json"), + $this->getStepLineRef(), + [], + $this->ocsApiVersion, + $headers + ); + + $this->setResponse($res); + $this->theHTTPStatusCodeShouldBeSuccess(); + + $json = \json_decode($res->getBody()->getContents(), true); + $deleted = false; + foreach ($json['ocs']['data'] as $data) { + if (\stripslashes($data['path']) === $fileName) { + $id = $data['id']; + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "DELETE", + $this->getSharesEndpointPath("/{$id}"), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + + $this->setResponse($response); + $this->theHTTPStatusCodeShouldBeSuccess(); + + $deleted = true; + } + } + + if ($deleted === false) { + throw new Exception( + "Could not delete shares for user $user file $fileName" + ); + } + } + + /** + * @When user :user removes all shares from the file named :fileName using the sharing API + * + * @param string $user + * @param string $fileName + * + * @return void + * @throws Exception + */ + public function userRemovesAllSharesFromTheFileNamed(string $user, string $fileName):void { + $user = $this->getActualUsername($user); + $this->removeAllSharesFromResource($user, $fileName); + } + + /** + * @Given user :user has removed all shares from the file named :fileName + * + * @param string $user + * @param string $fileName + * + * @return void + * @throws Exception + */ + public function userHasRemovedAllSharesFromTheFileNamed(string $user, string $fileName):void { + $user = $this->getActualUsername($user); + $this->removeAllSharesFromResource($user, $fileName); + $dataResponded = $this->getShares($user, $fileName); + Assert::assertEquals( + 0, + \count($dataResponded), + __METHOD__ + . " Expected all shares to be removed from '$fileName' but got '" + . \count($dataResponded) + . "' shares still present" + ); + } + + /** + * Returns shares of a file or folder as a SimpleXMLElement + * + * Note: the "single" SimpleXMLElement may contain one or more actual + * shares (to users, groups or public links etc). If you access an item directly, + * for example, getShares()->id, then the value of "id" for the first element + * will be returned. To access all the elements, you can loop through the + * returned SimpleXMLElement with "foreach" - it will act like a PHP array + * of elements. + * + * @param string $user + * @param string $path + * + * @return SimpleXMLElement + */ + public function getShares(string $user, string $path):SimpleXMLElement { + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $this->getSharesEndpointPath("?path=$path"), + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + return $this->getResponseXml(null, __METHOD__)->data->element; + } + + /** + * @Then /^as user "([^"]*)" the public shares of (?:file|folder) "([^"]*)" should be$/ + * + * @param string $user + * @param string $path + * @param TableNode|null $TableNode + * + * @return void + * @throws Exception + */ + public function checkPublicShares(string $user, string $path, ?TableNode $TableNode):void { + $user = $this->getActualUsername($user); + $dataResponded = $this->getShares($user, $path); + + $this->verifyTableNodeColumns($TableNode, ['path', 'permissions', 'name']); + if ($TableNode instanceof TableNode) { + $elementRows = $TableNode->getHash(); + + foreach ($elementRows as $expectedElementsArray) { + $nameFound = false; + foreach ($dataResponded as $elementResponded) { + if ((string) $elementResponded->name[0] === $expectedElementsArray['name']) { + Assert::assertEquals( + $expectedElementsArray['path'], + (string) $elementResponded->path[0], + __METHOD__ + . " Expected '${expectedElementsArray['path']}' but got '" + . (string) $elementResponded->path[0] + . "'" + ); + Assert::assertEquals( + $expectedElementsArray['permissions'], + (string) $elementResponded->permissions[0], + __METHOD__ + . " Expected '${expectedElementsArray['permissions']}' but got '" + . (string) $elementResponded->permissions[0] + . "'" + ); + $nameFound = true; + break; + } + } + Assert::assertTrue( + $nameFound, + "Shared link name {$expectedElementsArray['name']} not found" + ); + } + } + } + + /** + * @Then /^as user "([^"]*)" the (file|folder) "([^"]*)" should not have any shares$/ + * + * @param string $user + * @param string $entry + * @param string $path + * + * @return void + * @throws Exception + */ + public function checkPublicSharesAreEmpty(string $user, string $entry, string $path):void { + $user = $this->getActualUsername($user); + $this->asFileOrFolderShouldExist($user, $entry, $path); + $dataResponded = $this->getShares($user, $path); + //It shouldn't have public shares + Assert::assertEquals( + 0, + \count($dataResponded), + __METHOD__ + . " As '$user', '$path' was expected to have no shares, but got '" + . \count($dataResponded) + . "' shares present" + ); + } + + /** + * @param string $user + * @param string $path to share + * @param string $name of share + * + * @return string|null + */ + public function getPublicShareIDByName(string $user, string $path, string $name):?string { + $dataResponded = $this->getShares($user, $path); + foreach ($dataResponded as $elementResponded) { + if ((string) $elementResponded->name[0] === $name) { + return (string) $elementResponded->id[0]; + } + } + return null; + } + + /** + * @param string $user + * @param string $name + * @param string $path + * + * @return void + */ + public function deletePublicLinkShareUsingTheSharingApi( + string $user, + string $name, + string $path + ):void { + $user = $this->getActualUsername($user); + $share_id = $this->getPublicShareIDByName($user, $path, $name); + $url = $this->getSharesEndpointPath("/$share_id"); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "DELETE", + $url, + null + ); + } + + /** + * @When /^user "([^"]*)" deletes public link share named "([^"]*)" in (?:file|folder) "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $name + * @param string $path + * + * @return void + */ + public function userDeletesPublicLinkShareNamedUsingTheSharingApi( + string $user, + string $name, + string $path + ):void { + $this->deletePublicLinkShareUsingTheSharingApi( + $user, + $name, + $path + ); + } + + /** + * @Given /^user "([^"]*)" has deleted public link share named "([^"]*)" in (?:file|folder) "([^"]*)"$/ + * + * @param string $user + * @param string $name + * @param string $path + * + * @return void + */ + public function userHasDeletedPublicLinkShareNamedUsingTheSharingApi( + string $user, + string $name, + string $path + ):void { + $this->deletePublicLinkShareUsingTheSharingApi( + $user, + $name, + $path + ); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When /^user "([^"]*)" (declines|accepts) share "([^"]*)" offered by user "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $action + * @param string $share + * @param string $offeredBy + * @param string|null $state specify 'accepted', 'pending', 'rejected' or 'declined' to only consider shares in that state + * + * @return void + * @throws Exception + */ + public function userReactsToShareOfferedBy(string $user, string $action, string $share, string $offeredBy, ?string $state = ''):void { + $user = $this->getActualUsername($user); + $offeredBy = $this->getActualUsername($offeredBy); + + $dataResponded = $this->getAllSharesSharedWithUser($user); + $shareId = null; + foreach ($dataResponded as $shareElement) { + $shareFolder = \trim( + SetupHelper::getSystemConfigValue('share_folder', $this->getStepLineRef()) + ); + + if ($shareFolder) { + $shareFolder = \ltrim($shareFolder, '/'); + } + + // Add share folder to share path if given + if ($shareFolder && !(strpos($share, "/$shareFolder") === 0)) { + $share = '/' . $shareFolder . $share; + } + + // SharingHelper::SHARE_STATES has the mapping between the words for share states + // like "accepted", "pending",... and the integer constants 0, 1,... that are in + // the "state" field of the share data. + if ($state === '') { + // Any share state is OK + $matchesShareState = true; + } else { + $requiredStateCode = SharingHelper::SHARE_STATES[$state]; + if ($shareElement['state'] === $requiredStateCode) { + $matchesShareState = true; + } else { + $matchesShareState = false; + } + } + + if ($matchesShareState + && (string) $shareElement['uid_owner'] === $offeredBy + && (string) $shareElement['path'] === $share + ) { + $shareId = (string) $shareElement['id']; + break; + } + } + Assert::assertNotNull( + $shareId, + __METHOD__ . " could not find share $share, offered by $offeredBy to $user" + ); + $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}" . + "/shares/pending/$shareId"; + if (\substr($action, 0, 7) === "decline") { + $httpRequestMethod = "DELETE"; + } elseif (\substr($action, 0, 6) === "accept") { + $httpRequestMethod = "POST"; + } + + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $httpRequestMethod, + $url, + null + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" (declines|accepts) the following shares offered by user "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $action + * @param string $offeredBy + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userReactsToTheFollowingSharesOfferedBy(string $user, string $action, string $offeredBy, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $share) { + $this->userReactsToShareOfferedBy( + $user, + $action, + $share["path"], + $offeredBy + ); + } + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" (declines|accepts) share with ID "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $action + * @param string $share_id + * + * @return void + * @throws Exception + */ + public function userReactsToShareWithShareIDOfferedBy(string $user, string $action, string $share_id):void { + $user = $this->getActualUsername($user); + + $shareId = $this->substituteInLineCodes($share_id, $user); + + $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}" . + "/shares/pending/$shareId"; + if (\substr($action, 0, 7) === "decline") { + $httpRequestMethod = "DELETE"; + } elseif (\substr($action, 0, 6) === "accept") { + $httpRequestMethod = "POST"; + } + + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + $httpRequestMethod, + $url, + null + ); + } + + /** + * @Given /^user "([^"]*)" has (declined|accepted) share "([^"]*)" offered by user "([^"]*)"$/ + * + * @param string $user + * @param string $action + * @param string $share + * @param string $offeredBy + * + * @return void + * @throws Exception + */ + public function userHasReactedToShareOfferedBy(string $user, string $action, string $share, string $offeredBy):void { + $this->userReactsToShareOfferedBy($user, $action, $share, $offeredBy); + if ($action === 'declined') { + $actionText = 'decline'; + } + if ($action === 'accepted') { + $actionText = 'accept'; + } + $this->theHTTPStatusCodeShouldBe( + 200, + __METHOD__ . " could not $actionText share $share to $user by $offeredBy" + ); + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + } + + /** + * @When /^user "([^"]*)" accepts the (?:first|next|) pending share "([^"]*)" offered by user "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $share + * @param string $offeredBy + * + * @return void + * @throws Exception + */ + public function userAcceptsThePendingShareOfferedBy(string $user, string $share, string $offeredBy):void { + $this->userReactsToShareOfferedBy($user, 'accepts', $share, $offeredBy, 'pending'); + } + + /** + * @Given /^user "([^"]*)" has accepted the (?:first|next|) pending share "([^"]*)" offered by user "([^"]*)"$/ + * + * @param string $user + * @param string $share + * @param string $offeredBy + * + * @return void + * @throws Exception + */ + public function userHasAcceptedThePendingShareOfferedBy(string $user, string $share, string $offeredBy) { + $this->userAcceptsThePendingShareOfferedBy($user, $share, $offeredBy); + $this->theHTTPStatusCodeShouldBe( + 200, + __METHOD__ . " could not accept the pending share $share to $user by $offeredBy" + ); + $this->ocsContext->assertOCSResponseIndicatesSuccess(); + } + + /** + * @Then /^user "([^"]*)" should be able to (decline|accept) pending share "([^"]*)" offered by user "([^"]*)"$/ + * + * @param string $user + * @param string $action + * @param string $share + * @param string $offeredBy + * + * @return void + * @throws Exception + */ + public function userShouldBeAbleToAcceptShareOfferedBy( + string $user, + string $action, + string $share, + string $offeredBy + ) { + if ($action === 'accept') { + $this->userHasAcceptedThePendingShareOfferedBy($user, $share, $offeredBy); + } elseif ($action === 'decline') { + $this->userHasReactedToShareOfferedBy($user, 'declined', $share, $offeredBy); + } + } + + /** + * + * @Then /^the sharing API should report to user "([^"]*)" that these shares are in the (pending|accepted|declined) state$/ + * + * @param string $user + * @param string $state + * @param TableNode $table table with headings that correspond to the attributes + * of the share e.g. "|path|uid_owner|" + * + * @return void + * @throws Exception + */ + public function assertSharesOfUserAreInState(string $user, string $state, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["path"], $this->shareResponseFields); + $usersShares = $this->getAllSharesSharedWithUser($user, $state); + foreach ($table as $row) { + $found = false; + //the API returns the path without trailing slash, but we want to + //be able to accept leading and/or trailing slashes in the step definition + $row['path'] = "/" . \trim($row['path'], "/"); + foreach ($usersShares as $share) { + try { + Assert::assertArrayHasKey('path', $share); + Assert::assertEquals($row['path'], $share['path']); + $found = true; + break; + } catch (PHPUnit\Framework\ExpectationFailedException $e) { + } + } + if (!$found) { + Assert::fail( + "could not find the share with this attributes " . + \print_r($row, true) + ); + } + } + } + + /** + * + * @Then /^the sharing API should report to user "([^"]*)" that no shares are in the (pending|accepted|declined) state$/ + * + * @param string $user + * @param string $state + * + * @return void + * @throws Exception + */ + public function assertNoSharesOfUserAreInState(string $user, string $state):void { + $usersShares = $this->getAllSharesSharedWithUser($user, $state); + Assert::assertEmpty( + $usersShares, + "user has " . \count($usersShares) . " share(s) in the $state state" + ); + } + + /** + * @Then the sharing API should report that no shares are shared with user :user + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function assertThatNoSharesAreSharedWithUser(string $user):void { + $usersShares = $this->getAllSharesSharedWithUser($user); + Assert::assertEmpty( + $usersShares, + "user has " . \count($usersShares) . " share(s)" + ); + } + + /** + * @When the administrator adds group :group to the exclude groups from receiving shares list using the occ command + * + * @param string $group + * + * @return int + * @throws Exception + */ + public function administratorAddsGroupToExcludeFromReceivingSharesList(string $group): int { + //get current groups + $occExitCode = $this->runOcc( + ['config:app:get files_sharing blacklisted_receiver_groups'] + ); + + $occStdOut = $this->getStdOutOfOccCommand(); + $occStdErr = $this->getStdErrOfOccCommand(); + + if (($occExitCode !== 0) && ($occExitCode !== 1)) { + throw new Exception( + "occ config:app:get files_sharing blacklisted_receiver_groups failed with exit code " . + $occExitCode . ", output " . + $occStdOut . ", error output " . + $occStdErr + ); + } + + //if the setting was never set before stdOut will be empty and return code will be 1 + if (\trim($occStdOut) === "") { + $occStdOut = "[]"; + } + + $currentGroups = \json_decode($occStdOut, true); + Assert::assertNotNull( + $currentGroups, + "could not json decode app setting 'blacklisted_receiver_groups' of 'files_sharing'\n" . + "stdOut: '" . $occStdOut . "'\n" . + "stdErr: '" . $occStdErr . "'" + ); + + $currentGroups[] = $group; + return $this->runOcc( + [ + 'config:app:set', + 'files_sharing blacklisted_receiver_groups', + '--value=' . \json_encode($currentGroups) + ] + ); + } + + /** + * @Given the administrator has added group :group to the exclude groups from receiving shares list + * + * @param string $group + * + * @return void + * @throws Exception + */ + public function administratorHasAddedGroupToExcludeFromReceivingSharesList(string $group):void { + $setSettingExitCode = $this->administratorAddsGroupToExcludeFromReceivingSharesList($group); + if ($setSettingExitCode !== 0) { + throw new Exception( + __METHOD__ . " could not set files_sharing blacklisted_receiver_groups " . + $setSettingExitCode . " " . + $this->getStdOutOfOccCommand() . " " . + $this->getStdErrOfOccCommand() + ); + } + } + + /** + * @When user :user gets share with id :share using the sharing API + * + * @param string $user + * @param string $share_id + * + * @return ResponseInterface|null + */ + public function userGetsTheLastShareWithTheShareIdUsingTheSharingApi(string $user, string $share_id): ?ResponseInterface { + $user = $this->getActualUsername($user); + $share_id = $this->substituteInLineCodes($share_id, $user); + $url = $this->getSharesEndpointPath("/$share_id"); + + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "GET", + $url, + $this->getStepLineRef(), + [], + $this->ocsApiVersion + ); + return $this->response; + } + + /** + * + * @param string $user + * @param string|null $state pending|accepted|declined|rejected|all + * + * @return array of shares that are shared with this user + * @throws Exception + */ + private function getAllSharesSharedWithUser(string $user, ?string $state = "all"): array { + switch ($state) { + case 'pending': + case 'accepted': + case 'declined': + case 'rejected': + $stateCode = SharingHelper::SHARE_STATES[$state]; + break; + case 'all': + $stateCode = "all"; + break; + default: + throw new InvalidArgumentException( + __METHOD__ . ' invalid "state" given' + ); + break; + } + + $url = $this->getSharesEndpointPath("?format=json&shared_with_me=true&state=$stateCode"); + $this->ocsContext->userSendsHTTPMethodToOcsApiEndpointWithBody( + $user, + "GET", + $url, + null + ); + if ($this->response->getStatusCode() !== 200) { + throw new Exception( + __METHOD__ . " could not retrieve information about shares" + ); + } + $result = $this->response->getBody()->getContents(); + $usersShares = \json_decode($result, true); + if (!\is_array($usersShares)) { + throw new Exception( + __METHOD__ . " API result about shares is not valid JSON" + ); + } + return $usersShares['ocs']['data']; + } + + /** + * The tests can create public link shares with the API or with the webUI. + * If lastPublicShareData is null, then there have not been any created with the API, + * so look for details of a public link share created with the webUI. + * + * @return string authorization token + */ + public function getLastPublicShareToken():string { + if ($this->lastPublicShareData === null) { + return $this->getLastCreatedPublicLinkToken(); + } else { + if (\count($this->lastPublicShareData->data->element) > 0) { + return (string)$this->lastPublicShareData->data[0]->token; + } + + return (string)$this->lastPublicShareData->data->token; + } + } + + /** + * Returns the attribute values from the last public link share data + * + * @param $attr - attribute name to get + * + * @return string + * @throws Exception + */ + public function getLastPublicShareAttribute(string $attr): string { + if ($this->lastPublicShareData === null) { + throw new Exception(__METHOD__ . "No public share data available."); + } + if (!\in_array($attr, $this->shareResponseFields)) { + throw new Exception( + __METHOD__ . " attribute $attr is not in the list of allowed attributes" + ); + } + if (\count($this->lastPublicShareData->data->element) > 0) { + if (!isset($this->lastPublicShareData->data[0]->$attr)) { + throw new Exception(__METHOD__ . " No attribute $attr available in the last share data."); + } + return (string)$this->lastPublicShareData->data[0]->{$attr}; + } + + if (!isset($this->lastPublicShareData->data->$attr)) { + throw new Exception(__METHOD__ . " No attribute $attr available in the last share data."); + } + + return (string)$this->lastPublicShareData->data->{$attr}; + } + + /** + * @return string path of file that was shared (relevant when a single file has been shared) + */ + public function getLastPublicSharePath():string { + if ($this->lastPublicShareData === null) { + // There have not been any public links created with the API + // so get the path of the last public link created with the webUI + return $this->getLastCreatedPublicLinkPath(); + } else { + if (\count($this->lastPublicShareData->data->element) > 0) { + return (string)$this->lastPublicShareData->data[0]->path; + } + + return (string)$this->lastPublicShareData->data->path; + } + } + + /** + * Send request for preview of a file in a public link + * + * @param string $fileName + * @param string $token + * + * @return void + */ + public function getPublicPreviewOfFile(string $fileName, string $token):void { + $url = $this->getBaseUrl() . + "/index.php/apps/files_sharing/ajax/publicpreview.php" . + "?file=$fileName&t=$token"; + $resp = HttpRequestHelper::get( + $url, + $this->getStepLineRef() + ); + $this->setResponse($resp); + } + + /** + * @When the public accesses the preview of file :path from the last shared public link using the sharing API + * + * @param string $path + * + * @return void + */ + public function thePublicAccessesThePreviewOfTheSharedFileUsingTheSharingApi(string $path):void { + $shareData = $this->getLastPublicShareData(); + $token = (string) $shareData->data->token; + $this->getPublicPreviewOfFile($path, $token); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When the public accesses the preview of the following files from the last shared public link using the sharing API + * + * @param TableNode $table + * + * @throws Exception + * @return void + */ + public function thePublicAccessesThePreviewOfTheFollowingSharedFileUsingTheSharingApi( + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + $this->emptyLastHTTPStatusCodesArray(); + $this->emptyLastOCSStatusCodesArray(); + foreach ($paths as $path) { + $shareData = $this->getLastPublicShareData(); + $token = (string) $shareData->data->token; + $this->getPublicPreviewOfFile($path["path"], $token); + $this->pushToLastStatusCodesArrays(); + } + } + + /** + * @param string $user + * @param string $shareServer + * @param string|null $password + * + * @return void + */ + public function saveLastSharedPublicLinkShare( + string $user, + string $shareServer, + ?string $password = "" + ):void { + $user = $this->getActualUsername($user); + $userPassword = $this->getPasswordForUser($user); + + $shareData = $this->getLastPublicShareData(); + $owner = (string) $shareData->data->uid_owner; + $name = $this->encodePath((string) $shareData->data->file_target); + $name = \trim($name, "/"); + $ownerDisplayName = (string) $shareData->data->displayname_owner; + $token = (string) $shareData->data->token; + + if (\strtolower($shareServer) == "remote") { + $remote = $this->getRemoteBaseUrl(); + } else { + $remote = $this->getLocalBaseUrl(); + } + + $body['remote'] = $remote; + $body['token'] = $token; + $body['owner'] = $owner; + $body['ownerDisplayName'] = $ownerDisplayName; + $body['name'] = $name; + $body['password'] = $password; + + Assert::assertNotNull( + $token, + __METHOD__ . " could not find any public share" + ); + + $url = $this->getBaseUrl() . "/index.php/apps/files_sharing/external"; + + $response = HttpRequestHelper::post( + $url, + $this->getStepLineRef(), + $user, + $userPassword, + null, + $body + ); + $this->setResponse($response); + } + + /** + * @Given /^user "([^"]*)" has added the public share created from server "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $shareServer + * + * @return void + */ + public function userHasAddedPublicShareCreatedByUser(string $user, string $shareServer):void { + $this->saveLastSharedPublicLinkShare($user, $shareServer); + + $resBody = json_decode($this->response->getBody()->getContents()); + $status = ''; + $message = ''; + if ($resBody) { + $status = $resBody->status; + $message = $resBody->data->message; + } + + Assert::assertEquals( + 200, + $this->response->getStatusCode(), + __METHOD__ + . " Expected status code is '200' but got '" + . $this->response->getStatusCode() + . "'" + ); + Assert::assertNotEquals( + 'error', + $status, + __METHOD__ + . "\nFailed to save public share.\n'$message'" + ); + } + + /** + * @param string $user + * @param string $path + * @param string $type + * @param int $permissions + * + * @return array + */ + public function preparePublicQuickLinkPayload(string $user, string $path, string $type, int $permissions = 1): array { + return [ + "permissions" => $permissions, + "expireDate" => "", + "shareType" => 3, + "itemType" => $type, + "itemSource" => $this->getFileIdForPath($user, $path), + "name" => "Public quick link", + "attributes" => [ + [ + "scope" => "files_sharing", + "key" => "isQuickLink", + "value" => true + ] + ], + "path" => $path + ]; + } + + /** + * @Given /^user "([^"]*)" has created a read only public link for (file|folder) "([^"]*)"$/ + * + * @param string $user + * @param string $type + * @param string $path + * + * @return void + * @throws Exception + */ + public function theUserHasCreatedAReadOnlyPublicLinkForFileFolder(string $user, string $type, string $path):void { + $user = $this->getActualUsername($user); + $userPassword = $this->getPasswordForUser($user); + + $requestPayload = $this->preparePublicQuickLinkPayload($user, $path, $type); + $url = $this->getBaseUrl() . "/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json"; + + $response = HttpRequestHelper::post( + $url, + $this->getStepLineRef(), + $user, + $userPassword, + null, + $requestPayload + ); + $this->setResponse($response); + $this->theHTTPStatusCodeShouldBe(200); + } + + /** + * @When /^user "([^"]*)" adds the public share created from server "([^"]*)" using the sharing API$/ + * + * @param string $user + * @param string $shareServer + * + * @return void + */ + public function userAddsPublicShareCreatedByUser(string $user, string $shareServer):void { + $this->saveLastSharedPublicLinkShare($user, $shareServer); + } + + /** + * Expires last created public link share using the testing API + * + * @return void + * @throws GuzzleException + */ + public function expireLastCreatedPublicLinkShare():void { + $shareId = $this->getLastPublicLinkShareId(); + $this->expireShare($shareId); + } + + /** + * Expires a share using the testing API + * + * @param string|null $shareId optional share id, if null then expire the last share that was created. + * + * @return void + * @throws GuzzleException + */ + public function expireShare(string $shareId = null):void { + $adminUser = $this->getAdminUsername(); + if ($shareId === null) { + $shareId = $this->getLastShareId(); + } + $response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $adminUser, + $this->getAdminPassword(), + 'POST', + "/apps/testing/api/v1/expire-share/{$shareId}", + $this->getStepLineRef(), + [], + $this->getOcsApiVersion() + ); + $this->setResponse($response); + } + + /** + * @Given the administrator has expired the last created share using the testing API + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorHasExpiredTheLastCreatedShare():void { + $this->expireShare(); + $httpStatus = $this->getResponse()->getStatusCode(); + Assert::assertSame( + 200, + $httpStatus, + "Request to expire last share failed. HTTP status was '$httpStatus'" + ); + $ocsStatusMessage = $this->ocsContext->getOCSResponseStatusMessage($this->getResponse()); + if ($this->getOcsApiVersion() === 1) { + $expectedOcsStatusCode = "100"; + } else { + $expectedOcsStatusCode = "200"; + } + $this->ocsContext->theOCSStatusCodeShouldBe( + $expectedOcsStatusCode, + "Request to expire last share failed: '$ocsStatusMessage'" + ); + } + + /** + * @Given the administrator has expired the last created public link share using the testing API + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorHasExpiredTheLastCreatedPublicLinkShare():void { + $this->expireLastCreatedPublicLinkShare(); + $httpStatus = $this->getResponse()->getStatusCode(); + Assert::assertSame( + 200, + $this->getResponse()->getStatusCode(), + "Request to expire last public link share failed. HTTP status was '$httpStatus'" + ); + $ocsStatusMessage = $this->ocsContext->getOCSResponseStatusMessage($this->getResponse()); + if ($this->getOcsApiVersion() === 1) { + $expectedOcsStatusCode = "100"; + } else { + $expectedOcsStatusCode = "200"; + } + $this->ocsContext->theOCSStatusCodeShouldBe( + $expectedOcsStatusCode, + "Request to expire last public link share failed: '$ocsStatusMessage'" + ); + } + + /** + * @When the administrator expires the last created share using the testing API + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorExpiresTheLastCreatedShare():void { + $this->expireShare(); + } + + /** + * @When the administrator expires the last created public link share using the testing API + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorExpiresTheLastCreatedPublicLinkShare():void { + $this->expireLastCreatedPublicLinkShare(); + } + + /** + * replace values from table + * + * @param string $field + * @param string $value + * + * @return string + */ + public function replaceValuesFromTable(string $field, string $value):string { + if (\substr($field, 0, 10) === "share_with") { + $value = \str_replace( + "REMOTE", + $this->getRemoteBaseUrl(), + $value + ); + $value = \str_replace( + "LOCAL", + $this->getLocalBaseUrl(), + $value + ); + } + if (\substr($field, 0, 6) === "remote") { + $value = \str_replace( + "REMOTE", + $this->getRemoteBaseUrl(), + $value + ); + $value = \str_replace( + "LOCAL", + $this->getLocalBaseUrl(), + $value + ); + } + if ($field === "permissions") { + if (\is_string($value) && !\is_numeric($value)) { + $value = $this->splitPermissionsString($value); + } + $value = (string)SharingHelper::getPermissionSum($value); + } + if ($field === "share_type") { + $value = (string)SharingHelper::getShareType($value); + } + return $value; + } + + /** + * @return array of common sharing capability settings for testing + */ + protected function getCommonSharingConfigs():array { + return [ + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'auto_accept_share', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_auto_accept_share', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'api_enabled', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_enabled', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'public@@@enabled', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_links', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'public@@@upload', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_public_upload', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'group_sharing', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_group_sharing', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'share_with_group_members_only', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_only_share_with_group_members', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'share_with_membership_groups_only', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_only_share_with_membership_groups', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'exclude_groups_from_sharing', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_exclude_groups', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'exclude_groups_from_sharing_list', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_exclude_groups_list', + 'testingState' => '' + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => + 'user_enumeration@@@enabled', + 'testingApp' => 'core', + 'testingParameter' => + 'shareapi_allow_share_dialog_user_enumeration', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => + 'user_enumeration@@@group_members_only', + 'testingApp' => 'core', + 'testingParameter' => + 'shareapi_share_dialog_user_enumeration_group_members', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'resharing', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_resharing', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => + 'public@@@password@@@enforced_for@@@read_only', + 'testingApp' => 'core', + 'testingParameter' => + 'shareapi_enforce_links_password_read_only', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => + 'public@@@password@@@enforced_for@@@read_write', + 'testingApp' => 'core', + 'testingParameter' => + 'shareapi_enforce_links_password_read_write', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => + 'public@@@password@@@enforced_for@@@upload_only', + 'testingApp' => 'core', + 'testingParameter' => + 'shareapi_enforce_links_password_write_only', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'public@@@send_mail', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_public_notification', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'public@@@social_share', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_social_share', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'public@@@expire_date@@@enabled', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_default_expire_date', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'public@@@expire_date@@@enforced', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_enforce_expire_date', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'user@@@send_mail', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_allow_mail_notification', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'user@@@expire_date@@@enabled', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_default_expire_date_user_share', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'user@@@expire_date@@@enforced', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_enforce_expire_date_user_share', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'group@@@expire_date@@@enabled', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_default_expire_date_group_share', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'group@@@expire_date@@@enforced', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_enforce_expire_date_group_share', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'remote@@@expire_date@@@enabled', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_default_expire_date_remote_share', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'remote@@@expire_date@@@enforced', + 'testingApp' => 'core', + 'testingParameter' => 'shareapi_enforce_expire_date_remote_share', + 'testingState' => false + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'federation@@@outgoing', + 'testingApp' => 'files_sharing', + 'testingParameter' => 'outgoing_server2server_share_enabled', + 'testingState' => true + ], + [ + 'capabilitiesApp' => 'files_sharing', + 'capabilitiesParameter' => 'federation@@@incoming', + 'testingApp' => 'files_sharing', + 'testingParameter' => 'incoming_server2server_share_enabled', + 'testingState' => true + ] + ]; + } +} diff --git a/tests/acceptance/features/bootstrap/TUSContext.php b/tests/acceptance/features/bootstrap/TUSContext.php new file mode 100644 index 000000000..15df1b8a3 --- /dev/null +++ b/tests/acceptance/features/bootstrap/TUSContext.php @@ -0,0 +1,489 @@ + + * + * @copyright Copyright (c) 2020, ownCloud GmbH + * @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 + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use GuzzleHttp\Exception\GuzzleException; +use TestHelpers\HttpRequestHelper; +use TestHelpers\WebDavHelper; +use TusPhp\Exception\ConnectionException; +use TusPhp\Exception\TusException; +use TusPhp\Tus\Client; +use PHPUnit\Framework\Assert; + +require_once 'bootstrap.php'; + +/** + * TUS related test steps + */ +class TUSContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + private $resourceLocation = null; + + /** + * @When user :user creates a new TUS resource on the WebDAV API with these headers: + * + * @param string $user + * @param TableNode $headers + * @param string $content + * + * @return void + * + * @throws Exception + * @throws GuzzleException + */ + public function createNewTUSResourceWithHeaders(string $user, TableNode $headers, string $content = ''): void { + $this->featureContext->verifyTableNodeColumnsCount($headers, 2); + $user = $this->featureContext->getActualUsername($user); + $password = $this->featureContext->getUserPassword($user); + $this->resourceLocation = null; + + $this->featureContext->setResponse( + $this->featureContext->makeDavRequest( + $user, + "POST", + null, + $headers->getRowsHash(), + $content, + "files", + null, + false, + $password + ) + ); + $locationHeader = $this->featureContext->getResponse()->getHeader('Location'); + if (\sizeof($locationHeader) > 0) { + $this->resourceLocation = $locationHeader[0]; + } + } + + /** + * @Given user :user has created a new TUS resource on the WebDAV API with these headers: + * + * @param string $user + * @param TableNode $headers Tus-Resumable: 1.0.0 header is added automatically + * + * @return void + * + * @throws Exception + * @throws GuzzleException + */ + public function createNewTUSResource(string $user, TableNode $headers): void { + $rows = $headers->getRows(); + $rows[] = ['Tus-Resumable', '1.0.0']; + $this->createNewTUSResourceWithHeaders($user, new TableNode($rows)); + $this->featureContext->theHTTPStatusCodeShouldBe(201); + } + + /** + * @When /^user "([^"]*)" sends a chunk to the last created TUS Location with offset "([^"]*)" and data "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $offset + * @param string $data + * @param string $checksum + * + * @return void + * + * @throws GuzzleException + * @throws JsonException + */ + public function sendsAChunkToTUSLocationWithOffsetAndData(string $user, string $offset, string $data, string $checksum = ''): void { + $user = $this->featureContext->getActualUsername($user); + $password = $this->featureContext->getUserPassword($user); + $this->featureContext->setResponse( + HttpRequestHelper::sendRequest( + $this->resourceLocation, + $this->featureContext->getStepLineRef(), + 'PATCH', + $user, + $password, + [ + 'Content-Type' => 'application/offset+octet-stream', + 'Tus-Resumable' => '1.0.0', + 'Upload-Checksum' => $checksum, + 'Upload-Offset' => $offset + ], + $data + ) + ); + WebDavHelper::$SPACE_ID_FROM_OCIS = ''; + } + + /** + * @When user :user uploads file :source to :destination using the TUS protocol on the WebDAV API + * + * @param string|null $user + * @param string $source + * @param string $destination + * @param array $uploadMetadata array of metadata to be placed in the + * `Upload-Metadata` header. + * see https://tus.io/protocols/resumable-upload.html#upload-metadata + * Don't Base64 encode the value. + * @param int $noOfChunks + * @param int|null $bytes + * @param string $checksum + * + * @return void + * @throws ConnectionException + * @throws GuzzleException + * @throws JsonException + * @throws ReflectionException + * @throws TusException + */ + public function userUploadsUsingTusAFileTo( + ?string $user, + string $source, + string $destination, + array $uploadMetadata = [], + int $noOfChunks = 1, + int $bytes = null, + string $checksum = '' + ): void { + $user = $this->featureContext->getActualUsername($user); + $password = $this->featureContext->getUserPassword($user); + $headers = [ + 'Authorization' => 'Basic ' . \base64_encode($user . ':' . $password) + ]; + if ($bytes !== null) { + $creationWithUploadHeader = [ + 'Content-Type' => 'application/offset+octet-stream', + 'Tus-Resumable' => '1.0.0' + ]; + $headers = \array_merge($headers, $creationWithUploadHeader); + } + if ($checksum != '') { + $checksumHeader = [ + 'Upload-Checksum' => $checksum + ]; + $headers = \array_merge($headers, $checksumHeader); + } + + $client = new Client( + $this->featureContext->getBaseUrl(), + ['verify' => false, + 'headers' => $headers + ] + ); + $client->setApiPath( + WebDavHelper::getDavPath( + $user, + $this->featureContext->getDavPathVersion(), + "files", + WebDavHelper::$SPACE_ID_FROM_OCIS + ? WebDavHelper::$SPACE_ID_FROM_OCIS + : $this->featureContext->getPersonalSpaceIdForUser($user) + ) + ); + WebDavHelper::$SPACE_ID_FROM_OCIS = ''; + $client->setMetadata($uploadMetadata); + $sourceFile = $this->featureContext->acceptanceTestsDirLocation() . $source; + $client->setKey((string)rand())->file($sourceFile, $destination); + $this->featureContext->pauseUploadDelete(); + + if ($bytes !== null) { + $client->file($sourceFile, $destination)->createWithUpload($client->getKey(), $bytes); + } elseif ($noOfChunks === 1) { + $client->file($sourceFile, $destination)->upload(); + } else { + $bytesPerChunk = (int)\ceil(\filesize($sourceFile) / $noOfChunks); + for ($i = 0; $i < $noOfChunks; $i++) { + $client->upload($bytesPerChunk); + } + } + $this->featureContext->setLastUploadDeleteTime(\time()); + } + + /** + * @When user :user uploads file with content :content to :destination using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $content + * @param string $destination + * + * @return void + * @throws GuzzleException + * @throws Exception + */ + public function userUploadsAFileWithContentToUsingTus( + string $user, + string $content, + string $destination + ): void { + $tmpfname = $this->writeDataToTempFile($content); + try { + $this->userUploadsUsingTusAFileTo( + $user, + \basename($tmpfname), + $destination + ); + } catch (Exception $e) { + Assert::assertStringContainsString('TusPhp\Exception\FileException: Unable to create resource', (string)$e); + } + \unlink($tmpfname); + } + + /** + * @When user :user uploads file with content :content in :noOfChunks chunks to :destination using the TUS protocol on the WebDAV API + * + * @param string|null $user + * @param string $content + * @param int|null $noOfChunks + * @param string $destination + * + * @return void + * @throws ConnectionException + * @throws GuzzleException + * @throws JsonException + * @throws ReflectionException + * @throws TusException + * @throws Exception + * @throws GuzzleException + */ + public function userUploadsAFileWithContentInChunksUsingTus( + ?string $user, + string $content, + ?int $noOfChunks, + string $destination + ): void { + $tmpfname = $this->writeDataToTempFile($content); + $this->userUploadsUsingTusAFileTo( + $user, + \basename($tmpfname), + $destination, + [], + $noOfChunks + ); + \unlink($tmpfname); + } + + /** + * @When user :user uploads file :source to :destination with mtime :mtime using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * @param string $mtime Time in human readable format is taken as input which is converted into milliseconds that is used by API + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function userUploadsFileWithContentToWithMtimeUsingTUS( + string $user, + string $source, + string $destination, + string $mtime + ): void { + $mtime = new DateTime($mtime); + $mtime = $mtime->format('U'); + $user = $this->featureContext->getActualUsername($user); + $this->userUploadsUsingTusAFileTo( + $user, + $source, + $destination, + ['mtime' => $mtime] + ); + } + + /** + * @param string $content + * + * @return string the file name + * @throws Exception + */ + private function writeDataToTempFile(string $content): string { + $tmpfname = \tempnam( + $this->featureContext->acceptanceTestsDirLocation(), + "tus-upload-test-" + ); + if ($tmpfname === false) { + throw new \Exception("could not create a temporary filename"); + } + $tempfile = \fopen($tmpfname, "w"); + if ($tempfile === false) { + throw new \Exception("could not open " . $tmpfname . " for write"); + } + \fwrite($tempfile, $content); + \fclose($tempfile); + return $tmpfname; + } + + /** + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function setUpScenario(BeforeScenarioScope $scope): void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } + + /** + * @When user :user creates a new TUS resource with content :content on the WebDAV API with these headers: + * + * @param string $user + * @param string $content + * @param TableNode $headers + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function userCreatesWithUpload( + string $user, + string $content, + TableNode $headers + ): void { + $this->createNewTUSResourceWithHeaders($user, $headers, $content); + } + + /** + * @When user :user creates file :source and uploads content :content in the same request using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $source + * @param string $content + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function userUploadsWithCreatesWithUpload( + string $user, + string $source, + string $content + ): void { + $tmpfname = $this->writeDataToTempFile($content); + $this->userUploadsUsingTusAFileTo( + $user, + \basename($tmpfname), + $source, + [], + 1, + -1 + ); + \unlink($tmpfname); + } + + /** + * @When user :user uploads file with checksum :checksum to the last created TUS Location with offset :offset and content :content using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $checksum + * @param string $offset + * @param string $content + * + * @return void + * @throws Exception + */ + public function userUploadsFileWithChecksum( + string $user, + string $checksum, + string $offset, + string $content + ): void { + $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $content, $checksum); + } + + /** + * @Given user :user has uploaded file with checksum :checksum to the last created TUS Location with offset :offset and content :content using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $checksum + * @param string $offset + * @param string $content + * + * @return void + * @throws Exception + */ + public function userHasUploadedFileWithChecksum( + string $user, + string $checksum, + string $offset, + string $content + ): void { + $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $content, $checksum); + $this->featureContext->theHTTPStatusCodeShouldBe(204, ""); + } + + /** + * @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $offset + * @param string $data + * @param string $checksum + * + * @return void + * @throws Exception + */ + public function userUploadsChunkFileWithChecksum(string $user, string $offset, string $data, string $checksum): void { + $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum); + } + + /** + * @Given user :user has uploaded a chunk to the last created TUS Location with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API + * + * @param string $user + * @param string $offset + * @param string $data + * @param string $checksum + * + * @return void + * @throws Exception + */ + public function userHasUploadedChunkFileWithChecksum(string $user, string $offset, string $data, string $checksum): void { + $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum); + $this->featureContext->theHTTPStatusCodeShouldBe(204, ""); + } + + /** + * @When user :user overwrites recently shared file with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API with these headers: + * @When user :user overwrites existing file with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API with these headers: + * + * @param string $user + * @param string $offset + * @param string $data + * @param string $checksum + * @param TableNode $headers Tus-Resumable: 1.0.0 header is added automatically + * + * @return void + * + * @throws GuzzleException + * @throws Exception + */ + public function userOverwritesFileWithChecksum(string $user, string $offset, string $data, string $checksum, TableNode $headers): void { + $this->createNewTUSResource($user, $headers); + $this->userHasUploadedChunkFileWithChecksum($user, $offset, $data, $checksum); + } +} diff --git a/tests/acceptance/features/bootstrap/TrashbinContext.php b/tests/acceptance/features/bootstrap/TrashbinContext.php new file mode 100644 index 000000000..ed0e90c81 --- /dev/null +++ b/tests/acceptance/features/bootstrap/TrashbinContext.php @@ -0,0 +1,1182 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @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 + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; +use Psr\Http\Message\ResponseInterface; +use TestHelpers\HttpRequestHelper; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * Trashbin context + */ +class TrashbinContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * + * @var OccContext + */ + private $occContext; + + /** + * @When user :user empties the trashbin using the trashbin API + * + * @param string|null $user user + * + * @return ResponseInterface + */ + public function emptyTrashbin(?string $user):ResponseInterface { + $user = $this->featureContext->getActualUsername($user); + $davPathVersion = $this->featureContext->getDavPathVersion(); + $response = WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + $user, + $this->featureContext->getPasswordForUser($user), + 'DELETE', + null, + [], + $this->featureContext->getStepLineRef(), + null, + $davPathVersion, + 'trash-bin' + ); + + $this->featureContext->setResponse($response); + return $response; + } + + /** + * @Given user :user has emptied the trashbin + * + * @param string $user user + * + * @return void + */ + public function userHasEmptiedTrashbin(string $user):void { + $response = $this->emptyTrashbin($user); + + Assert::assertEquals( + 204, + $response->getStatusCode(), + __METHOD__ . " Expected status code was '204' but got '" . $response->getStatusCode() . "'" + ); + } + + /** + * Get files list from the response from trashbin api + * + * @param SimpleXMLElement|null $responseXml + * + * @return array + */ + public function getTrashbinContentFromResponseXml(?SimpleXMLElement $responseXml): array { + $xmlElements = $responseXml->xpath('//d:response'); + $files = \array_map( + static function (SimpleXMLElement $element) { + $href = $element->xpath('./d:href')[0]; + + $propStats = $element->xpath('./d:propstat'); + $successPropStat = \array_filter( + $propStats, + static function (SimpleXMLElement $propStat) { + $status = $propStat->xpath('./d:status'); + return (string) $status[0] === 'HTTP/1.1 200 OK'; + } + ); + if (isset($successPropStat[0])) { + $successPropStat = $successPropStat[0]; + + $name = $successPropStat->xpath('./d:prop/oc:trashbin-original-filename'); + $mtime = $successPropStat->xpath('./d:prop/oc:trashbin-delete-timestamp'); + $resourcetype = $successPropStat->xpath('./d:prop/d:resourcetype'); + if (\array_key_exists(0, $resourcetype) && ($resourcetype[0]->asXML() === "")) { + $collection[0] = true; + } else { + $collection[0] = false; + } + $originalLocation = $successPropStat->xpath('./d:prop/oc:trashbin-original-location'); + } else { + $name = []; + $mtime = []; + $collection = []; + $originalLocation = []; + } + + return [ + 'href' => (string) $href, + 'name' => isset($name[0]) ? (string) $name[0] : null, + 'mtime' => isset($mtime[0]) ? (string) $mtime[0] : null, + 'collection' => isset($collection[0]) ? $collection[0] : false, + 'original-location' => isset($originalLocation[0]) ? (string) $originalLocation[0] : null + ]; + }, + $xmlElements + ); + + return $files; + } + + /** + * List the top of the trashbin folder for a user + * + * @param string|null $user user + * @param string $depth + * + * @return array response + * @throws Exception + */ + public function listTopOfTrashbinFolder(?string $user, string $depth = "infinity"):array { + $password = $this->featureContext->getPasswordForUser($user); + $davPathVersion = $this->featureContext->getDavPathVersion(); + $response = WebDavHelper::listFolder( + $this->featureContext->getBaseUrl(), + $user, + $password, + "", + $depth, + $this->featureContext->getStepLineRef(), + [ + 'oc:trashbin-original-filename', + 'oc:trashbin-original-location', + 'oc:trashbin-delete-timestamp', + 'd:getlastmodified' + ], + 'trash-bin', + $davPathVersion + ); + $this->featureContext->setResponse($response); + $responseXml = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + + $this->featureContext->setResponseXmlObject($responseXml); + $files = $this->getTrashbinContentFromResponseXml($responseXml); + // filter root element + $files = \array_filter( + $files, + static function ($element) use ($user) { + return ($element['href'] !== "/remote.php/dav/trash-bin/$user/"); + } + ); + return $files; + } + + /** + * List trashbin folder + * + * @param string|null $user user + * @param string $depth + * + * @return array of all the items in the trashbin of the user + * @throws Exception + */ + public function listTrashbinFolder(?string $user, string $depth = "1"):array { + return $this->listTrashbinFolderCollection( + $user, + "", + $depth + ); + } + + /** + * List a collection in the trashbin + * + * @param string|null $user user + * @param string|null $collectionPath the string of ids of the folder and sub-folders + * @param string $depth + * @param int $level + * + * @return array response + * @throws Exception + */ + public function listTrashbinFolderCollection(?string $user, ?string $collectionPath = "", string $depth = "1", int $level = 1):array { + // $collectionPath should be some list of file-ids like 2147497661/2147497662 + // or the empty string, which will list the whole trashbin from the top. + $collectionPath = \trim($collectionPath, "/"); + $password = $this->featureContext->getPasswordForUser($user); + $davPathVersion = $this->featureContext->getDavPathVersion(); + $response = WebDavHelper::listFolder( + $this->featureContext->getBaseUrl(), + $user, + $password, + $collectionPath, + $depth, + $this->featureContext->getStepLineRef(), + [ + 'oc:trashbin-original-filename', + 'oc:trashbin-original-location', + 'oc:trashbin-delete-timestamp', + 'd:resourcetype', + 'd:getlastmodified' + ], + 'trash-bin', + $davPathVersion + ); + $responseXml = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ . " $collectionPath" + ); + + $files = $this->getTrashbinContentFromResponseXml($responseXml); + // filter out the collection itself, we only want to return the members + $files = \array_filter( + $files, + static function ($element) use ($user, $collectionPath) { + $path = $collectionPath; + if ($path !== "") { + $path = $path . "/"; + } + return ($element['href'] !== "/remote.php/dav/trash-bin/$user/$path"); + } + ); + + foreach ($files as $file) { + // check for unexpected/invalid href values and fail early in order to + // avoid "common" situations that could cause infinite recursion. + $trashbinRef = $file["href"]; + $trimmedTrashbinRef = \trim($trashbinRef, "/"); + $expectedStart = "remote.php/dav/trash-bin/$user"; + $expectedStartLength = \strlen($expectedStart); + if ((\substr($trimmedTrashbinRef, 0, $expectedStartLength) !== $expectedStart) + || (\strlen($trimmedTrashbinRef) === $expectedStartLength) + ) { + // A top href (maybe without even the username) has been returned + // in the response. That should never happen, or have been filtered out + // by code above. + throw new Exception( + __METHOD__ . " Error: unexpected href in trashbin propfind at level $level: '$trashbinRef'" + ); + } + if ($file["collection"]) { + $trimmedHref = \trim($trashbinRef, "/"); + $explodedHref = \explode("/", $trimmedHref); + $trashbinId = $collectionPath . "/" . end($explodedHref); + $nextFiles = $this->listTrashbinFolderCollection( + $user, + $trashbinId, + $depth, + $level + 1 + ); + // filter the collection element. We only want the members. + $nextFiles = \array_filter( + $nextFiles, + static function ($element) use ($user, $trashbinRef) { + return ($element['href'] !== $trashbinRef); + } + ); + \array_push($files, ...$nextFiles); + } + } + return $files; + } + + /** + * @When user :user lists the resources in the trashbin with depth :depth using the WebDAV API + * + * @param string $user + * @param string $depth + * + * @return void + * @throws Exception + */ + public function userGetsFilesInTheTrashbinWithDepthUsingTheWebdavApi(string $user, string $depth):void { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + $this->listTopOfTrashbinFolder($user, $depth); + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + } + + /** + * @Then the trashbin DAV response should not contain these nodes + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theTrashbinDavResponseShouldNotContainTheseNodes(TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['name']); + $responseXml = $this->featureContext->getResponseXmlObject(); + $files = $this->getTrashbinContentFromResponseXml($responseXml); + + foreach ($table->getHash() as $row) { + $path = trim((string)$row['name'], "/"); + foreach ($files as $file) { + if (trim((string)$file['original-location'], "/") === $path) { + throw new Exception("file $path was not expected in trashbin response but was found"); + } + } + } + } + + /** + * @Then the trashbin DAV response should contain these nodes + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theTrashbinDavResponseShouldContainTheseNodes(TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['name']); + $responseXml = $this->featureContext->getResponseXmlObject(); + + $files = $this->getTrashbinContentFromResponseXml($responseXml); + + foreach ($table->getHash() as $row) { + $path = trim($row['name'], "/"); + $found = false; + foreach ($files as $file) { + if (trim((string)$file['original-location'], "/") === $path) { + $found = true; + break; + } + } + if (!$found) { + throw new Exception("file $path was expected in trashbin response but was not found"); + } + } + } + + /** + * Send a webdav request to list the trashbin content + * + * @param string $user user + * @param string|null $asUser - To send request as another user + * @param string|null $password + * + * @return void + * @throws Exception + */ + public function sendTrashbinListRequest(string $user, ?string $asUser = null, ?string $password = null):void { + $asUser = $asUser ?? $user; + $password = $password ?? $this->featureContext->getPasswordForUser($asUser); + $davPathVersion = $this->featureContext->getDavPathVersion(); + $response = WebDavHelper::propfind( + $this->featureContext->getBaseUrl(), + $asUser, + $password, + null, + [ + 'oc:trashbin-original-filename', + 'oc:trashbin-original-location', + 'oc:trashbin-delete-timestamp', + 'd:getlastmodified' + ], + $this->featureContext->getStepLineRef(), + '1', + 'trash-bin', + $davPathVersion, + $user + ); + $this->featureContext->setResponse($response); + try { + $responseXmlObject = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $this->featureContext->setResponseXmlObject($responseXmlObject); + } catch (Exception $e) { + $this->featureContext->clearResponseXmlObject(); + } + } + + /** + * @When user :asUser tries to list the trashbin content for user :user + * + * @param string $asUser + * @param string $user + * + * @return void + * @throws Exception + */ + public function userTriesToListTheTrashbinContentForUser(string $asUser, string $user) { + $user = $this->featureContext->getActualUsername($user); + $asUser = $this->featureContext->getActualUsername($asUser); + $this->sendTrashbinListRequest($user, $asUser); + } + + /** + * @When user :asUser tries to list the trashbin content for user :user using password :password + * + * @param string $asUser + * @param string $user + * @param string $password + * + * @return void + * @throws Exception + */ + public function userTriesToListTheTrashbinContentForUserUsingPassword(string $asUser, string $user, string $password):void { + $this->sendTrashbinListRequest($user, $asUser, $password); + } + + /** + * @Then the last webdav response should contain the following elements + * + * @param TableNode $elements + * + * @return void + */ + public function theLastWebdavResponseShouldContainFollowingElements(TableNode $elements):void { + $files = $this->getTrashbinContentFromResponseXml($this->featureContext->getResponseXmlObject()); + if (!($elements instanceof TableNode)) { + throw new InvalidArgumentException( + '$expectedElements has to be an instance of TableNode' + ); + } + $elementRows = $elements->getHash(); + foreach ($elementRows as $expectedElement) { + $found = false; + $expectedPath = $expectedElement['path']; + foreach ($files as $file) { + if (\ltrim($expectedPath, "/") === \ltrim($file['original-location'], "/")) { + $found = true; + break; + } + } + Assert::assertTrue($found, "$expectedPath expected to be listed in response but not found"); + } + } + + /** + * @Then the last webdav response should not contain the following elements + * + * @param TableNode $elements + * + * @return void + * @throws Exception + */ + public function theLastWebdavResponseShouldNotContainFollowingElements(TableNode $elements):void { + $files = $this->getTrashbinContentFromResponseXml($this->featureContext->getResponseXmlObject()); + + // 'user' is also allowed in the table even though it is not used anywhere + // This for better readability in feature files + $this->featureContext->verifyTableNodeColumns($elements, ['path'], ['path', 'user']); + $elementRows = $elements->getHash(); + foreach ($elementRows as $expectedElement) { + $notFound = true; + $expectedPath = "/" . \ltrim($expectedElement['path'], "/"); + foreach ($files as $file) { + // Allow the table of expected elements to have entries that do + // not have to specify the "implied" leading slash, or have multiple + // leading slashes, to make scenario outlines more flexible + if ($expectedPath === $file['original-location']) { + $notFound = false; + } + } + Assert::assertTrue($notFound, "$expectedPath expected not to be listed in response but found"); + } + } + + /** + * @When user :asUser tries to delete the file with original path :path from the trashbin of user :user using the trashbin API + * + * @param string $asUser + * @param string $path + * @param string $user + * + * @return void + * @throws Exception + */ + public function userTriesToDeleteFromTrashbinOfUser(string $asUser, string $path, string $user):void { + $numItemsDeleted = $this->tryToDeleteFileFromTrashbin($user, $path, $asUser); + } + + /** + * @When user :asUser tries to delete the file with original path :path from the trashbin of user :user using the password :password and the trashbin API + * + * @param string|null $asUser + * @param string|null $path + * @param string|null $user + * @param string|null $password + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function userTriesToDeleteFromTrashbinOfUserUsingPassword(?string $asUser, ?string $path, ?string $user, ?string $password):void { + $user = $this->featureContext->getActualUsername($user); + $asUser = $this->featureContext->getActualUsername($asUser); + $numItemsDeleted = $this->tryToDeleteFileFromTrashbin($user, $path, $asUser, $password); + } + + /** + * @When user :asUser tries to restore the file with original path :path from the trashbin of user :user using the trashbin API + * + * @param string|null $asUser + * @param string|null $path + * @param string|null $user + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function userTriesToRestoreFromTrashbinOfUser(?string $asUser, ?string $path, ?string $user):void { + $user = $this->featureContext->getActualUsername($user); + $asUser = $this->featureContext->getActualUsername($asUser); + $this->restoreElement($user, $path, null, true, $asUser); + } + + /** + * @When user :asUser tries to restore the file with original path :path from the trashbin of user :user using the password :password and the trashbin API + * + * @param string|null $asUser + * @param string|null $path + * @param string|null $user + * @param string|null $password + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function userTriesToRestoreFromTrashbinOfUserUsingPassword(?string $asUser, ?string $path, ?string $user, ?string $password):void { + $asUser = $this->featureContext->getActualUsername($asUser); + $user = $this->featureContext->getActualUsername($user); + $this->restoreElement($user, $path, null, true, $asUser, $password); + } + + /** + * converts the trashItemHRef from //remote.php/dav/trash-bin/// to /trash-bin// + * + * @param string $href + * + * @return string + */ + private function convertTrashbinHref(string $href):string { + $trashItemHRef = \trim($href, '/'); + $trashItemHRef = \strstr($trashItemHRef, '/trash-bin'); + $trashItemHRef = \trim($trashItemHRef, '/'); + $parts = \explode('/', $trashItemHRef); + $decodedParts = \array_slice($parts, 2); + return '/' . \join('/', $decodedParts); + } + + /** + * @When /^user "([^"]*)" tries to delete the (?:file|folder|entry) with original path "([^"]*)" from the trashbin using the trashbin API$/ + * + * @param string|null $user + * @param string|null $originalPath + * @param string|null $asUser + * @param string|null $password + * + * @return int the number of items that were matched and requested for delete + * @throws JsonException + * @throws Exception + */ + public function tryToDeleteFileFromTrashbin(?string $user, ?string $originalPath, ?string $asUser = null, ?string $password = null):int { + $user = $this->featureContext->getActualUsername($user); + $asUser = $asUser ?? $user; + $listing = $this->listTrashbinFolder($user); + $originalPath = \trim($originalPath, '/'); + $numItemsDeleted = 0; + + foreach ($listing as $entry) { + if ($entry['original-location'] === $originalPath) { + $trashItemHRef = $this->convertTrashbinHref($entry['href']); + $response = $this->featureContext->makeDavRequest( + $asUser, + 'DELETE', + $trashItemHRef, + [], + null, + 'trash-bin', + null, + false, + $password, + [], + $user + ); + $this->featureContext->setResponse($response); + $numItemsDeleted++; + } + } + + return $numItemsDeleted; + } + + /** + * @When /^user "([^"]*)" deletes the (?:file|folder|entry) with original path "([^"]*)" from the trashbin using the trashbin API$/ + * + * @param string $user + * @param string $originalPath + * + * @return void + * @throws Exception + */ + public function deleteFileFromTrashbin(string $user, string $originalPath):void { + $numItemsDeleted = $this->tryToDeleteFileFromTrashbin($user, $originalPath); + + Assert::assertEquals( + 1, + $numItemsDeleted, + "Expected to delete exactly one item from the trashbin but $numItemsDeleted were deleted" + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" deletes the following (?:files|folders|entries) with original path from the trashbin$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function deleteFollowingFilesFromTrashbin(string $user, TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $path) { + $this->deleteFileFromTrashbin($user, $path["path"]); + + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @Then /^as "([^"]*)" (?:file|folder|entry) "([^"]*)" should exist in the trashbin$/ + * + * @param string|null $user + * @param string|null $path + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function asFileOrFolderExistsInTrash(?string $user, ?string $path):void { + $user = $this->featureContext->getActualUsername($user); + $path = \trim($path, '/'); + $sections = \explode('/', $path, 2); + + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + + $firstEntry = $this->findFirstTrashedEntry($user, \trim($sections[0], '/')); + + Assert::assertNotNull( + $firstEntry, + "The first trash entry was not found while looking for trashbin entry '$path' of user '$user'" + ); + + if (\count($sections) !== 1) { + // TODO: handle deeper structures + $listing = $this->listTrashbinFolderCollection($user, \basename(\rtrim($firstEntry['href'], '/'))); + } + + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + + // query was on the main element ? + if (\count($sections) === 1) { + // already found, return + return; + } + + $checkedName = \basename($path); + + $found = false; + foreach ($listing as $entry) { + if ($entry['name'] === $checkedName) { + $found = true; + break; + } + } + + Assert::assertTrue( + $found, + __METHOD__ + . " Could not find expected resource '$path' in the trash" + ); + } + + /** + * Function to check if an element is in the trashbin + * + * @param string|null $user + * @param string|null $originalPath + * + * @return bool + * @throws Exception + */ + private function isInTrash(?string $user, ?string $originalPath):bool { + $techPreviewHadToBeEnabled = $this->occContext->enableDAVTechPreview(); + $res = $this->featureContext->getResponse(); + $listing = $this->listTrashbinFolder($user); + + $this->featureContext->setResponse($res); + if ($techPreviewHadToBeEnabled) { + $this->occContext->disableDAVTechPreview(); + } + + // we don't care if the test step writes a leading "/" or not + $originalPath = \ltrim($originalPath, '/'); + + foreach ($listing as $entry) { + if ($entry['original-location'] !== null && \ltrim($entry['original-location'], '/') === $originalPath) { + return true; + } + } + return false; + } + + /** + * @param string $user + * @param string $trashItemHRef + * @param string $destinationPath + * @param string|null $asUser - To send request as another user + * @param string|null $password + * + * @return ResponseInterface + */ + private function sendUndeleteRequest(string $user, string $trashItemHRef, string $destinationPath, ?string $asUser = null, ?string $password = null):ResponseInterface { + $asUser = $asUser ?? $user; + $destinationPath = \trim($destinationPath, '/'); + $destinationValue = $this->featureContext->getBaseUrl() . "/remote.php/dav/files/$user/$destinationPath"; + + $trashItemHRef = $this->convertTrashbinHref($trashItemHRef); + $headers['Destination'] = $destinationValue; + $response = $this->featureContext->makeDavRequest( + $asUser, + 'MOVE', + $trashItemHRef, + $headers, + null, + 'trash-bin', + '2', + false, + $password, + [], + $user + ); + $this->featureContext->setResponse($response); + return $response; + } + + /** + * @param string $user + * @param string $originalPath + * @param string|null $destinationPath + * @param bool $throwExceptionIfNotFound + * @param string|null $asUser - To send request as another user + * @param string|null $password + * + * @return ResponseInterface|null + * @throws Exception + */ + private function restoreElement(string $user, string $originalPath, ?string $destinationPath = null, bool $throwExceptionIfNotFound = true, ?string $asUser = null, ?string $password = null):?ResponseInterface { + $asUser = $asUser ?? $user; + $listing = $this->listTrashbinFolder($user); + $originalPath = \trim($originalPath, '/'); + if ($destinationPath === null) { + $destinationPath = $originalPath; + } + foreach ($listing as $entry) { + if ($entry['original-location'] === $originalPath) { + return $this->sendUndeleteRequest( + $user, + $entry['href'], + $destinationPath, + $asUser, + $password + ); + } + } + // The requested element to restore was not even in the trashbin. + // Throw an exception, because there was not any API call, and so there + // is also no up-to-date response to examine in later test steps. + if ($throwExceptionIfNotFound) { + throw new \Exception( + __METHOD__ + . " cannot restore from trashbin because no element was found for user $user at original path $originalPath" + ); + } + return null; + } + + /** + * @When user :user restores the folder with original path :originalPath without specifying the destination using the trashbin API + * + * @param $user string + * @param $originalPath string + * + * @return ResponseInterface + * @throws Exception + */ + public function restoreFileWithoutDestination(string $user, string $originalPath):ResponseInterface { + $asUser = $asUser ?? $user; + $listing = $this->listTrashbinFolder($user); + $originalPath = \trim($originalPath, '/'); + + foreach ($listing as $entry) { + if ($entry['original-location'] === $originalPath) { + $trashItemHRef = $this->convertTrashbinHref($entry['href']); + $response = $this->featureContext->makeDavRequest( + $asUser, + 'MOVE', + $trashItemHRef, + [], + null, + 'trash-bin' + ); + $this->featureContext->setResponse($response); + // this gives empty response in ocis + try { + $responseXml = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + $this->featureContext->setResponseXmlObject($responseXml); + } catch (Exception $e) { + } + + return $response; + } + } + throw new \Exception( + __METHOD__ + . " cannot restore from trashbin because no element was found for user $user at original path $originalPath" + ); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" if the file is also in the trashbin should be "([^"]*)" otherwise "([^"]*)"$/ + * + * Note: this is a special step for an unusual bug combination. + * Delete it when the bug is fixed and the step is no longer needed. + * + * @param string|null $fileName + * @param string|null $user + * @param string|null $content + * @param string|null $alternativeContent + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function contentOfFileForUserIfAlsoInTrashShouldBeOtherwise( + ?string $fileName, + ?string $user, + ?string $content, + ?string $alternativeContent + ):void { + $isInTrash = $this->isInTrash($user, $fileName); + $user = $this->featureContext->getActualUsername($user); + $this->featureContext->downloadFileAsUserUsingPassword($user, $fileName); + if ($isInTrash) { + $this->featureContext->downloadedContentShouldBe($content); + } else { + $this->featureContext->downloadedContentShouldBe($alternativeContent); + } + } + + /** + * @When /^user "([^"]*)" tries to restore the (?:file|folder|entry) with original path "([^"]*)" using the trashbin API$/ + * + * @param string $user + * @param string $originalPath + * + * @return void + * @throws Exception + */ + public function userTriesToRestoreElementInTrash(string $user, string $originalPath):void { + $this->restoreElement($user, $originalPath, null, false); + } + + /** + * @When /^user "([^"]*)" restores the (?:file|folder|entry) with original path "([^"]*)" using the trashbin API$/ + * + * @param string|null $user + * @param string $originalPath + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function elementInTrashIsRestored(?string $user, string $originalPath):void { + $user = $this->featureContext->getActualUsername($user); + $this->restoreElement($user, $originalPath); + } + + /** + * @When /^user "([^"]*)" restores the following (?:files|folders|entries) with original path$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userRestoresFollowingFiles(string $user, TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $originalPath) { + $this->elementInTrashIsRestored($user, $originalPath["path"]); + + $this->featureContext->pushToLastStatusCodesArrays(); + } + } + + /** + * @Given /^user "([^"]*)" has restored the (?:file|folder|entry) with original path "([^"]*)"$/ + * + * @param string $user + * @param string $originalPath + * + * @return void + * @throws Exception + */ + public function elementInTrashHasBeenRestored(string $user, string $originalPath):void { + $this->restoreElement($user, $originalPath); + if ($this->isInTrash($user, $originalPath)) { + throw new Exception("File previously located at $originalPath is still in the trashbin"); + } + } + + /** + * @When /^user "([^"]*)" restores the (?:file|folder|entry) with original path "([^"]*)" to "([^"]*)" using the trashbin API$/ + * + * @param string|null $user + * @param string|null $originalPath + * @param string|null $destinationPath + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function userRestoresTheFileWithOriginalPathToUsingTheTrashbinApi( + ?string $user, + ?string $originalPath, + ?string $destinationPath + ):void { + $user = $this->featureContext->getActualUsername($user); + $this->restoreElement($user, $originalPath, $destinationPath); + } + + /** + * @Then /^as "([^"]*)" the (?:file|folder|entry) with original path "([^"]*)" should exist in the trashbin$/ + * + * @param string|null $user + * @param string|null $originalPath + * + * @return void + * @throws JsonException + * @throws Exception + */ + public function elementIsInTrashCheckingOriginalPath( + ?string $user, + ?string $originalPath + ):void { + $user = $this->featureContext->getActualUsername($user); + Assert::assertTrue( + $this->isInTrash($user, $originalPath), + "File previously located at $originalPath wasn't found in the trashbin of user $user" + ); + } + + /** + * @Then /^as "([^"]*)" the (?:file|folder|entry) with original path "([^"]*)" should not exist in the trashbin/ + * + * @param string|null $user + * @param string $originalPath + * + * @return void + * @throws Exception + */ + public function elementIsNotInTrashCheckingOriginalPath( + ?string $user, + string $originalPath + ):void { + $user = $this->featureContext->getActualUsername($user); + Assert::assertFalse( + $this->isInTrash($user, $originalPath), + "File previously located at $originalPath was found in the trashbin of user $user" + ); + } + + /** + * @Then /^as "([^"]*)" the (?:files|folders|entries) with following original paths should not exist in the trashbin$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function followingElementsAreNotInTrashCheckingOriginalPath( + string $user, + TableNode $table + ):void { + $this->featureContext->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash($table); + + foreach ($paths as $originalPath) { + $this->elementIsNotInTrashCheckingOriginalPath($user, $originalPath["path"]); + } + } + + /** + * @Then /^as "([^"]*)" the (?:files|folders|entries) with following original paths should exist in the trashbin$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function followingElementsAreInTrashCheckingOriginalPath( + string $user, + TableNode $table + ):void { + $this->featureContext->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash($table); + + foreach ($paths as $originalPath) { + $this->elementIsInTrashCheckingOriginalPath($user, $originalPath["path"]); + } + } + + /** + * Finds the first trashed entry matching the given name + * + * @param string $user + * @param string $name + * + * @return array|null real entry name with timestamp suffix or null if not found + * @throws Exception + */ + private function findFirstTrashedEntry(string $user, string $name):?array { + $listing = $this->listTrashbinFolder($user); + + foreach ($listing as $entry) { + if ($entry['name'] === $name) { + return $entry; + } + } + + return null; + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + $this->occContext = $environment->getContext('OccContext'); + } + + /** + * @Then /^the deleted (?:file|folder) "([^"]*)" should have the correct deletion mtime in the response$/ + * + * @param string $resource file or folder in trashbin + * + * @return void + */ + public function theDeletedFileFolderShouldHaveCorrectDeletionMtimeInTheResponse(string $resource):void { + $files = $this->getTrashbinContentFromResponseXml( + $this->featureContext->getResponseXmlObject() + ); + + $found = false; + $expectedMtime = $this->featureContext->getLastUploadDeleteTime(); + $responseMtime = ''; + + foreach ($files as $file) { + if (\ltrim((string)$resource, "/") === \ltrim((string)$file['original-location'], "/")) { + $responseMtime = $file['mtime']; + $mtime_difference = \abs((int)\trim((string)$expectedMtime) - (int)\trim($responseMtime)); + + if ($mtime_difference <= 2) { + $found = true; + break; + } + } + } + Assert::assertTrue( + $found, + "$resource expected to be listed in response with mtime '$expectedMtime' but found '$responseMtime'" + ); + } + + /** + * @Given the administrator has set the following file extensions to be skipped from the trashbin + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetFollowingFileExtensionsToBeSkippedFromTrashbin(TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['extension']); + foreach ($table->getHash() as $idx => $row) { + $this->featureContext->runOcc(['config:system:set', 'trashbin_skip_extensions', $idx, '--value=' . $row['extension']]); + } + } + + /** + * @Given the administrator has set the following directories to be skipped from the trashbin + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetFollowingDirectoriesToBeSkippedFromTrashbin(TableNode $table):void { + $this->featureContext->verifyTableNodeColumns($table, ['directory']); + foreach ($table->getHash() as $idx => $row) { + $this->featureContext->runOcc(['config:system:set', 'trashbin_skip_directories', $idx, '--value=' . $row['directory']]); + } + } + + /** + * @Given the administrator has set the trashbin skip size threshold to :threshold + * + * @param string $threshold + * + * @return void + * @throws Exception + */ + public function theAdministratorHasSetTrashbinSkipSizethreshold(string $threshold) { + $this->featureContext->runOcc(['config:system:set', 'trashbin_skip_size_threshold', '--value=' . $threshold]); + } +} diff --git a/tests/acceptance/features/bootstrap/WebDav.php b/tests/acceptance/features/bootstrap/WebDav.php new file mode 100644 index 000000000..d966840cd --- /dev/null +++ b/tests/acceptance/features/bootstrap/WebDav.php @@ -0,0 +1,5527 @@ + + * + * @copyright Copyright (c) 2018, ownCloud GmbH + * @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 + * + */ + +use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; +use GuzzleHttp\Exception\BadResponseException; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Ring\Exception\ConnectException; +use PHPUnit\Framework\Assert; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Stream\StreamInterface; +use TestHelpers\OcisHelper; +use TestHelpers\OcsApiHelper; +use TestHelpers\SetupHelper; +use TestHelpers\UploadHelper; +use TestHelpers\WebDavHelper; +use TestHelpers\HttpRequestHelper; +use TestHelpers\Asserts\WebDav as WebDavAssert; + +/** + * WebDav functions + */ +trait WebDav { + /** + * @var string + */ + private $davPath = "remote.php/webdav"; + + /** + * @var boolean + */ + private $usingOldDavPath = true; + + /** + * @var boolean + */ + private $usingSpacesDavPath = false; + + /** + * @var ResponseInterface[] + */ + private $uploadResponses; + + /** + * @var integer + */ + private $storedFileID = null; + + /** + * @var int + */ + private $lastUploadDeleteTime = null; + + /** + * a variable that contains the DAV path without "remote.php/(web)dav" + * when setting $this->davPath directly by usingDavPath() + * + * @var string + */ + private $customDavPath = null; + + private $previousAsyncSetting = null; + + private $previousDavSlowdownSetting = null; + + /** + * @var int + */ + private $currentDavSlowdownSettingSeconds = 0; + + /** + * response content parsed from XML to an array + * + * @var array + */ + private $responseXml = []; + + /** + * add resource created by admin in an array + * This array is used while cleaning up the resource created by admin during test run + * As of now it tracks only for (files|folder) creation + * This can be expanded and modified to track other actions like (upload, deleted ..) + * + * @var array + */ + private $adminResources = []; + + /** + * response content parsed into a SimpleXMLElement + * + * @var SimpleXMLElement + */ + private $responseXmlObject; + + private $httpRequestTimeout = 0; + + private $chunkingToUse = null; + + /** + * The ability to do requests with depth infinity is disabled by default. + * This remembers when the setting dav.propfind.depth_infinity has been + * enabled, so that test code can make use of it as appropriate. + * + * @var bool + */ + private $davPropfindDepthInfinityEnabled = false; + + /** + * @return void + */ + public function davPropfindDepthInfinityEnabled():void { + $this->davPropfindDepthInfinityEnabled = true; + } + + /** + * @return void + */ + public function davPropfindDepthInfinityDisabled():void { + $this->davPropfindDepthInfinityEnabled = false; + } + + /** + * @return bool + */ + public function davPropfindDepthInfinityIsEnabled():bool { + return $this->davPropfindDepthInfinityEnabled; + } + + /** + * @param int $lastUploadDeleteTime + * + * @return void + */ + public function setLastUploadDeleteTime(int $lastUploadDeleteTime):void { + $this->lastUploadDeleteTime = $lastUploadDeleteTime; + } + + /** + * @return number + */ + public function getLastUploadDeleteTime():int { + return $this->lastUploadDeleteTime; + } + + /** + * @return SimpleXMLElement|null + */ + public function getResponseXmlObject():?SimpleXMLElement { + return $this->responseXmlObject; + } + + /** + * @param SimpleXMLElement $responseXmlObject + * + * @return void + */ + public function setResponseXmlObject(SimpleXMLElement $responseXmlObject):void { + $this->responseXmlObject = $responseXmlObject; + } + + /** + * @return void + */ + public function clearResponseXmlObject():void { + $this->responseXmlObject = null; + } + + /** + * + * @return string the etag or an empty string if the getetag property does not exist + */ + public function getEtagFromResponseXmlObject():string { + $xmlObject = $this->getResponseXmlObject(); + $xmlPart = $xmlObject->xpath("//d:prop/d:getetag"); + if (!\is_array($xmlPart) || (\count($xmlPart) === 0)) { + return ''; + } + return $xmlPart[0]->__toString(); + } + + /** + * + * @param string|null $eTag if null then get eTag from response XML object + * + * @return boolean + */ + public function isEtagValid(?string $eTag = null):bool { + if ($eTag === null) { + $eTag = $this->getEtagFromResponseXmlObject(); + } + if (\preg_match("/^\"[a-f0-9:\.]{1,32}\"$/", $eTag) + ) { + return true; + } else { + return false; + } + } + + /** + * @param array $responseXml + * + * @return void + */ + public function setResponseXml(array $responseXml):void { + $this->responseXml = $responseXml; + } + + /** + * @param ResponseInterface[] $uploadResponses + * + * @return void + */ + public function setUploadResponses(array $uploadResponses):void { + $this->uploadResponses = $uploadResponses; + } + + /** + * @Given /^using DAV path "([^"]*)"$/ + * + * @param string $davPath + * + * @return void + */ + public function usingDavPath(string $davPath):void { + $this->davPath = $davPath; + $this->customDavPath = \preg_replace( + "/remote\.php\/(web)?dav\//", + "", + $davPath + ); + } + + /** + * @return string + */ + public function getOldDavPath():string { + return "remote.php/webdav"; + } + + /** + * @return string + */ + public function getNewDavPath():string { + return "remote.php/dav"; + } + + /** + * @return string + */ + public function getSpacesDavPath():string { + return "dav/spaces"; + } + + /** + * @Given /^using (old|new|spaces) (?:dav|DAV) path$/ + * + * @param string $davChoice + * + * @return void + */ + public function usingOldOrNewDavPath(string $davChoice):void { + if ($davChoice === 'old') { + $this->usingOldDavPath(); + } elseif ($davChoice === 'new') { + $this->usingNewDavPath(); + } else { + $this->usingSpacesDavPath(); + } + } + + /** + * Select the old DAV path as the default for later scenario steps + * + * @return void + */ + public function usingOldDavPath():void { + $this->davPath = $this->getOldDavPath(); + $this->usingOldDavPath = true; + $this->customDavPath = null; + } + + /** + * Select the new DAV path as the default for later scenario steps + * + * @return void + */ + public function usingNewDavPath():void { + $this->davPath = $this->getNewDavPath(); + $this->usingOldDavPath = false; + $this->customDavPath = null; + $this->usingSpacesDavPath = false; + } + + /** + * Select the spaces dav path as the default for later scenario steps + * + * @return void + */ + public function usingSpacesDavPath():void { + $this->davPath = $this->getSpacesDavPath(); + $this->usingOldDavPath = false; + $this->customDavPath = null; + $this->usingSpacesDavPath = true; + } + + /** + * gives the DAV path of a file including the subfolder of the webserver + * e.g. when the server runs in `http://localhost/owncloud/` + * this function will return `owncloud/remote.php/webdav/prueba.txt` + * + * @param string $user + * + * @return string + * @throws GuzzleException + */ + public function getFullDavFilesPath(string $user):string { + $spaceId = null; + if ($this->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES) { + $spaceId = (WebDavHelper::$SPACE_ID_FROM_OCIS) ? WebDavHelper::$SPACE_ID_FROM_OCIS : WebDavHelper::getPersonalSpaceIdForUser( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + $this->getStepLineRef() + ); + } + $path = $this->getBasePath() . "/" . + WebDavHelper::getDavPath($user, $this->getDavPathVersion(), "files", $spaceId); + $path = WebDavHelper::sanitizeUrl($path); + return \ltrim($path, "/"); + } + + /** + * @param string $token + * @param string $type + * + * @return string + */ + public function getPublicLinkDavPath(string $token, string $type):string { + $path = $this->getBasePath() . "/" . + WebDavHelper::getDavPath($token, $this->getDavPathVersion(), $type); + $path = WebDavHelper::sanitizeUrl($path); + return \ltrim($path, "/"); + } + + /** + * Select a suitable DAV path version number. + * Some endpoints have only existed since a certain point in time, so for + * those make sure to return a DAV path version that works for that endpoint. + * Otherwise return the currently selected DAV path version. + * + * @param string|null $for the category of endpoint that the DAV path will be used for + * + * @return int DAV path version (1, 2 or 3) selected, or appropriate for the endpoint + */ + public function getDavPathVersion(?string $for = null):?int { + if ($this->usingSpacesDavPath) { + return WebDavHelper::DAV_VERSION_SPACES; + } + if ($for === 'systemtags') { + // systemtags only exists since DAV v2 + return WebDavHelper::DAV_VERSION_NEW; + } + if ($for === 'file_versions') { + // file_versions only exists since DAV v2 + return WebDavHelper::DAV_VERSION_NEW; + } + if ($this->usingOldDavPath === true) { + return WebDavHelper::DAV_VERSION_OLD; + } else { + return WebDavHelper::DAV_VERSION_NEW; + } + } + + /** + * Select a suitable DAV path. + * Some endpoints have only existed since a certain point in time, so for + * those make sure to return a DAV path that works for that endpoint. + * Otherwise return the currently selected DAV path. + * + * @param string|null $for the category of endpoint that the DAV path will be used for + * + * @return string DAV path selected, or appropriate for the endpoint + */ + public function getDavPath(?string $for = null):string { + $davPathVersion = $this->getDavPathVersion($for); + if ($davPathVersion === WebDavHelper::DAV_VERSION_OLD) { + return $this->getOldDavPath(); + } + + if ($davPathVersion === WebDavHelper::DAV_VERSION_NEW) { + return $this->getNewDavPath(); + } + + return $this->getSpacesDavPath(); + } + + /** + * @param string|null $user + * @param string|null $method + * @param string|null $path + * @param array|null $headers + * @param StreamInterface|null $body + * @param string|null $type + * @param string|null $davPathVersion + * @param bool $stream Set to true to stream a response rather + * than download it all up-front. + * @param string|null $password + * @param array|null $urlParameter + * @param string|null $doDavRequestAsUser + * + * @return ResponseInterface + * @throws GuzzleException|JsonException + */ + public function makeDavRequest( + ?string $user, + ?string $method, + ?string $path, + ?array $headers, + $body = null, + ?string $type = "files", + ?string $davPathVersion = null, + bool $stream = false, + ?string $password = null, + ?array $urlParameter = [], + ?string $doDavRequestAsUser = null + ):ResponseInterface { + $user = $this->getActualUsername($user); + if ($this->customDavPath !== null) { + $path = $this->customDavPath . $path; + } + + if ($davPathVersion === null) { + $davPathVersion = $this->getDavPathVersion(); + } else { + $davPathVersion = (int) $davPathVersion; + } + + if ($password === null) { + $password = $this->getPasswordForUser($user); + } + + return WebDavHelper::makeDavRequest( + $this->getBaseUrl(), + $user, + $password, + $method, + $path, + $headers, + $this->getStepLineRef(), + $body, + $davPathVersion, + $type, + null, + "basic", + $stream, + $this->httpRequestTimeout, + null, + $urlParameter, + $doDavRequestAsUser + ); + } + + /** + * @param string $user + * @param string|null $path + * @param string|null $doDavRequestAsUser + * @param string|null $width + * @param string|null $height + * + * @return void + */ + public function downloadPreviews(string $user, ?string $path, ?string $doDavRequestAsUser, ?string $width, ?string $height):void { + $user = $this->getActualUsername($user); + $doDavRequestAsUser = $this->getActualUsername($doDavRequestAsUser); + $urlParameter = [ + 'x' => $width, + 'y' => $height, + 'forceIcon' => '0', + 'preview' => '1' + ]; + $this->response = $this->makeDavRequest( + $user, + "GET", + $path, + [], + null, + "files", + '2', + false, + null, + $urlParameter, + $doDavRequestAsUser + ); + } + + /** + * @Then the number of versions should be :arg1 + * + * @param int $number + * + * @return void + * @throws Exception + */ + public function theNumberOfVersionsShouldBe(int $number):void { + $resXml = $this->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->getResponse(), + __METHOD__ + ); + $this->setResponseXmlObject($resXml); + } + $xmlPart = $resXml->xpath("//d:getlastmodified"); + $actualNumber = \count($xmlPart); + Assert::assertEquals( + $number, + $actualNumber, + "Expected number of versions was '$number', but got '$actualNumber'" + ); + } + + /** + * @Then the number of etag elements in the response should be :number + * + * @param int $number + * + * @return void + * @throws Exception + */ + public function theNumberOfEtagElementInTheResponseShouldBe(int $number):void { + $resXml = $this->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->getResponse(), + __METHOD__ + ); + } + $xmlPart = $resXml->xpath("//d:getetag"); + $actualNumber = \count($xmlPart); + Assert::assertEquals( + $number, + $actualNumber, + "Expected number of etag elements was '$number', but got '$actualNumber'" + ); + } + + /** + * @Given /^the administrator has (enabled|disabled) async operations$/ + * + * @param string $enabledOrDisabled + * + * @return void + * @throws Exception + */ + public function triggerAsyncUpload(string $enabledOrDisabled):void { + $switch = ($enabledOrDisabled !== "disabled"); + if ($switch) { + $value = 'true'; + } else { + $value = 'false'; + } + if ($this->previousAsyncSetting === null) { + $previousAsyncSetting = SetupHelper::runOcc( + ['config:system:get', 'dav.enable.async'], + $this->getStepLineRef() + )['stdOut']; + $this->previousAsyncSetting = \trim($previousAsyncSetting); + } + $this->runOcc( + [ + 'config:system:set', + 'dav.enable.async', + '--type', + 'boolean', + '--value', + $value + ] + ); + } + + /** + * @Given the HTTP-Request-timeout is set to :seconds seconds + * + * @param int $timeout + * + * @return void + */ + public function setHttpTimeout(int $timeout):void { + $this->httpRequestTimeout = (int) $timeout; + } + + /** + * @Given the :method DAV requests are slowed down by :seconds seconds + * + * @param string $method + * @param int $seconds + * + * @return void + * @throws Exception + */ + public function slowdownDavRequests(string $method, int $seconds):void { + if ($this->previousDavSlowdownSetting === null) { + $previousDavSlowdownSetting = SetupHelper::runOcc( + ['config:system:get', 'dav.slowdown'], + $this->getStepLineRef() + )['stdOut']; + $this->previousDavSlowdownSetting = \trim($previousDavSlowdownSetting); + } + OcsApiHelper::sendRequest( + $this->getBaseUrl(), + $this->getAdminUsername(), + $this->getAdminPassword(), + "PUT", + "/apps/testing/api/v1/davslowdown/$method/$seconds", + $this->getStepLineRef() + ); + $this->currentDavSlowdownSettingSeconds = $seconds; + } + + /** + * Wait for possible slowed-down DAV requests to finish + * + * @return void + */ + public function waitForDavRequestsToFinish():void { + if ($this->currentDavSlowdownSettingSeconds > 0) { + // There could be a slowed-down request still happening on the server + // Wait just-in-case so that we do not accidentally have an effect on + // the next scenario. + \sleep($this->currentDavSlowdownSettingSeconds); + } + } + + /** + * @param string $user + * @param string $fileDestination + * + * @return string + * @throws GuzzleException + */ + public function destinationHeaderValue(string $user, string $fileDestination):string { + $spaceId = $this->getPersonalSpaceIdForUser($user); + $fullUrl = $this->getBaseUrl() . '/' . + WebDavHelper::getDavPath($user, $this->getDavPathVersion(), "files", $spaceId); + return \rtrim($fullUrl, '/') . '/' . \ltrim($fileDestination, '/'); + } + + /** + * @Given /^user "([^"]*)" has moved (?:file|folder|entry) "([^"]*)" to "([^"]*)"$/ + * + * @param string|null $user + * @param string|null $fileSource + * @param string|null $fileDestination + * + * @return void + */ + public function userHasMovedFile( + ?string $user, + ?string $fileSource, + ?string $fileDestination + ):void { + $user = $this->getActualUsername($user); + $headers['Destination'] = $this->destinationHeaderValue( + $user, + $fileDestination + ); + $this->response = $this->makeDavRequest( + $user, + "MOVE", + $fileSource, + $headers + ); + $expectedStatusCode = 201; + $actualStatusCode = $this->response->getStatusCode(); + Assert::assertEquals( + $expectedStatusCode, + $actualStatusCode, + __METHOD__ . " Failed moving resource '$fileSource' to '$fileDestination'." + . " Expected status code was '$expectedStatusCode' but got '$actualStatusCode'" + ); + } + + /** + * @Given /^the user has moved (?:file|folder|entry) "([^"]*)" to "([^"]*)"$/ + * + * @param string $fileSource + * @param string $fileDestination + * + * @return void + */ + public function theUserHasMovedFile(string $fileSource, string $fileDestination):void { + $this->userHasMovedFile($this->getCurrentUser(), $fileSource, $fileDestination); + } + + /** + * @When /^user "([^"]*)" moves (file|folder|entry) "([^"]*)"\s?(asynchronously|) to these (?:filenames|foldernames|entries) using the webDAV API then the results should be as listed$/ + * + * @param string $user + * @param string $entry + * @param string $fileSource + * @param string $type "asynchronously" or empty + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userMovesEntriesUsingTheAPI( + string $user, + string $entry, + string $fileSource, + string $type, + TableNode $table + ):void { + $user = $this->getActualUsername($user); + foreach ($table->getHash() as $row) { + // Allow the "filename" column to be optionally be called "foldername" + // to help readability of scenarios that test moving folders + if (isset($row['foldername'])) { + $targetName = $row['foldername']; + } else { + $targetName = $row['filename']; + } + $this->userMovesFileUsingTheAPI( + $user, + $fileSource, + $type, + $targetName + ); + $this->theHTTPStatusCodeShouldBe( + $row['http-code'], + "HTTP status code is not the expected value while trying to move " . $targetName + ); + if ($row['exists'] === "yes") { + $this->asFileOrFolderShouldExist($user, $entry, $targetName); + // The move was successful. + // Move the file/folder back so the source file/folder exists for the next move + $this->userMovesFileUsingTheAPI( + $user, + $targetName, + '', + $fileSource + ); + } else { + $this->asFileOrFolderShouldNotExist($user, $entry, $targetName); + } + } + } + + /** + * @When /^user "([^"]*)" moves (?:file|folder|entry) "([^"]*)"\s?(asynchronously|) to "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $fileSource + * @param string $type "asynchronously" or empty + * @param string $fileDestination + * + * @return void + * @throws JsonException + * @throws GuzzleException + */ + public function userMovesFileUsingTheAPI( + string $user, + string $fileSource, + string $type, + string $fileDestination + ):void { + $user = $this->getActualUsername($user); + $headers['Destination'] = $this->destinationHeaderValue( + $user, + $fileDestination + ); + $stream = false; + if ($type === "asynchronously") { + $headers['OC-LazyOps'] = 'true'; + if ($this->httpRequestTimeout > 0) { + //LazyOps is set and a request timeout, so we want to use stream + //to be able to read data from the request before its times out + //when doing LazyOps the server does not close the connection + //before its really finished + //but we want to read JobStatus-Location before the end of the job + //to see if it reports the correct values + $stream = true; + } + } + try { + $this->response = $this->makeDavRequest( + $user, + "MOVE", + $fileSource, + $headers, + null, + "files", + null, + $stream + ); + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + $this->pushToLastHttpStatusCodesArray( + (string) $this->getResponse()->getStatusCode() + ); + } catch (ConnectException $e) { + } + } + + /** + * @When user :user moves the following file using the WebDAV API + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userMovesTheFollowingFileUsingTheWebdavApi(string $user, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["source", "destination"]); + $rows = $table->getHash(); + foreach ($rows as $row) { + $this->userMovesFileUsingTheAPI($user, $row["source"], "", $row["destination"]); + } + } + + /** + * @When /^user "([^"]*)" moves the following (?:files|folders|entries)\s?(asynchronously|) using the WebDAV API$/ + * + * @param string $user + * @param string $type "asynchronously" or empty + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userMovesFollowingFileUsingTheAPI( + string $user, + string $type, + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["from", "to"]); + $paths = $table->getHash(); + + foreach ($paths as $file) { + $this->userMovesFileUsingTheAPI($user, $file['from'], $type, $file['to']); + $this->pushToLastStatusCodesArrays(); + } + } + + /** + * @Then /^user "([^"]*)" should be able to rename (file|folder|entry) "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $entry + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function theUserShouldBeAbleToRenameEntryTo(string $user, string $entry, string $source, string $destination):void { + $user = $this->getActualUsername($user); + $this->asFileOrFolderShouldExist($user, $entry, $source); + $this->userMovesFileUsingTheAPI($user, $source, "", $destination); + $this->asFileOrFolderShouldNotExist($user, $entry, $source); + $this->asFileOrFolderShouldExist($user, $entry, $destination); + } + + /** + * @Then /^user "([^"]*)" should not be able to rename (file|folder|entry) "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $entry + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function theUserShouldNotBeAbleToRenameEntryTo(string $user, string $entry, string $source, string $destination):void { + $this->asFileOrFolderShouldExist($user, $entry, $source); + $this->userMovesFileUsingTheAPI($user, $source, "", $destination); + $this->asFileOrFolderShouldExist($user, $entry, $source); + $this->asFileOrFolderShouldNotExist($user, $entry, $destination); + } + + /** + * @When /^user "([^"]*)" on "(LOCAL|REMOTE)" moves (?:file|folder|entry) "([^"]*)" to "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $server + * @param string $fileSource + * @param string $fileDestination + * + * @return void + */ + public function userOnMovesFileUsingTheAPI( + string $user, + string $server, + string $fileSource, + string $fileDestination + ):void { + $previousServer = $this->usingServer($server); + $this->userMovesFileUsingTheAPI($user, $fileSource, "", $fileDestination); + $this->usingServer($previousServer); + } + + /** + * @When /^user "([^"]*)" copies (?:file|folder) "([^"]*)" to "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $fileSource + * @param string $fileDestination + * + * @return void + */ + public function userCopiesFileUsingTheAPI( + string $user, + string $fileSource, + string $fileDestination + ):void { + $user = $this->getActualUsername($user); + $headers['Destination'] = $this->destinationHeaderValue( + $user, + $fileDestination + ); + $this->response = $this->makeDavRequest( + $user, + "COPY", + $fileSource, + $headers + ); + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + $this->pushToLastHttpStatusCodesArray( + (string) $this->getResponse()->getStatusCode() + ); + } + + /** + * @Given /^user "([^"]*)" has copied file "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $fileSource + * @param string $fileDestination + * + * @return void + */ + public function userHasCopiedFileUsingTheAPI( + string $user, + string $fileSource, + string $fileDestination + ):void { + $this->userCopiesFileUsingTheAPI($user, $fileSource, $fileDestination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to copy file '$fileSource' to '$fileDestination' for user '$user'" + ); + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @When /^the user copies file "([^"]*)" to "([^"]*)" using the WebDAV API$/ + * + * @param string $fileSource + * @param string $fileDestination + * + * @return void + */ + public function theUserCopiesFileUsingTheAPI(string $fileSource, string $fileDestination):void { + $this->userCopiesFileUsingTheAPI($this->getCurrentUser(), $fileSource, $fileDestination); + } + + /** + * @Given /^the user has copied file "([^"]*)" to "([^"]*)"$/ + * + * @param string $fileSource + * @param string $fileDestination + * + * @return void + */ + public function theUserHasCopiedFileUsingTheAPI(string $fileSource, string $fileDestination):void { + $this->theUserCopiesFileUsingTheAPI($fileSource, $fileDestination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to copy file '$fileSource' to '$fileDestination'" + ); + } + + /** + * @When /^the user downloads file "([^"]*)" with range "([^"]*)" using the WebDAV API$/ + * + * @param string $fileSource + * @param string $range + * + * @return void + */ + public function downloadFileWithRange(string $fileSource, string $range):void { + $this->userDownloadsFileWithRange( + $this->currentUser, + $fileSource, + $range + ); + } + + /** + * @When /^user "([^"]*)" downloads file "([^"]*)" with range "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $fileSource + * @param string $range + * + * @return void + */ + public function userDownloadsFileWithRange(string $user, string $fileSource, string $range):void { + $user = $this->getActualUsername($user); + $headers['Range'] = $range; + $this->response = $this->makeDavRequest( + $user, + "GET", + $fileSource, + $headers + ); + } + + /** + * @Then /^user "([^"]*)" using password "([^"]*)" should not be able to download file "([^"]*)"$/ + * + * @param string $user + * @param string $password + * @param string $fileName + * + * @return void + */ + public function userUsingPasswordShouldNotBeAbleToDownloadFile( + string $user, + string $password, + string $fileName + ):void { + $user = $this->getActualUsername($user); + $password = $this->getActualPassword($password); + $this->downloadFileAsUserUsingPassword($user, $fileName, $password); + Assert::assertGreaterThanOrEqual( + 400, + $this->getResponse()->getStatusCode(), + __METHOD__ + . ' download must fail' + ); + Assert::assertLessThanOrEqual( + 499, + $this->getResponse()->getStatusCode(), + __METHOD__ + . ' 4xx error expected but got status code "' + . $this->getResponse()->getStatusCode() . '"' + ); + } + + /** + * @Then /^user "([^"]*)" should not be able to download file "([^"]*)"$/ + * + * @param string $user + * @param string $fileName + * + * @return void + * @throws JsonException + */ + public function userShouldNotBeAbleToDownloadFile( + string $user, + string $fileName + ):void { + $user = $this->getActualUsername($user); + $password = $this->getPasswordForUser($user); + $this->downloadFileAsUserUsingPassword($user, $fileName, $password); + Assert::assertGreaterThanOrEqual( + 400, + $this->getResponse()->getStatusCode(), + __METHOD__ + . ' download must fail' + ); + Assert::assertLessThanOrEqual( + 499, + $this->getResponse()->getStatusCode(), + __METHOD__ + . ' 4xx error expected but got status code "' + . $this->getResponse()->getStatusCode() . '"' + ); + } + + /** + * @Then /^user "([^"]*)" should be able to access a skeleton file$/ + * + * @param string $user + * + * @return void + */ + public function userShouldBeAbleToAccessASkeletonFile(string $user):void { + $this->contentOfFileForUserShouldBePlusEndOfLine( + "textfile0.txt", + $user, + "ownCloud test text file 0" + ); + } + + /** + * @Then the size of the downloaded file should be :size bytes + * + * @param string $size + * + * @return void + */ + public function sizeOfDownloadedFileShouldBe(string $size):void { + $actualSize = \strlen((string) $this->response->getBody()); + Assert::assertEquals( + $size, + $actualSize, + "Expected size of the downloaded file was '$size' but got '$actualSize'" + ); + } + + /** + * @Then /^the downloaded content should end with "([^"]*)"$/ + * + * @param string $content + * + * @return void + */ + public function downloadedContentShouldEndWith(string $content):void { + $actualContent = \substr((string) $this->response->getBody(), -\strlen($content)); + Assert::assertEquals( + $content, + $actualContent, + "The downloaded content was expected to end with '$content', but actually ended with '$actualContent'." + ); + } + + /** + * @Then /^the downloaded content should be "([^"]*)"$/ + * + * @param string $content + * + * @return void + */ + public function downloadedContentShouldBe(string $content):void { + $this->checkDownloadedContentMatches($content); + } + + /** + * @param string $expectedContent + * @param string $extraErrorText + * + * @return void + */ + public function checkDownloadedContentMatches( + string $expectedContent, + string $extraErrorText = "" + ):void { + $actualContent = (string) $this->response->getBody(); + // For this test we really care about the content. + // A separate "Then" step can specifically check the HTTP status. + // But if the content is wrong (e.g. empty) then it is useful to + // report the HTTP status to give some clue what might be the problem. + $actualStatus = $this->response->getStatusCode(); + if ($extraErrorText !== "") { + $extraErrorText .= "\n"; + } + Assert::assertEquals( + $expectedContent, + $actualContent, + $extraErrorText . "The content was expected to be '$expectedContent', but actually is '$actualContent'. HTTP status was $actualStatus" + ); + } + + /** + * @Then the content in the response should match the following content: + * + * @param PyStringNode $content + * + * @return void + */ + public function theContentInTheResponseShouldMatchTheFollowingContent(PyStringNode $content): void { + $this->checkDownloadedContentMatches($content->getRaw()); + } + + /** + * @Then /^if the HTTP status code was "([^"]*)" then the downloaded content for multipart byterange should be:$/ + * + * @param int $statusCode + * @param PyStringNode $content + * + * @return void + * + */ + public function theDownloadedContentForMultipartByterangeShouldBe(int $statusCode, PyStringNode $content):void { + $actualStatusCode = $this->response->getStatusCode(); + if ($actualStatusCode === $statusCode) { + $actualContent = (string) $this->response->getBody(); + $pattern = ["/--\w*/", "/\s*/m"]; + $actualContent = \preg_replace($pattern, "", $actualContent); + $content = \preg_replace("/\s*/m", '', $content->getRaw()); + Assert::assertEquals( + $content, + $actualContent, + "The downloaded content was expected to be '$content', but actually is '$actualContent'. HTTP status was $actualStatusCode" + ); + } + } + + /** + * @Then /^if the HTTP status code was "([^"]*)" then the downloaded content should be "([^"]*)"$/ + * + * @param int $statusCode + * @param string $content + * + * @return void + */ + public function checkStatusCodeForDownloadedContentShouldBe(int $statusCode, string $content):void { + $actualStatusCode = $this->response->getStatusCode(); + if ($actualStatusCode === $statusCode) { + $this->downloadedContentShouldBe($content); + } + } + + /** + * @Then /^the downloaded content should be "([^"]*)" plus end-of-line$/ + * + * @param string $content + * + * @return void + */ + public function downloadedContentShouldBePlusEndOfLine(string $content):void { + $this->downloadedContentShouldBe("$content\n"); + } + + /** + * @Then /^the content of file "([^"]*)" should be "([^"]*)"$/ + * + * @param string $fileName + * @param string $content + * + * @return void + */ + public function contentOfFileShouldBe(string $fileName, string $content):void { + $this->theUserDownloadsTheFileUsingTheAPI($fileName); + $this->downloadedContentShouldBe($content); + } + + /** + * @Then /^the content of file "([^"]*)" should be:$/ + * + * @param string $fileName + * @param PyStringNode $content + * + * @return void + */ + public function contentOfFileShouldBePyString( + string $fileName, + PyStringNode $content + ):void { + $this->contentOfFileShouldBe($fileName, $content->getRaw()); + } + + /** + * @Then /^the content of file "([^"]*)" should be "([^"]*)" plus end-of-line$/ + * + * @param string $fileName + * @param string $content + * + * @return void + */ + public function contentOfFileShouldBePlusEndOfLine(string $fileName, string $content):void { + $this->theUserDownloadsTheFileUsingTheAPI($fileName); + $this->downloadedContentShouldBePlusEndOfLine($content); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $fileName + * @param string $user + * @param string $content + * + * @return void + */ + public function contentOfFileForUserShouldBe(string $fileName, string $user, string $content):void { + $user = $this->getActualUsername($user); + $this->downloadFileAsUserUsingPassword($user, $fileName); + $this->downloadedContentShouldBe($content); + } + + /** + * @Then /^the content of the following files for user "([^"]*)" should be "([^"]*)"$/ + * + * @param string $user + * @param string $content + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function contentOfFollowingFilesShouldBe(string $user, string $content, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $file) { + $this->contentOfFileForUserShouldBe($file["path"], $user, $content); + } + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" on server "([^"]*)" should be "([^"]*)"$/ + * + * @param string $fileName + * @param string $user + * @param string $server + * @param string $content + * + * @return void + */ + public function theContentOfFileForUserOnServerShouldBe( + string $fileName, + string $user, + string $server, + string $content + ):void { + $previousServer = $this->usingServer($server); + $this->contentOfFileForUserShouldBe($fileName, $user, $content); + $this->usingServer($previousServer); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" using password "([^"]*)" should be "([^"]*)"$/ + * + * @param string $fileName + * @param string $user + * @param string|null $password + * @param string $content + * + * @return void + */ + public function contentOfFileForUserUsingPasswordShouldBe( + string $fileName, + string $user, + ?string $password, + string $content + ):void { + $user = $this->getActualUsername($user); + $password = $this->getActualPassword($password); + $this->downloadFileAsUserUsingPassword($user, $fileName, $password); + $this->downloadedContentShouldBe($content); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" should be:$/ + * + * @param string $fileName + * @param string $user + * @param PyStringNode $content + * + * @return void + */ + public function contentOfFileForUserShouldBePyString( + string $fileName, + string $user, + PyStringNode $content + ):void { + $this->contentOfFileForUserShouldBe($fileName, $user, $content->getRaw()); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" using password "([^"]*)" should be:$/ + * + * @param string $fileName + * @param string $user + * @param string|null $password + * @param PyStringNode $content + * + * @return void + */ + public function contentOfFileForUserUsingPasswordShouldBePyString( + string $fileName, + string $user, + ?string $password, + PyStringNode $content + ):void { + $this->contentOfFileForUserUsingPasswordShouldBe( + $fileName, + $user, + $password, + $content->getRaw() + ); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" should be "([^"]*)" plus end-of-line$/ + * + * @param string $fileName + * @param string $user + * @param string $content + * + * @return void + */ + public function contentOfFileForUserShouldBePlusEndOfLine(string $fileName, string $user, string $content):void { + $this->contentOfFileForUserShouldBe( + $fileName, + $user, + "$content\n" + ); + } + + /** + * @Then the content of the following files for user :user should be the following plus end-of-line + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theContentOfTheFollowingFilesForUserShouldBeTheFollowingPlusEndOfLine(string $user, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["filename", "content"]); + $rows = $table->getHash(); + foreach ($rows as $row) { + $this->contentOfFileForUserShouldBePlusEndOfLine($row["filename"], $user, $row["content"]); + } + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" on server "([^"]*)" should be "([^"]*)" plus end-of-line$/ + * + * @param string $fileName + * @param string $user + * @param string $server + * @param string $content + * + * @return void + */ + public function theContentOfFileForUserOnServerShouldBePlusEndOfLine( + string $fileName, + string $user, + string $server, + string $content + ):void { + $previousServer = $this->usingServer($server); + $this->contentOfFileForUserShouldBePlusEndOfLine($fileName, $user, $content); + $this->usingServer($previousServer); + } + + /** + * @Then /^the content of file "([^"]*)" for user "([^"]*)" using password "([^"]*)" should be "([^"]*)" plus end-of-line$/ + * + * @param string $fileName + * @param string $user + * @param string|null $password + * @param string $content + * + * @return void + */ + public function contentOfFileForUserUsingPasswordShouldBePlusEndOfLine( + string $fileName, + string $user, + ?string $password, + string $content + ):void { + $user = $this->getActualUsername($user); + $this->contentOfFileForUserUsingPasswordShouldBe( + $fileName, + $user, + $password, + "$content\n" + ); + } + + /** + * @Then /^the downloaded content when downloading file "([^"]*)" with range "([^"]*)" should be "([^"]*)"$/ + * + * @param string $fileSource + * @param string $range + * @param string $content + * + * @return void + */ + public function downloadedContentWhenDownloadingWithRangeShouldBe( + string $fileSource, + string $range, + string $content + ):void { + $this->downloadFileWithRange($fileSource, $range); + $this->downloadedContentShouldBe($content); + } + + /** + * @Then /^the downloaded content when downloading file "([^"]*)" for user "([^"]*)" with range "([^"]*)" should be "([^"]*)"$/ + * + * @param string $fileSource + * @param string $user + * @param string $range + * @param string $content + * + * @return void + */ + public function downloadedContentWhenDownloadingForUserWithRangeShouldBe( + string $fileSource, + string $user, + string $range, + string $content + ):void { + $user = $this->getActualUsername($user); + $this->userDownloadsFileWithRange($user, $fileSource, $range); + $this->downloadedContentShouldBe($content); + } + + /** + * @When the user downloads the file :fileName using the WebDAV API + * + * @param string $fileName + * + * @return void + */ + public function theUserDownloadsTheFileUsingTheAPI(string $fileName):void { + $this->downloadFileAsUserUsingPassword($this->currentUser, $fileName); + } + + /** + * @When user :user downloads file :fileName using the WebDAV API + * + * @param string $user + * @param string $fileName + * + * @return void + */ + public function userDownloadsFileUsingTheAPI( + string $user, + string $fileName + ):void { + $this->downloadFileAsUserUsingPassword($user, $fileName); + } + + /** + * @When user :user using password :password downloads the file :fileName using the WebDAV API + * + * @param string $user + * @param string|null $password + * @param string $fileName + * + * @return void + */ + public function userUsingPasswordDownloadsTheFileUsingTheAPI( + string $user, + ?string $password, + string $fileName + ):void { + $this->downloadFileAsUserUsingPassword($user, $fileName, $password); + } + + /** + * @param string $user + * @param string $fileName + * @param string|null $password + * @param array|null $headers + * + * @return void + */ + public function downloadFileAsUserUsingPassword( + string $user, + string $fileName, + ?string $password = null, + ?array $headers = [] + ):void { + $user = $this->getActualUsername($user); + $password = $this->getActualPassword($password); + $this->response = $this->makeDavRequest( + $user, + 'GET', + $fileName, + $headers, + null, + "files", + null, + false, + $password + ); + } + + /** + * @When the public gets the size of the last shared public link using the WebDAV API + * + * @return void + * @throws Exception + */ + public function publicGetsSizeOfLastSharedPublicLinkUsingTheWebdavApi():void { + $tokenArray = $this->getLastPublicShareData()->data->token; + $token = (string)$tokenArray[0]; + $url = $this->getBaseUrl() . "/remote.php/dav/public-files/{$token}"; + $this->response = HttpRequestHelper::sendRequest( + $url, + $this->getStepLineRef(), + "PROPFIND", + null, + null, + null + ); + } + + /** + * @When user :user gets the size of file :resource using the WebDAV API + * + * @param string $user + * @param string $resource + * + * @return void + * @throws Exception + */ + public function userGetsSizeOfFileUsingTheWebdavApi(string $user, string $resource):void { + $user = $this->getActualUsername($user); + $password = $this->getPasswordForUser($user); + $this->response = WebDavHelper::propfind( + $this->getBaseUrl(), + $user, + $password, + $resource, + [], + $this->getStepLineRef(), + "0", + "files", + $this->getDavPathVersion() + ); + } + + /** + * @Then the size of the file should be :size + * + * @param string $size + * + * @return void + * @throws Exception + */ + public function theSizeOfTheFileShouldBe(string $size):void { + $responseXml = HttpRequestHelper::getResponseXml( + $this->response, + __METHOD__ + ); + $xmlPart = $responseXml->xpath("//d:prop/d:getcontentlength"); + $actualSize = (string) $xmlPart[0]; + Assert::assertEquals( + $size, + $actualSize, + __METHOD__ + . " Expected size of the file was '$size', but got '$actualSize' instead." + ); + } + + /** + * @Then the following headers should be set + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingHeadersShouldBeSet(TableNode $table):void { + $this->verifyTableNodeColumns( + $table, + ['header', 'value'] + ); + foreach ($table->getColumnsHash() as $header) { + $headerName = $header['header']; + $expectedHeaderValue = $header['value']; + $returnedHeader = $this->response->getHeader($headerName); + $expectedHeaderValue = $this->substituteInLineCodes($expectedHeaderValue); + + if (\is_array($returnedHeader)) { + if (empty($returnedHeader)) { + throw new Exception( + \sprintf( + "Missing expected header '%s'", + $headerName + ) + ); + } + $headerValue = $returnedHeader[0]; + } else { + $headerValue = $returnedHeader; + } + + Assert::assertEquals( + $expectedHeaderValue, + $headerValue, + __METHOD__ + . " Expected value for header '$headerName' was '$expectedHeaderValue', but got '$headerValue' instead." + ); + } + } + + /** + * @Then the downloaded content should start with :start + * + * @param string $start + * + * @return void + * @throws Exception + */ + public function downloadedContentShouldStartWith(string $start):void { + Assert::assertEquals( + 0, + \strpos($this->response->getBody()->getContents(), $start), + __METHOD__ + . " The downloaded content was expected to start with '$start', but actually started with '{$this->response->getBody()->getContents()}'" + ); + } + + /** + * @Then the oc job status values of last request for user :user should match these regular expressions + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function jobStatusValuesShouldMatchRegEx(string $user, TableNode $table):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumnsCount($table, 2); + $headerArray = $this->response->getHeader("OC-JobStatus-Location"); + $url = $headerArray[0]; + $url = $this->getBaseUrlWithoutPath() . $url; + $response = HttpRequestHelper::get( + $url, + $this->getStepLineRef(), + $user, + $this->getPasswordForUser($user) + ); + $contents = $response->getBody()->getContents(); + $result = \json_decode($contents, true); + PHPUnit\Framework\Assert::assertNotNull($result, "'$contents' is not valid JSON"); + foreach ($table->getTable() as $row) { + $expectedKey = $row[0]; + Assert::assertArrayHasKey( + $expectedKey, + $result, + "response does not have expected key '$expectedKey'" + ); + $expectedValue = $this->substituteInLineCodes( + $row[1], + $user, + ['preg_quote' => ['/']] + ); + Assert::assertNotFalse( + (bool) \preg_match($expectedValue, (string)$result[$expectedKey]), + "'$expectedValue' does not match '$result[$expectedKey]'" + ); + } + } + + /** + * @Then /^as "([^"]*)" (file|folder|entry) "([^"]*)" should not exist$/ + * + * @param string $user + * @param string $entry + * @param string|null $path + * @param string $type + * + * @return ResponseInterface + * @throws Exception + */ + public function asFileOrFolderShouldNotExist( + string $user, + string $entry = "file", + ?string $path = null, + string $type = "files" + ):ResponseInterface { + $user = $this->getActualUsername($user); + $path = $this->substituteInLineCodes($path); + $response = $this->listFolder( + $user, + $path, + '0', + null, + $type + ); + $statusCode = $response->getStatusCode(); + if ($statusCode < 401 || $statusCode > 404) { + try { + $this->responseXmlObject = HttpRequestHelper::getResponseXml( + $response, + __METHOD__ + ); + } catch (Exception $e) { + Assert::fail( + "$entry '$path' should not exist. But API returned $statusCode without XML in the body" + ); + } + Assert::assertTrue( + $this->isEtagValid(), + "$entry '$path' should not exist. But API returned $statusCode without an etag in the body" + ); + $isCollection = $this->getResponseXmlObject()->xpath("//d:prop/d:resourcetype/d:collection"); + if (\count($isCollection) === 0) { + $actualResourceType = "file"; + } else { + $actualResourceType = "folder"; + } + Assert::fail( + "$entry '$path' should not exist. But it does exist and is a $actualResourceType" + ); + } + return $response; + } + + /** + * @Then /^as "([^"]*)" the following (files|folders) should not exist$/ + * + * @param string $user + * @param string $entry + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function followingFilesShouldNotExist( + string $user, + string $entry, + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + $entry = \rtrim($entry, "s"); + + foreach ($paths as $file) { + $this->asFileOrFolderShouldNotExist($user, $entry, $file["path"]); + } + } + + /** + * @Then /^as "([^"]*)" (file|folder|entry) "([^"]*)" should exist$/ + * + * @param string $user + * @param string $entry + * @param string $path + * @param string $type + * + * @return void + * @throws Exception + */ + public function asFileOrFolderShouldExist( + string $user, + string $entry, + string $path, + string $type = "files" + ):void { + $user = $this->getActualUsername($user); + $path = $this->substituteInLineCodes($path); + $this->responseXmlObject = $this->listFolderAndReturnResponseXml( + $user, + $path, + '0', + null, + $type + ); + Assert::assertTrue( + $this->isEtagValid(), + "$entry '$path' expected to exist for user $user but not found" + ); + $isCollection = $this->getResponseXmlObject()->xpath("//d:prop/d:resourcetype/d:collection"); + if ($entry === "folder") { + Assert::assertEquals(\count($isCollection), 1, "Unexpectedly, `$path` is not a folder"); + } elseif ($entry === "file") { + Assert::assertEquals(\count($isCollection), 0, "Unexpectedly, `$path` is not a file"); + } + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @Then /^as "([^"]*)" the following (files|folders) should exist$/ + * + * @param string $user + * @param string $entry + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function followingFilesOrFoldersShouldExist( + string $user, + string $entry, + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + $entry = \rtrim($entry, "s"); + + foreach ($paths as $file) { + $this->asFileOrFolderShouldExist($user, $entry, $file["path"]); + } + } + + /** + * + * @param string $user + * @param string $entry + * @param string $path + * @param string $type + * + * @return bool + */ + public function fileOrFolderExists( + string $user, + string $entry, + string $path, + string $type = "files" + ):bool { + try { + $this->asFileOrFolderShouldExist($user, $entry, $path, $type); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * @Then /^as "([^"]*)" exactly one of these (files|folders|entries) should exist$/ + * + * @param string $user + * @param string $entries + * @param TableNode $table of file, folder or entry paths + * + * @return void + * @throws Exception + */ + public function asExactlyOneOfTheseFilesOrFoldersShouldExist(string $user, string $entries, TableNode $table):void { + $numEntriesThatExist = 0; + foreach ($table->getTable() as $row) { + $path = $this->substituteInLineCodes($row[0]); + $this->responseXmlObject = $this->listFolderAndReturnResponseXml( + $user, + $path, + '0' + ); + if ($this->isEtagValid()) { + $numEntriesThatExist = $numEntriesThatExist + 1; + } + } + Assert::assertEquals( + 1, + $numEntriesThatExist, + "exactly one of these $entries should exist but found $numEntriesThatExist $entries" + ); + } + + /** + * + * @param string $user + * @param string $path + * @param string $folderDepth requires 1 to see elements without children + * @param array|null $properties + * @param string $type + * + * @return ResponseInterface + * @throws Exception + */ + public function listFolder( + string $user, + string $path, + string $folderDepth, + ?array $properties = null, + string $type = "files" + ):ResponseInterface { + if ($this->customDavPath !== null) { + $path = $this->customDavPath . $path; + } + + return WebDavHelper::listFolder( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getPasswordForUser($user), + $path, + $folderDepth, + $this->getStepLineRef(), + $properties, + $type, + $this->getDavPathVersion() + ); + } + + /** + * + * @param string $user + * @param string $path + * @param string $folderDepth requires 1 to see elements without children + * @param array|null $properties + * @param string $type + * + * @return SimpleXMLElement + * @throws Exception + */ + public function listFolderAndReturnResponseXml( + string $user, + string $path, + string $folderDepth, + ?array $properties = null, + string $type = "files" + ):SimpleXMLElement { + return HttpRequestHelper::getResponseXml( + $this->listFolder( + $user, + $path, + $folderDepth, + $properties, + $type + ), + __METHOD__ + ); + } + + /** + * @Then /^user "([^"]*)" should (not|)\s?see the following elements$/ + * + * @param string $user + * @param string $shouldOrNot + * @param TableNode $elements + * + * @return void + * @throws InvalidArgumentException|Exception + * + */ + public function userShouldSeeTheElements(string $user, string $shouldOrNot, TableNode $elements):void { + $should = ($shouldOrNot !== "not"); + $this->checkElementList($user, $elements, $should); + } + + /** + * @Then /^user "([^"]*)" should not see the following elements if the upper and lower case username are different/ + * + * @param string $user + * @param TableNode $elements + * + * @return void + * @throws InvalidArgumentException|Exception + * + */ + public function userShouldNotSeeTheElementsIfUpperAndLowerCaseUsernameDifferent(string $user, TableNode $elements):void { + $effectiveUser = $this->getActualUsername($user); + if (\strtoupper($effectiveUser) === \strtolower($effectiveUser)) { + $expectedToBeListed = true; + } else { + $expectedToBeListed = false; + } + $this->checkElementList($user, $elements, $expectedToBeListed); + } + + /** + * asserts that the user can or cannot see a list of files/folders by propfind + * + * @param string $user + * @param TableNode $elements + * @param boolean $expectedToBeListed + * + * @return void + * @throws InvalidArgumentException + * @throws Exception + * + */ + public function checkElementList( + string $user, + TableNode $elements, + bool $expectedToBeListed = true + ):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumnsCount($elements, 1); + $elementRows = $elements->getRows(); + $elementsSimplified = $this->simplifyArray($elementRows); + if ($this->davPropfindDepthInfinityIsEnabled()) { + // get a full "infinite" list of the user's root folder in one request + // and process that to check the elements (resources) + $responseXmlObject = $this->listFolderAndReturnResponseXml( + $user, + "/", + "infinity" + ); + foreach ($elementsSimplified as $expectedElement) { + // Allow the table of expected elements to have entries that do + // not have to specify the "implied" leading slash, or have multiple + // leading slashes, to make scenario outlines more flexible + $expectedElement = $this->encodePath($expectedElement); + $expectedElement = "/" . \ltrim($expectedElement, "/"); + $webdavPath = "/" . $this->getFullDavFilesPath($user) . $expectedElement; + $element = $responseXmlObject->xpath( + "//d:response/d:href[text() = \"$webdavPath\"]" + ); + if ($expectedToBeListed + && (!isset($element[0]) || urldecode($element[0]->__toString()) !== urldecode($webdavPath)) + ) { + Assert::fail( + "$webdavPath is not in propfind answer but should be" + ); + } elseif (!$expectedToBeListed && isset($element[0]) + ) { + Assert::fail( + "$webdavPath is in propfind answer but should not be" + ); + } + } + } else { + // do a PROPFIND for each element + foreach ($elementsSimplified as $elementToRequest) { + // Allow the table of expected elements to have entries that do + // not have to specify the "implied" leading slash, or have multiple + // leading slashes, to make scenario outlines more flexible + $elementToRequest = "/" . \ltrim($elementToRequest, "/"); + // Note: in the request we ask to do a PROPFIND on a resource like: + // /some-folder with spaces/sub-folder + // but the response has encoded values for the special characters like: + // /some-folder%20with%20spaces/sub-folder + // So we need both $elementToRequest and $expectedElement + $expectedElement = $this->encodePath($elementToRequest); + $responseXmlObject = $this->listFolderAndReturnResponseXml( + $user, + $elementToRequest, + "1" + ); + $webdavPath = "/" . $this->getFullDavFilesPath($user) . $expectedElement; + $element = $responseXmlObject->xpath( + "//d:response/d:href[text() = \"$webdavPath\"]" + ); + if ($expectedToBeListed + && (!isset($element[0]) || urldecode($element[0]->__toString()) !== urldecode($webdavPath)) + ) { + Assert::fail( + "$webdavPath is not in propfind answer but should be" + ); + } elseif (!$expectedToBeListed && isset($element[0]) + ) { + Assert::fail( + "$webdavPath is in propfind answer but should not be" + ); + } + } + } + } + + /** + * @When user :user uploads file :source to :destination using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + */ + public function userUploadsAFileTo(string $user, string $source, string $destination):void { + $user = $this->getActualUsername($user); + $file = \fopen($this->acceptanceTestsDirLocation() . $source, 'r'); + $this->pauseUploadDelete(); + $this->response = $this->makeDavRequest( + $user, + "PUT", + $destination, + [], + $file + ); + $this->lastUploadDeleteTime = \time(); + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + $this->pushToLastHttpStatusCodesArray( + (string) $this->getResponse()->getStatusCode() + ); + } + + /** + * @Given user :user has uploaded file :source to :destination + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + */ + public function userHasUploadedAFileTo(string $user, string $source, string $destination):void { + $this->userUploadsAFileTo($user, $source, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$source' to '$destination' for user '$user'" + ); + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @When the user uploads file :source to :destination using the WebDAV API + * + * @param string $source + * @param string $destination + * + * @return void + */ + public function theUserUploadsAFileTo(string $source, string $destination):void { + $this->userUploadsAFileTo($this->currentUser, $source, $destination); + } + + /** + * @Given the user has uploaded file :source to :destination + * + * @param string $source + * @param string $destination + * + * @return void + */ + public function theUserHasUploadedFileTo(string $source, string $destination):void { + $this->theUserUploadsAFileTo($source, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$source' to '$destination'" + ); + } + + /** + * @When /^user "([^"]*)" on "(LOCAL|REMOTE)" uploads file "([^"]*)" to "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $server + * @param string $source + * @param string $destination + * + * @return void + */ + public function userOnUploadsAFileTo(string $user, string $server, string $source, string $destination):void { + $previousServer = $this->usingServer($server); + $this->userUploadsAFileTo($user, $source, $destination); + $this->usingServer($previousServer); + } + + /** + * @Given /^user "([^"]*)" on "(LOCAL|REMOTE)" has uploaded file "([^"]*)" to "([^"]*)"$/ + * + * @param string $user + * @param string $server + * @param string $source + * @param string $destination + * + * @return void + */ + public function userOnHasUploadedAFileTo(string $user, string $server, string $source, string $destination):void { + $this->userOnUploadsAFileTo($user, $server, $source, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$source' to '$destination' for user '$user' on server '$server'" + ); + } + + /** + * Upload file as a user with different headers + * + * @param string $user + * @param string $source + * @param string $destination + * @param array|null $headers + * @param int|null $noOfChunks Only use for chunked upload when $this->chunkingToUse is not null + * + * @return void + * @throws Exception + */ + public function uploadFileWithHeaders( + string $user, + string $source, + string $destination, + ?array $headers = [], + ?int $noOfChunks = 0 + ):void { + $chunkingVersion = $this->chunkingToUse; + if ($noOfChunks <= 0) { + $chunkingVersion = null; + } + try { + $this->responseXml = []; + $this->pauseUploadDelete(); + $this->response = UploadHelper::upload( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getUserPassword($user), + $source, + $destination, + $this->getStepLineRef(), + $headers, + $this->getDavPathVersion(), + $chunkingVersion, + $noOfChunks + ); + $this->lastUploadDeleteTime = \time(); + } catch (BadResponseException $e) { + // 4xx and 5xx responses cause an exception + $this->response = $e->getResponse(); + } + } + + /** + * @When /^user "([^"]*)" uploads file "([^"]*)" to "([^"]*)" in (\d+) chunks (?:with (new|old|v1|v2) chunking and)?\s?using the WebDAV API$/ + * @When user :user uploads file :source to :destination with chunks using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * @param int $noOfChunks + * @param string|null $chunkingVersion old|v1|new|v2 null for autodetect + * @param bool $async use asynchronous move at the end or not + * @param array|null $headers + * + * @return void + * @throws Exception + */ + public function userUploadsAFileToWithChunks( + string $user, + string $source, + string $destination, + int $noOfChunks = 2, + ?string $chunkingVersion = null, + bool $async = false, + ?array $headers = [] + ):void { + $user = $this->getActualUsername($user); + Assert::assertGreaterThan( + 0, + $noOfChunks, + "What does it mean to have $noOfChunks chunks?" + ); + //use the chunking version that works with the set DAV version + if ($chunkingVersion === null) { + if ($this->usingOldDavPath || $this->usingSpacesDavPath) { + $chunkingVersion = "v1"; + } else { + $chunkingVersion = "v2"; + } + } + $this->useSpecificChunking($chunkingVersion); + Assert::assertTrue( + WebDavHelper::isValidDavChunkingCombination( + $this->getDavPathVersion(), + $this->chunkingToUse + ), + "invalid chunking/webdav version combination" + ); + + if ($async === true) { + $headers['OC-LazyOps'] = 'true'; + } + $this->uploadFileWithHeaders( + $user, + $this->acceptanceTestsDirLocation() . $source, + $destination, + $headers, + $noOfChunks + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @When /^user "([^"]*)" uploads file "([^"]*)" asynchronously to "([^"]*)" in (\d+) chunks (?:with (new|old|v1|v2) chunking and)?\s?using the WebDAV API$/ + * + * @param string $user + * @param string $source + * @param string $destination + * @param int $noOfChunks + * @param string|null $chunkingVersion old|v1|new|v2 null for autodetect + * + * @return void + * @throws Exception + */ + public function userUploadsAFileAsyncToWithChunks( + string $user, + string $source, + string $destination, + int $noOfChunks = 2, + ?string $chunkingVersion = null + ):void { + $user = $this->getActualUsername($user); + $this->userUploadsAFileToWithChunks( + $user, + $source, + $destination, + $noOfChunks, + $chunkingVersion, + true + ); + } + + /** + * sets the chunking version from human readable format + * + * @param string $version (no|v1|v2|new|old) + * + * @return void + */ + public function useSpecificChunking(string $version):void { + if ($version === "v1" || $version === "old") { + $this->chunkingToUse = 1; + } elseif ($version === "v2" || $version === "new") { + $this->chunkingToUse = 2; + } elseif ($version === "no") { + $this->chunkingToUse = null; + } else { + throw new InvalidArgumentException( + "cannot set chunking version to $version" + ); + } + } + + /** + * Uploading with old/new DAV and chunked/non-chunked. + * + * @When user :user uploads file :source to filenames based on :destination with all mechanisms using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userUploadsAFileToWithAllMechanisms( + string $user, + string $source, + string $destination + ):void { + $user = $this->getActualUsername($user); + $this->uploadResponses = UploadHelper::uploadWithAllMechanisms( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getUserPassword($user), + $this->acceptanceTestsDirLocation() . $source, + $destination, + $this->getStepLineRef() + ); + } + + /** + * Uploading with old/new DAV and chunked/non-chunked. + * Except do not do the new-DAV-new-chunking combination. That is not being + * supported on all implementations. + * + * @When user :user uploads file :source to filenames based on :destination with all mechanisms except new chunking using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userUploadsAFileToWithAllMechanismsExceptNewChunking( + string $user, + string $source, + string $destination + ):void { + $user = $this->getActualUsername($user); + $this->uploadResponses = UploadHelper::uploadWithAllMechanisms( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getUserPassword($user), + $this->acceptanceTestsDirLocation() . $source, + $destination, + $this->getStepLineRef(), + false, + 'new' + ); + + $this->pushToLastStatusCodesArrays(); + } + + /** + * Overwriting with old/new DAV and chunked/non-chunked. + * + * @When user :user overwrites from file :source to file :destination with all mechanisms using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userOverwritesAFileToWithAllMechanisms( + string $user, + string $source, + string $destination + ):void { + $user = $this->getActualUsername($user); + $this->uploadResponses = UploadHelper::uploadWithAllMechanisms( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getUserPassword($user), + $this->acceptanceTestsDirLocation() . $source, + $destination, + $this->getStepLineRef(), + true + ); + } + + /** + * Overwriting with old/new DAV and chunked/non-chunked. + * Except do not do the new-DAV-new-chunking combination. That is not being + * supported on all implementations. + * + * @When user :user overwrites from file :source to file :destination with all mechanisms except new chunking using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userOverwritesAFileToWithAllMechanismsExceptNewChunking( + string $user, + string $source, + string $destination + ):void { + $user = $this->getActualUsername($user); + $this->uploadResponses = UploadHelper::uploadWithAllMechanisms( + $this->getBaseUrl(), + $this->getActualUsername($user), + $this->getUserPassword($user), + $this->acceptanceTestsDirLocation() . $source, + $destination, + $this->getStepLineRef(), + true, + 'new' + ); + } + + /** + * @Then /^the HTTP status code of all upload responses should be "([^"]*)"$/ + * + * @param int $statusCode + * + * @return void + */ + public function theHTTPStatusCodeOfAllUploadResponsesShouldBe(int $statusCode):void { + foreach ($this->uploadResponses as $response) { + Assert::assertEquals( + $statusCode, + $response->getStatusCode(), + 'Response did not return expected status code' + ); + } + } + + /** + * @Then the HTTP status code of responses on all endpoints should be :statusCode + * + * @param int $statusCode + * + * @return void + * @throws Exception + */ + public function theHTTPStatusCodeOfResponsesOnAllEndpointsShouldBe(int $statusCode):void { + $duplicateRemovedStatusCodes = \array_unique($this->lastHttpStatusCodesArray); + if (\count($duplicateRemovedStatusCodes) === 1) { + Assert::assertSame( + \intval($statusCode), + \intval($duplicateRemovedStatusCodes[0]), + 'Responses did not return expected http status code' + ); + $this->emptyLastHTTPStatusCodesArray(); + } else { + throw new Exception( + 'Expected same but found different http status codes of last requested responses.' . + 'Found status codes: ' . \implode(',', $this->lastHttpStatusCodesArray) + ); + } + } + + /** + * @Then the HTTP status code of responses on each endpoint should be :statusCode respectively + * + * @param string $statusCodes + * + * @return void + * @throws Exception + */ + public function theHTTPStatusCodeOfResponsesOnEachEndpointShouldBe(string $statusCodes):void { + $statusCodes = \explode(',', $statusCodes); + $count = \count($statusCodes); + if ($count === \count($this->lastHttpStatusCodesArray)) { + for ($i = 0; $i < $count; $i++) { + Assert::assertSame( + (int)\trim($statusCodes[$i]), + (int)$this->lastHttpStatusCodesArray[$i], + 'Responses did not return expected HTTP status code' + ); + } + $this->emptyLastHTTPStatusCodesArray(); + } else { + throw new Exception( + 'Expected HTTP status codes: "' . \implode(',', $statusCodes) . + '". Found HTTP status codes: "' . \implode(',', $this->lastHttpStatusCodesArray) . '"' + ); + } + } + + /** + * @Then the OCS status code of responses on each endpoint should be :statusCode respectively + * + * @param string $statusCodes + * + * @return void + * @throws Exception + */ + public function theOCStatusCodeOfResponsesOnEachEndpointShouldBe(string $statusCodes):void { + $statusCodes = \explode(',', $statusCodes); + $count = \count($statusCodes); + if ($count === \count($this->lastOCSStatusCodesArray)) { + for ($i = 0; $i < $count; $i++) { + Assert::assertSame( + (int)\trim($statusCodes[$i]), + (int)$this->lastOCSStatusCodesArray[$i], + 'Responses did not return expected OCS status code' + ); + } + } else { + throw new Exception( + 'Expected OCS status codes: "' . \implode(',', $statusCodes) . + '". Found OCS status codes: "' . \implode(',', $this->lastOCSStatusCodesArray) . '"' + ); + } + } + + /** + * @Then the HTTP status code of responses on all endpoints should be :statusCode1 or :statusCode2 + * + * @param string $statusCode1 + * @param string $statusCode2 + * + * @return void + * @throws Exception + */ + public function theHTTPStatusCodeOfResponsesOnAllEndpointsShouldBeOr(string $statusCode1, string $statusCode2):void { + $duplicateRemovedStatusCodes = \array_unique($this->lastHttpStatusCodesArray); + foreach ($duplicateRemovedStatusCodes as $status) { + $status = (string)$status; + if (($status != $statusCode1) && ($status != $statusCode2)) { + Assert::fail("Unexpected status code received " . $status); + } + } + } + + /** + * @Then the OCS status code of responses on all endpoints should be :statusCode + * + * @param string $statusCode + * + * @return void + * @throws Exception + */ + public function theOCSStatusCodeOfResponsesOnAllEndpointsShouldBe(string $statusCode):void { + $duplicateRemovedStatusCodes = \array_unique($this->lastOCSStatusCodesArray); + if (\count($duplicateRemovedStatusCodes) === 1) { + Assert::assertSame( + \intval($statusCode), + \intval($duplicateRemovedStatusCodes[0]), + 'Responses did not return expected ocs status code' + ); + $this->emptyLastOCSStatusCodesArray(); + } else { + throw new Exception( + 'Expected same but found different ocs status codes of last requested responses.' . + 'Found status codes: ' . \implode(',', $this->lastOCSStatusCodesArray) + ); + } + } + + /** + * @Then /^the HTTP reason phrase of all upload responses should be "([^"]*)"$/ + * + * @param string $reasonPhrase + * + * @return void + */ + public function theHTTPReasonPhraseOfAllUploadResponsesShouldBe(string $reasonPhrase):void { + foreach ($this->uploadResponses as $response) { + Assert::assertEquals( + $reasonPhrase, + $response->getReasonPhrase(), + 'Response did not return expected reason phrase' + ); + } + } + + /** + * @Then user :user should be able to upload file :source to :destination + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userShouldBeAbleToUploadFileTo(string $user, string $source, string $destination):void { + $user = $this->getActualUsername($user); + $this->userUploadsAFileTo($user, $source, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$destination'" + ); + $this->asFileOrFolderShouldExist($user, "file", $destination); + } + + /** + * @Then the following users should be able to upload file :source to :destination + * + * @param string $source + * @param string $destination + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function usersShouldBeAbleToUploadFileTo( + string $source, + string $destination, + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["username"]); + $usernames = $table->getHash(); + foreach ($usernames as $username) { + $actualUser = $this->getActualUsername($username["username"]); + $this->userUploadsAFileTo($actualUser, $source, $destination); + $this->asFileOrFolderShouldExist($actualUser, "file", $destination); + } + } + + /** + * @Then user :user should not be able to upload file :source to :destination + * + * @param string $user + * @param string $source + * @param string $destination + * + * @return void + * @throws Exception + */ + public function theUserShouldNotBeAbleToUploadFileTo(string $user, string $source, string $destination):void { + $fileAlreadyExists = $this->fileOrFolderExists($user, "file", $destination); + if ($fileAlreadyExists) { + $this->downloadFileAsUserUsingPassword($user, $destination); + $initialContent = (string) $this->response->getBody(); + } + $this->userUploadsAFileTo($user, $source, $destination); + $this->theHTTPStatusCodeShouldBe(["403", "423"]); + if ($fileAlreadyExists) { + $this->downloadFileAsUserUsingPassword($user, $destination); + $currentContent = (string) $this->response->getBody(); + Assert::assertSame( + $initialContent, + $currentContent, + __METHOD__ . " user $user was unexpectedly able to upload $source to $destination - the content has changed:" + ); + } else { + $this->asFileOrFolderShouldNotExist($user, "file", $destination); + } + } + + /** + * @Then /^the HTTP status code of all upload responses should be between "(\d+)" and "(\d+)"$/ + * + * @param int $minStatusCode + * @param int $maxStatusCode + * + * @return void + */ + public function theHTTPStatusCodeOfAllUploadResponsesShouldBeBetween( + int $minStatusCode, + int $maxStatusCode + ):void { + foreach ($this->uploadResponses as $response) { + Assert::assertGreaterThanOrEqual( + $minStatusCode, + $response->getStatusCode(), + 'Response did not return expected status code' + ); + Assert::assertLessThanOrEqual( + $maxStatusCode, + $response->getStatusCode(), + 'Response did not return expected status code' + ); + } + } + + /** + * Check that all the files uploaded with old/new DAV and chunked/non-chunked exist. + * + * @Then /^as "([^"]*)" the files uploaded to "([^"]*)" with all mechanisms should (not|)\s?exist$/ + * + * @param string $user + * @param string $destination + * @param string $shouldOrNot + * @param string|null $exceptChunkingType empty string or "old" or "new" + * + * @return void + * @throws Exception + */ + public function filesUploadedToWithAllMechanismsShouldExist( + string $user, + string $destination, + string $shouldOrNot, + ?string $exceptChunkingType = '' + ):void { + switch ($exceptChunkingType) { + case 'old': + $exceptChunkingSuffix = 'olddav-oldchunking'; + break; + case 'new': + $exceptChunkingSuffix = 'newdav-newchunking'; + break; + default: + $exceptChunkingSuffix = ''; + break; + } + + if ($shouldOrNot !== "not") { + foreach (['old', 'new'] as $davVersion) { + foreach (["{$davVersion}dav-regular", "{$davVersion}dav-{$davVersion}chunking"] as $suffix) { + if ($suffix !== $exceptChunkingSuffix) { + $this->asFileOrFolderShouldExist( + $user, + 'file', + "$destination-$suffix" + ); + } + } + } + } else { + foreach (['old', 'new'] as $davVersion) { + foreach (["{$davVersion}dav-regular", "{$davVersion}dav-{$davVersion}chunking"] as $suffix) { + if ($suffix !== $exceptChunkingSuffix) { + $this->asFileOrFolderShouldNotExist( + $user, + 'file', + "$destination-$suffix" + ); + } + } + } + } + } + + /** + * Check that all the files uploaded with old/new DAV and chunked/non-chunked exist. + * Except do not check the new-DAV-new-chunking combination. That is not being + * supported on all implementations. + * + * @Then /^as "([^"]*)" the files uploaded to "([^"]*)" with all mechanisms except new chunking should (not|)\s?exist$/ + * + * @param string $user + * @param string $destination + * @param string $shouldOrNot + * + * @return void + * @throws Exception + */ + public function filesUploadedToWithAllMechanismsExceptNewChunkingShouldExist( + string $user, + string $destination, + string $shouldOrNot + ):void { + $this->filesUploadedToWithAllMechanismsShouldExist( + $user, + $destination, + $shouldOrNot, + 'new' + ); + } + + /** + * @Then /^as user "([^"]*)" on server "([^"]*)" the files uploaded to "([^"]*)" with all mechanisms should (not|)\s?exist$/ + * + * @param string $user + * @param string $server + * @param string $destination + * @param string $shouldOrNot + * + * @return void + * @throws Exception + */ + public function asUserOnServerTheFilesUploadedToWithAllMechanismsShouldExit( + string $user, + string $server, + string $destination, + string $shouldOrNot + ):void { + $previousServer = $this->usingServer($server); + $this->filesUploadedToWithAllMechanismsShouldExist($user, $destination, $shouldOrNot); + $this->usingServer($previousServer); + } + + /** + * @Given user :user has uploaded file :destination of size :bytes bytes + * + * @param string $user + * @param string $destination + * @param string $bytes + * + * @return void + * @throws Exception + */ + public function userHasUploadedFileToOfSizeBytes(string $user, string $destination, string $bytes):void { + $user = $this->getActualUsername($user); + $this->userUploadsAFileToOfSizeBytes($user, $destination, $bytes); + $expectedElements = new TableNode([["$destination"]]); + $this->checkElementList($user, $expectedElements); + } + + /** + * @When user :user uploads file :destination of size :bytes bytes + * + * @param string $user + * @param string $destination + * @param string $bytes + * + * @return void + */ + public function userUploadsAFileToOfSizeBytes(string $user, string $destination, string $bytes):void { + $this->userUploadsAFileToEndingWithOfSizeBytes($user, $destination, 'a', $bytes); + } + + /** + * @Given user :user has uploaded file :destination ending with :text of size :bytes bytes + * + * @param string $user + * @param string $destination + * @param string $text + * @param string $bytes + * + * @return void + * @throws Exception + */ + public function userHasUploadedFileToEndingWithOfSizeBytes(string $user, string $destination, string $text, string $bytes):void { + $this->userUploadsAFileToEndingWithOfSizeBytes($user, $destination, $text, $bytes); + $expectedElements = new TableNode([["$destination"]]); + $this->checkElementList($user, $expectedElements); + } + + /** + * @When user :user uploads file :destination ending with :text of size :bytes bytes + * + * @param string $user + * @param string $destination + * @param string $text + * @param string $bytes + * + * @return void + */ + public function userUploadsAFileToEndingWithOfSizeBytes(string $user, string $destination, string $text, string $bytes):void { + $filename = "filespecificSize.txt"; + $this->createLocalFileOfSpecificSize($filename, $bytes, $text); + Assert::assertFileExists($this->workStorageDirLocation() . $filename); + $this->userUploadsAFileTo( + $user, + $this->temporaryStorageSubfolderName() . "/$filename", + $destination + ); + $this->removeFile($this->workStorageDirLocation(), $filename); + } + + /** + * @When user :user uploads to these filenames with content :content using the webDAV API then the results should be as listed + * + * @param string $user + * @param string $content + * @param TableNode $table + * + * @return void + * @throws Exception + * @throws GuzzleException + */ + public function userUploadsFilesWithContentTo( + string $user, + string $content, + TableNode $table + ):void { + $user = $this->getActualUsername($user); + foreach ($table->getHash() as $row) { + $this->userUploadsAFileWithContentTo( + $user, + $content, + $row['filename'] + ); + $this->theHTTPStatusCodeShouldBe( + $row['http-code'], + "HTTP status code was not the expected value " . $row['http-code'] . " while trying to upload " . $row['filename'] + ); + if ($row['exists'] === "yes") { + $this->asFileOrFolderShouldExist($user, "file", $row['filename']); + } else { + $this->asFileOrFolderShouldNotExist($user, "file", $row['filename']); + } + } + } + + /** + * @param string $user + * @param string|null $content + * @param string $destination + * + * @return string[] + * @throws JsonException + * @throws GuzzleException + */ + public function uploadFileWithContent( + string $user, + ?string $content, + string $destination + ): array { + $user = $this->getActualUsername($user); + $this->pauseUploadDelete(); + $this->response = $this->makeDavRequest( + $user, + "PUT", + $destination, + [], + $content + ); + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + $this->lastUploadDeleteTime = \time(); + return $this->response->getHeader('oc-fileid'); + } + + /** + * @When the administrator uploads file with content :content to :destination using the WebDAV API + * + * @param string|null $content + * @param string $destination + * + * @return string[] + */ + public function adminUploadsAFileWithContentTo( + ?string $content, + string $destination + ):array { + return $this->uploadFileWithContent($this->getAdminUsername(), $content, $destination); + } + + /** + * @Given the administrator has uploaded file with content :content to :destination + * + * @param string|null $content + * @param string $destination + * + * @return string[] + */ + public function adminHasUploadedAFileWithContentTo( + ?string $content, + string $destination + ):array { + $fileId = $this->uploadFileWithContent($this->getAdminUsername(), $content, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$destination'" + ); + return $fileId; + } + + /** + * @When user :user uploads file with content :content to :destination using the WebDAV API + * + * @param string $user + * @param string|null $content + * @param string $destination + * + * @return void + * @throws GuzzleException + * @throws JsonException + */ + public function userUploadsAFileWithContentTo( + string $user, + ?string $content, + string $destination + ):void { + $this->uploadFileWithContent($user, $content, $destination); + $this->pushToLastHttpStatusCodesArray(); + } + + /** + * @When /^user "([^"]*)" uploads the following files with content "([^"]*)"$/ + * + * @param string $user + * @param string|null $content + * @param TableNode $table + * + * @return void + * @throws Exception|GuzzleException + */ + public function userUploadsFollowingFilesWithContentTo( + string $user, + ?string $content, + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $destination) { + $this->uploadFileWithContent($user, $content, $destination["path"]); + $this->pushToLastStatusCodesArrays(); + } + } + + /** + * @When user :user uploads file :source to :destination with mtime :mtime using the WebDAV API + * @Given user :user has uploaded file :source to :destination with mtime :mtime using the WebDAV API + * + * @param string $user + * @param string $source + * @param string $destination + * @param string $mtime Time in human readable format is taken as input which is converted into milliseconds that is used by API + * + * @return void + * @throws Exception + */ + public function userUploadsFileToWithMtimeUsingTheWebdavApi( + string $user, + string $source, + string $destination, + string $mtime + ):void { + $mtime = new DateTime($mtime); + $mtime = $mtime->format('U'); + $user = $this->getActualUsername($user); + $this->response = UploadHelper::upload( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + $this->acceptanceTestsDirLocation() . $source, + $destination, + $this->getStepLineRef(), + ["X-OC-Mtime" => $mtime], + $this->getDavPathVersion() + ); + } + + /** + * @Given user :user has uploaded file :filename with content :content and mtime of :days days ago using the WebDAV API + * + * @param string $user + * @param string $filename + * @param string $content + * @param string $days In days, e.g. "7" + * + * @return void + * @throws Exception + */ + public function userUploadsFileWithContentAndWithMtimeOfDaysAgoUsingWebdavApi( + string $user, + string $filename, + string $content, + string $days + ): void { + $today = new DateTime(); + $interval = new DateInterval('P' . $days . 'D'); + $mtime = $today->sub($interval)->format('U'); + + $user = $this->getActualUsername($user); + + $this->response = WebDavHelper::makeDavRequest( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + "PUT", + $filename, + ["X-OC-Mtime" => $mtime], + $this->getStepLineRef(), + $content + ); + } + + /** + * @Then as :user the mtime of the file :resource should be :mtime + * + * @param string $user + * @param string $resource + * @param string $mtime + * + * @return void + * @throws Exception + */ + public function theMtimeOfTheFileShouldBe( + string $user, + string $resource, + string $mtime + ):void { + $user = $this->getActualUsername($user); + $password = $this->getPasswordForUser($user); + $baseUrl = $this->getBaseUrl(); + $mtime = new DateTime($mtime); + $mtime = $mtime->format('U'); + Assert::assertEquals( + $mtime, + WebDavHelper::getMtimeOfResource( + $user, + $password, + $baseUrl, + $resource, + $this->getStepLineRef(), + $this->getDavPathVersion() + ) + ); + } + + /** + * @Then as :user the mtime of the file :resource should not be :mtime + * + * @param string $user + * @param string $resource + * @param string $mtime + * + * @return void + * @throws Exception + */ + public function theMtimeOfTheFileShouldNotBe( + string $user, + string $resource, + string $mtime + ):void { + $user = $this->getActualUsername($user); + $password = $this->getPasswordForUser($user); + $baseUrl = $this->getBaseUrl(); + $mtime = new DateTime($mtime); + $mtime = $mtime->format('U'); + Assert::assertNotEquals( + $mtime, + WebDavHelper::getMtimeOfResource( + $user, + $password, + $baseUrl, + $resource, + $this->getStepLineRef(), + $this->getDavPathVersion() + ) + ); + } + + /** + * @Given user :user has uploaded file with content :content to :destination + * + * @param string $user + * @param string|null $content + * @param string $destination + * + * @return string[] + */ + public function userHasUploadedAFileWithContentTo( + string $user, + ?string $content, + string $destination + ):array { + $user = $this->getActualUsername($user); + $fileId = $this->uploadFileWithContent($user, $content, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$destination' for user '$user'" + ); + $this->emptyLastHTTPStatusCodesArray(); + return $fileId; + } + + /** + * @Given /^user "([^"]*)" has uploaded the following files with content "([^"]*)"$/ + * + * @param string $user + * @param string|null $content + * @param TableNode $table + * + * @return array + * @throws Exception + */ + public function userHasUploadedFollowingFilesWithContent( + string $user, + ?string $content, + TableNode $table + ):array { + $this->verifyTableNodeColumns($table, ["path"]); + $files = $table->getHash(); + + $fileIds = []; + foreach ($files as $destination) { + $fileId = $this->userHasUploadedAFileWithContentTo($user, $content, $destination["path"])[0]; + \array_push($fileIds, $fileId); + } + return $fileIds; + } + + /** + * @When /^user "([^"]*)" downloads the following files using the WebDAV API$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userDownloadsFollowingFiles( + string $user, + TableNode $table + ):void { + $this->verifyTableNodeColumns($table, ["path"]); + $files = $table->getHash(); + $this->emptyLastHTTPStatusCodesArray(); + foreach ($files as $fileName) { + $this->downloadFileAsUserUsingPassword($user, $fileName["path"]); + $this->pushToLastStatusCodesArrays(); + } + } + + /** + * @When user :user uploads a file with content :content and mtime :mtime to :destination using the WebDAV API + * + * @param string $user + * @param string|null $content + * @param string $mtime + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userUploadsAFileWithContentAndMtimeTo( + string $user, + ?string $content, + string $mtime, + string $destination + ):void { + $user = $this->getActualUsername($user); + $mtime = new DateTime($mtime); + $mtime = $mtime->format('U'); + $this->makeDavRequest( + $user, + "PUT", + $destination, + ["X-OC-Mtime" => $mtime], + $content + ); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file '$destination' with mtime $mtime for user '$user'" + ); + } + + /** + * @When user :user uploads file with checksum :checksum and content :content to :destination using the WebDAV API + * + * @param string $user + * @param string $checksum + * @param string|null $content + * @param string $destination + * + * @return void + */ + public function userUploadsAFileWithChecksumAndContentTo( + string $user, + string $checksum, + ?string $content, + string $destination + ):void { + $this->pauseUploadDelete(); + $this->response = $this->makeDavRequest( + $user, + "PUT", + $destination, + ['OC-Checksum' => $checksum], + $content + ); + $this->lastUploadDeleteTime = \time(); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given user :user has uploaded file with checksum :checksum and content :content to :destination + * + * @param string $user + * @param string $checksum + * @param string|null $content + * @param string $destination + * + * @return void + */ + public function userHasUploadedAFileWithChecksumAndContentTo( + string $user, + string $checksum, + ?string $content, + string $destination + ):void { + $this->userUploadsAFileWithChecksumAndContentTo( + $user, + $checksum, + $content, + $destination + ); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload file with checksum '$checksum' to '$destination' for user '$user'" + ); + } + + /** + * @Then /^user "([^"]*)" should be able to delete (file|folder|entry) "([^"]*)"$/ + * + * @param string $user + * @param string $entry + * @param string $source + * + * @return void + * @throws Exception + */ + public function userShouldBeAbleToDeleteEntry(string $user, string $entry, string $source):void { + $user = $this->getActualUsername($user); + $this->asFileOrFolderShouldExist($user, $entry, $source); + $this->userDeletesFile($user, $source); + $this->asFileOrFolderShouldNotExist($user, $entry, $source); + } + + /** + * @Then /^user "([^"]*)" should not be able to delete (file|folder|entry) "([^"]*)"$/ + * + * @param string $user + * @param string $entry + * @param string $source + * + * @return void + * @throws Exception + */ + public function theUserShouldNotBeAbleToDeleteEntry(string $user, string $entry, string $source):void { + $this->asFileOrFolderShouldExist($user, $entry, $source); + $this->userDeletesFile($user, $source); + $this->asFileOrFolderShouldExist($user, $entry, $source); + } + + /** + * @Given file :file has been deleted for user :user + * + * @param string $file + * @param string $user + * + * @return void + */ + public function fileHasBeenDeleted(string $file, string $user):void { + $this->userHasDeletedFile($user, "deleted", "file", $file); + } + + /** + * @When /^user "([^"]*)" (?:deletes|unshares) (?:file|folder) "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $file + * + * @return void + */ + public function userDeletesFile(string $user, string $file):void { + $user = $this->getActualUsername($user); + $this->pauseUploadDelete(); + $this->response = $this->makeDavRequest($user, 'DELETE', $file, []); + $this->lastUploadDeleteTime = \time(); + $this->pushToLastHttpStatusCodesArray((string) $this->getResponse()->getStatusCode()); + } + + /** + * @Given /^user "([^"]*)" has (deleted|unshared) (file|folder) "([^"]*)"$/ + * + * @param string $user + * @param string $deletedOrUnshared + * @param string $fileOrFolder + * @param string $entry + * + * @return void + */ + public function userHasDeletedFile(string $user, string $deletedOrUnshared, string $fileOrFolder, string $entry):void { + $user = $this->getActualUsername($user); + $this->userDeletesFile($user, $entry); + // If the file or folder was there and got deleted then we get a 204 + // That is good and the expected status + // If the file or folder was already not there then then we get a 404 + // That is not expected. Scenarios that use "Given user has deleted..." + // should only be using such steps when it is a file that exists and needs + // to be deleted. + if ($deletedOrUnshared === "deleted") { + $deleteText = "delete"; + } else { + $deleteText = "unshare"; + } + + $this->theHTTPStatusCodeShouldBe( + ["204"], + "HTTP status code was not 204 while trying to $deleteText $fileOrFolder '$entry' for user '$user'" + ); + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @Given /^user "([^"]*)" has (deleted|unshared) the following (files|folders|resources)$/ + * + * @param string $user + * @param string $deletedOrUnshared + * @param string $fileOrFolder + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userHasDeletedFollowingFiles(string $user, string $deletedOrUnshared, string $fileOrFolder, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $file) { + $this->userHasDeletedFile($user, $deletedOrUnshared, $fileOrFolder, $file["path"]); + } + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @When /^user "([^"]*)" (?:deletes|unshares) the following (?:files|folders)$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userDeletesFollowingFiles(string $user, TableNode $table):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $file) { + $this->userDeletesFile($user, $file["path"]); + $this->pushToLastStatusCodesArrays(); + } + } + + /** + * @When /^the user (?:deletes|unshares) (?:file|folder) "([^"]*)" using the WebDAV API$/ + * + * @param string $file + * + * @return void + */ + public function theUserDeletesFile(string $file):void { + $this->userDeletesFile($this->getCurrentUser(), $file); + } + + /** + * @Given /^the user has (deleted|unshared) (file|folder) "([^"]*)"$/ + * + * @param string $deletedOrUnshared + * @param string $fileOrFolder + * @param string $file + * + * @return void + */ + public function theUserHasDeletedFile(string $deletedOrUnshared, string $fileOrFolder, string $file):void { + $this->userHasDeletedFile($this->getCurrentUser(), $deletedOrUnshared, $fileOrFolder, $file); + } + + /** + * @When /^user "([^"]*)" (?:deletes|unshares) these (?:files|folders|entries) without delays using the WebDAV API$/ + * + * @param string $user + * @param TableNode $table of files or folders to delete + * + * @return void + * @throws Exception + */ + public function userDeletesFilesFoldersWithoutDelays(string $user, TableNode $table):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumnsCount($table, 1); + foreach ($table->getTable() as $entry) { + $entryName = $entry[0]; + $this->response = $this->makeDavRequest($user, 'DELETE', $entryName, []); + $this->pushToLastStatusCodesArrays(); + } + $this->lastUploadDeleteTime = \time(); + } + + /** + * @When /^the user (?:deletes|unshares) these (?:files|folders|entries) without delays using the WebDAV API$/ + * + * @param TableNode $table of files or folders to delete + * + * @return void + * @throws Exception + */ + public function theUserDeletesFilesFoldersWithoutDelays(TableNode $table):void { + $this->userDeletesFilesFoldersWithoutDelays($this->getCurrentUser(), $table); + } + + /** + * @When /^user "([^"]*)" on "(LOCAL|REMOTE)" (?:deletes|unshares) (?:file|folder) "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $server + * @param string $file + * + * @return void + */ + public function userOnDeletesFile(string $user, string $server, string $file):void { + $previousServer = $this->usingServer($server); + $this->userDeletesFile($user, $file); + $this->usingServer($previousServer); + } + + /** + * @Given /^user "([^"]*)" on "(LOCAL|REMOTE)" has (deleted|unshared) (file|folder) "([^"]*)"$/ + * + * @param string $user + * @param string $server + * @param string $deletedOrUnshared + * @param string $fileOrFolder + * @param string $entry + * + * @return void + */ + public function userOnHasDeletedFile(string $user, string $server, string $deletedOrUnshared, string $fileOrFolder, string $entry):void { + $this->userOnDeletesFile($user, $server, $entry); + // If the file was there and got deleted then we get a 204 + // If the file was already not there then then get a 404 + // Either way, the outcome of the "given" step is OK + if ($deletedOrUnshared === "deleted") { + $deleteText = "delete"; + } else { + $deleteText = "unshare"; + } + + $this->theHTTPStatusCodeShouldBe( + ["204", "404"], + "HTTP status code was not 204 or 404 while trying to $deleteText $fileOrFolder '$entry' for user '$user' on server '$server'" + ); + } + + /** + * @When user :user creates folder :destination using the WebDAV API + * + * @param string $user + * @param string $destination + * + * @return void + * @throws JsonException | GuzzleException + */ + public function userCreatesFolder(string $user, string $destination):void { + $user = $this->getActualUsername($user); + $destination = '/' . \ltrim($destination, '/'); + $this->response = $this->makeDavRequest( + $user, + "MKCOL", + $destination, + [] + ); + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + $this->pushToLastHttpStatusCodesArray(); + } + + /** + * @Given user :user has created folder :destination + * + * @param string $user + * @param string $destination + * + * @return void + * @throws JsonException + * @throws GuzzleException + */ + public function userHasCreatedFolder(string $user, string $destination):void { + $user = $this->getActualUsername($user); + $this->userCreatesFolder($user, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to create folder '$destination' for user '$user'" + ); + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @Given admin has created folder :destination + * + * @param string $destination + * + * @return void + * @throws JsonException + * @throws GuzzleException + */ + public function adminHasCreatedFolder(string $destination):void { + $admin = $this->getAdminUsername(); + Assert::assertEquals( + "admin", + $admin, + __METHOD__ . "The provided user is not admin but '" . $admin . "'" + ); + $this->userCreatesFolder($admin, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to create folder '$destination' for admin '$admin'" + ); + $this->adminResources[] = $destination; + $this->emptyLastHTTPStatusCodesArray(); + } + + /** + * @Given /^user "([^"]*)" has created the following folders$/ + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function userHasCreatedFollowingFolders(string $user, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["path"]); + $paths = $table->getHash(); + + foreach ($paths as $path) { + $this->userHasCreatedFolder($user, $path["path"]); + } + } + + /** + * @When the user creates folder :destination using the WebDAV API + * + * @param string $destination + * + * @return void + */ + public function theUserCreatesFolder(string $destination):void { + $this->userCreatesFolder($this->getCurrentUser(), $destination); + } + + /** + * @Given the user has created folder :destination + * + * @param string $destination + * + * @return void + */ + public function theUserHasCreatedFolder(string $destination):void { + $this->theUserCreatesFolder($destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to create folder '$destination'" + ); + } + + /** + * @Then user :user should be able to create folder :destination + * + * @param string $user + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userShouldBeAbleToCreateFolder(string $user, string $destination):void { + $this->userHasCreatedFolder($user, $destination); + $this->asFileOrFolderShouldExist( + $user, + "folder", + $destination + ); + } + + /** + * @Then user :user should not be able to create folder :destination + * + * @param string $user + * @param string $destination + * + * @return void + * @throws Exception + */ + public function userShouldNotBeAbleToCreateFolder(string $user, string $destination):void { + $user = $this->getActualUsername($user); + $this->userCreatesFolder($user, $destination); + $this->theHTTPStatusCodeShouldBeFailure(); + $this->asFileOrFolderShouldNotExist( + $user, + "folder", + $destination + ); + } + + /** + * Old style chunking upload + * + * @When user :user uploads the following :total chunks to :file with old chunking and using the WebDAV API + * + * @param string $user + * @param string $total + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content with column headings, e.g. + * | number | content | + * | 1 | first data | + * | 2 | followed by second data | + * Chunks may be numbered out-of-order if desired. + * + * @return void + * @throws Exception + */ + public function userUploadsTheFollowingTotalChunksUsingOldChunking( + string $user, + string $total, + string $file, + TableNode $chunkDetails + ):void { + $this->verifyTableNodeColumns($chunkDetails, ['number', 'content']); + foreach ($chunkDetails->getHash() as $chunkDetail) { + $chunkNumber = (int)$chunkDetail['number']; + $chunkContent = $chunkDetail['content']; + $this->userUploadsChunkedFile($user, $chunkNumber, (int) $total, $chunkContent, $file); + } + } + + /** + * Old style chunking upload + * + * @Given user :user has uploaded the following :total chunks to :file with old chunking + * + * @param string $user + * @param string $total + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content with following headings, e.g. + * | number | content | + * | 1 | first data | + * | 2 | followed by second data | + * Chunks may be numbered out-of-order if desired. + * + * @return void + * @throws Exception + */ + public function userHasUploadedTheFollowingTotalChunksUsingOldChunking( + string $user, + string $total, + string $file, + TableNode $chunkDetails + ):void { + $this->verifyTableNodeColumns($chunkDetails, ['number', 'content']); + foreach ($chunkDetails->getHash() as $chunkDetail) { + $chunkNumber = (int) $chunkDetail['number']; + $chunkContent = $chunkDetail['content']; + $this->userHasUploadedChunkedFile($user, $chunkNumber, (int) $total, $chunkContent, $file); + } + } + + /** + * Old style chunking upload + * + * @When user :user uploads the following chunks to :file with old chunking and using the WebDAV API + * + * @param string $user + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content with column headings, e.g. + * | number | content | + * | 1 | first data | + * | 2 | followed by second data | + * Chunks may be numbered out-of-order if desired. + * + * @return void + * @throws Exception + */ + public function userUploadsTheFollowingChunksUsingOldChunking( + string $user, + string $file, + TableNode $chunkDetails + ):void { + $total = \count($chunkDetails->getHash()); + $this->userUploadsTheFollowingTotalChunksUsingOldChunking( + $user, + (string) $total, + $file, + $chunkDetails + ); + } + + /** + * Old style chunking upload + * + * @Given user :user has uploaded the following chunks to :file with old chunking + * + * @param string $user + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content with headings, e.g. + * | number | content | + * | 1 | first data | + * | 2 | followed by second data | + * Chunks may be numbered out-of-order if desired. + * + * @return void + * @throws Exception + */ + public function userHasUploadedTheFollowingChunksUsingOldChunking( + string $user, + string $file, + TableNode $chunkDetails + ):void { + $total = \count($chunkDetails->getRows()); + $this->userHasUploadedTheFollowingTotalChunksUsingOldChunking( + $user, + (string) $total, + $file, + $chunkDetails + ); + } + + /** + * Old style chunking upload + * + * @When user :user uploads chunk file :num of :total with :data to :destination using the WebDAV API + * + * @param string $user + * @param int $num + * @param int $total + * @param string|null $data + * @param string $destination + * + * @return void + */ + public function userUploadsChunkedFile( + string $user, + int $num, + int $total, + ?string $data, + string $destination + ):void { + $user = $this->getActualUsername($user); + $num -= 1; + $file = "$destination-chunking-42-$total-$num"; + $this->pauseUploadDelete(); + $this->response = $this->makeDavRequest( + $user, + 'PUT', + $file, + ['OC-Chunked' => '1'], + $data, + "uploads" + ); + $this->lastUploadDeleteTime = \time(); + } + + /** + * Old style chunking upload + * + * @Given user :user has uploaded chunk file :num of :total with :data to :destination + * + * @param string $user + * @param int|null $num + * @param int|null $total + * @param string|null $data + * @param string $destination + * + * @return void + */ + public function userHasUploadedChunkedFile( + string $user, + ?int $num, + ?int $total, + ?string $data, + string $destination + ):void { + $user = $this->getActualUsername($user); + $this->userUploadsChunkedFile($user, $num, $total, $data, $destination); + $this->theHTTPStatusCodeShouldBe( + ["201", "204"], + "HTTP status code was not 201 or 204 while trying to upload chunk $num of $total to file '$destination' for user '$user'" + ); + } + + /** + * New style chunking upload + * + * @When /^user "([^"]*)" uploads the following chunks\s?(asynchronously|) to "([^"]*)" with new chunking and using the WebDAV API$/ + * + * @param string $user + * @param string $type "asynchronously" or empty + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content, with headings e.g. + * | number | content | + * | 1 | first data | + * | 2 | second data | + * Chunks may be numbered out-of-order if desired. + * + * @return void + */ + public function userUploadsTheFollowingChunksUsingNewChunking( + string $user, + string $type, + string $file, + TableNode $chunkDetails + ):void { + $this->uploadTheFollowingChunksUsingNewChunking( + $user, + $type, + $file, + $chunkDetails + ); + } + + /** + * New style chunking upload + * + * @Given /^user "([^"]*)" has uploaded the following chunks\s?(asynchronously|) to "([^"]*)" with new chunking$/ + * + * @param string $user + * @param string $type "asynchronously" or empty + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content without column headings, e.g. + * | number | content | + * | 1 | first data | + * | 2 | followed by second data | + * Chunks may be numbered out-of-order if desired. + * + * @return void + */ + public function userHasUploadedTheFollowingChunksUsingNewChunking( + string $user, + string $type, + string $file, + TableNode $chunkDetails + ):void { + $this->uploadTheFollowingChunksUsingNewChunking( + $user, + $type, + $file, + $chunkDetails, + true + ); + } + + /** + * New style chunking upload + * + * @param string $user + * @param string $type "asynchronously" or empty + * @param string $file + * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk + * content with column headings, e.g. + * | number | content | + * | 1 | first data | + * | 2 | second data | + * Chunks may be numbered out-of-order if desired. + * @param bool $checkActions + * + * @return void + * @throws Exception + */ + public function uploadTheFollowingChunksUsingNewChunking( + string $user, + string $type, + string $file, + TableNode $chunkDetails, + bool $checkActions = false + ):void { + $user = $this->getActualUsername($user); + $async = false; + if ($type === "asynchronously") { + $async = true; + } + $this->verifyTableNodeColumns($chunkDetails, ["number", "content"]); + $this->userUploadsChunksUsingNewChunking( + $user, + $file, + 'chunking-42', + $chunkDetails->getHash(), + $async, + $checkActions + ); + } + + /** + * New style chunking upload + * + * @param string $user + * @param string $file + * @param string $chunkingId + * @param array $chunkDetails of chunks of the file. Each array entry is + * itself an array of 2 items: + * [number] the chunk number + * [content] data content of the chunk + * Chunks may be numbered out-of-order if desired. + * @param bool $async use asynchronous MOVE at the end or not + * @param bool $checkActions + * + * @return void + */ + public function userUploadsChunksUsingNewChunking( + string $user, + string $file, + string $chunkingId, + array $chunkDetails, + bool $async = false, + bool $checkActions = false + ):void { + $this->pauseUploadDelete(); + if ($checkActions) { + $this->userHasCreatedANewChunkingUploadWithId($user, $chunkingId); + } else { + $this->userCreatesANewChunkingUploadWithId($user, $chunkingId); + } + foreach ($chunkDetails as $chunkDetail) { + $chunkNumber = (int)$chunkDetail['number']; + $chunkContent = $chunkDetail['content']; + if ($checkActions) { + $this->userHasUploadedNewChunkFileOfWithToId($user, $chunkNumber, $chunkContent, $chunkingId); + } else { + $this->userUploadsNewChunkFileOfWithToId($user, $chunkNumber, $chunkContent, $chunkingId); + } + } + $headers = []; + if ($async === true) { + $headers = ['OC-LazyOps' => 'true']; + } + $this->moveNewDavChunkToFinalFile($user, $chunkingId, $file, $headers); + if ($checkActions) { + $this->theHTTPStatusCodeShouldBeSuccess(); + } + $this->lastUploadDeleteTime = \time(); + } + + /** + * @When user :user creates a new chunking upload with id :id using the WebDAV API + * + * @param string $user + * @param string $id + * + * @return void + */ + public function userCreatesANewChunkingUploadWithId(string $user, string $id):void { + $user = $this->getActualUsername($user); + $destination = "/uploads/$user/$id"; + $this->response = $this->makeDavRequest( + $user, + 'MKCOL', + $destination, + [], + null, + "uploads" + ); + } + + /** + * @Given user :user has created a new chunking upload with id :id + * + * @param string $user + * @param string $id + * + * @return void + */ + public function userHasCreatedANewChunkingUploadWithId(string $user, string $id):void { + $this->userCreatesANewChunkingUploadWithId($user, $id); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When user :user uploads new chunk file :num with :data to id :id using the WebDAV API + * + * @param string $user + * @param int $num + * @param string|null $data + * @param string $id + * + * @return void + */ + public function userUploadsNewChunkFileOfWithToId(string $user, int $num, ?string $data, string $id):void { + $user = $this->getActualUsername($user); + $destination = "/uploads/$user/$id/$num"; + $this->response = $this->makeDavRequest( + $user, + 'PUT', + $destination, + [], + $data, + "uploads" + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given user :user has uploaded new chunk file :num with :data to id :id + * + * @param string $user + * @param int $num + * @param string|null $data + * @param string $id + * + * @return void + */ + public function userHasUploadedNewChunkFileOfWithToId(string $user, int $num, ?string $data, string $id):void { + $this->userUploadsNewChunkFileOfWithToId($user, $num, $data, $id); + $this->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When /^user "([^"]*)" moves new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $id + * @param string $type "asynchronously" or empty + * @param string $dest + * + * @return void + */ + public function userMovesNewChunkFileWithIdToMychunkedfile( + string $user, + string $id, + string $type, + string $dest + ):void { + $headers = []; + if ($type === "asynchronously") { + $headers = ['OC-LazyOps' => 'true']; + } + $this->moveNewDavChunkToFinalFile($user, $id, $dest, $headers); + } + + /** + * @Given /^user "([^"]*)" has moved new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)"$/ + * + * @param string $user + * @param string $id + * @param string $type "asynchronously" or empty + * @param string $dest + * + * @return void + */ + public function userHasMovedNewChunkFileWithIdToMychunkedfile( + string $user, + string $id, + string $type, + string $dest + ):void { + $this->userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $type, $dest); + $this->theHTTPStatusCodeShouldBe("201"); + } + + /** + * @When user :user cancels chunking-upload with id :id using the WebDAV API + * + * @param string $user + * @param string $id + * + * @return void + */ + public function userCancelsUploadWithId( + string $user, + string $id + ):void { + $this->deleteUpload($user, $id, []); + } + + /** + * @Given user :user has canceled new chunking-upload with id :id + * + * @param string $user + * @param string $id + * + * @return void + */ + public function userHasCanceledUploadWithId( + string $user, + string $id + ):void { + $this->userCancelsUploadWithId($user, $id); + $this->theHTTPStatusCodeShouldBe("201"); + } + + /** + * @When /^user "([^"]*)" moves new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" with size (.*) using the WebDAV API$/ + * + * @param string $user + * @param string $id + * @param string $type "asynchronously" or empty + * @param string $dest + * @param int $size + * + * @return void + */ + public function userMovesNewChunkFileWithIdToMychunkedfileWithSize( + string $user, + string $id, + string $type, + string $dest, + int $size + ):void { + $headers = ['OC-Total-Length' => $size]; + if ($type === "asynchronously") { + $headers['OC-LazyOps'] = 'true'; + } + $this->moveNewDavChunkToFinalFile( + $user, + $id, + $dest, + $headers + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has moved new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" with size (.*)$/ + * + * @param string $user + * @param string $id + * @param string $type "asynchronously" or empty + * @param string $dest + * @param int $size + * + * @return void + */ + public function userHasMovedNewChunkFileWithIdToMychunkedfileWithSize( + string $user, + string $id, + string $type, + string $dest, + int $size + ):void { + $this->userMovesNewChunkFileWithIdToMychunkedfileWithSize( + $user, + $id, + $type, + $dest, + $size + ); + $this->theHTTPStatusCodeShouldBe("201"); + } + + /** + * @When /^user "([^"]*)" moves new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" with checksum "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $id + * @param string $type "asynchronously" or empty + * @param string $dest + * @param string $checksum + * + * @return void + */ + public function userMovesNewChunkFileWithIdToMychunkedfileWithChecksum( + string $user, + string $id, + string $type, + string $dest, + string $checksum + ):void { + $headers = ['OC-Checksum' => $checksum]; + if ($type === "asynchronously") { + $headers['OC-LazyOps'] = 'true'; + } + $this->moveNewDavChunkToFinalFile( + $user, + $id, + $dest, + $headers + ); + $this->pushToLastStatusCodesArrays(); + } + + /** + * @Given /^user "([^"]*)" has moved new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" with checksum "([^"]*)" + * + * @param string $user + * @param string $id + * @param string $type "asynchronously" or empty + * @param string $dest + * @param string $checksum + * + * @return void + */ + public function userHasMovedNewChunkFileWithIdToMychunkedfileWithChecksum( + string $user, + string $id, + string $type, + string $dest, + string $checksum + ):void { + $this->userMovesNewChunkFileWithIdToMychunkedfileWithChecksum( + $user, + $id, + $type, + $dest, + $checksum + ); + $this->theHTTPStatusCodeShouldBe("201"); + } + + /** + * Move chunked new DAV file to final file + * + * @param string $user user + * @param string $id upload id + * @param string $destination destination path + * @param array $headers extra headers + * + * @return void + */ + private function moveNewDavChunkToFinalFile(string $user, string $id, string $destination, array $headers):void { + $user = $this->getActualUsername($user); + $source = "/uploads/$user/$id/.file"; + $headers['Destination'] = $this->destinationHeaderValue( + $user, + $destination + ); + + $this->response = $this->makeDavRequest( + $user, + 'MOVE', + $source, + $headers, + null, + "uploads" + ); + } + + /** + * Delete chunked-upload directory + * + * @param string $user user + * @param string $id upload id + * @param array $headers extra headers + * + * @return void + */ + private function deleteUpload(string $user, string $id, array $headers) { + $source = "/uploads/$user/$id"; + $this->response = $this->makeDavRequest( + $user, + 'DELETE', + $source, + $headers, + null, + "uploads" + ); + } + + /** + * URL encodes the given path but keeps the slashes + * + * @param string $path to encode + * + * @return string encoded path + */ + public function encodePath(string $path):string { + // slashes need to stay + $encodedPath = \str_replace('%2F', '/', \rawurlencode($path)); + // in ocis even brackets are encoded + if (OcisHelper::isTestingOnOcisOrReva()) { + return $encodedPath; + } + // do not encode '(' and ')' for oc10 + $encodedPath = \str_replace('%28', '(', $encodedPath); + $encodedPath = \str_replace('%29', ')', $encodedPath); + return $encodedPath; + } + + /** + * @When an unauthenticated client connects to the DAV endpoint using the WebDAV API + * + * @return void + */ + public function connectingToDavEndpoint():void { + $this->response = $this->makeDavRequest( + null, + 'PROPFIND', + '', + [] + ); + } + + /** + * @Given an unauthenticated client has connected to the DAV endpoint + * + * @return void + */ + public function hasConnectedToDavEndpoint():void { + $this->connectingToDavEndpoint(); + $this->theHTTPStatusCodeShouldBe("401"); + } + + /** + * @Then there should be no duplicate headers + * + * @return void + * @throws Exception + */ + public function thereAreNoDuplicateHeaders():void { + $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 the following headers should not be set + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theFollowingHeadersShouldNotBeSet(TableNode $table):void { + $this->verifyTableNodeColumns( + $table, + ['header'] + ); + foreach ($table->getColumnsHash() as $header) { + $headerName = $header['header']; + $headerValue = $this->response->getHeader($headerName); + //Note: getHeader returns an empty array if the named header does not exist + if (isset($headerValue[0])) { + $headerValue0 = $headerValue[0]; + } else { + $headerValue0 = ''; + } + Assert::assertEmpty( + $headerValue, + "header $headerName should not exist " . + "but does and is set to $headerValue0" + ); + } + } + + /** + * @Then the following headers should match these regular expressions + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function headersShouldMatchRegularExpressions(TableNode $table):void { + $this->verifyTableNodeColumnsCount($table, 2); + foreach ($table->getTable() as $header) { + $headerName = $header[0]; + $expectedHeaderValue = $header[1]; + $expectedHeaderValue = $this->substituteInLineCodes( + $expectedHeaderValue, + null, + ['preg_quote' => ['/']] + ); + + $returnedHeaders = $this->response->getHeader($headerName); + $returnedHeader = $returnedHeaders[0]; + Assert::assertNotFalse( + (bool) \preg_match($expectedHeaderValue, $returnedHeader), + "'$expectedHeaderValue' does not match '$returnedHeader'" + ); + } + } + + /** + * @Then /^if the HTTP status code was "([^"]*)" then the following headers should match these regular expressions$/ + * + * @param int $statusCode + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function statusCodeShouldMatchTheseRegularExpressions(int $statusCode, TableNode $table):void { + $actualStatusCode = $this->response->getStatusCode(); + if ($actualStatusCode === $statusCode) { + $this->headersShouldMatchRegularExpressions($table); + } + } + + /** + * @Then the following headers should match these regular expressions for user :user + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function headersShouldMatchRegularExpressionsForUser(string $user, TableNode $table):void { + $this->verifyTableNodeColumnsCount($table, 2); + $user = $this->getActualUsername($user); + foreach ($table->getTable() as $header) { + $headerName = $header[0]; + $expectedHeaderValue = $header[1]; + $expectedHeaderValue = $this->substituteInLineCodes( + $expectedHeaderValue, + $user, + ['preg_quote' => ['/']] + ); + + $returnedHeaders = $this->response->getHeader($headerName); + $returnedHeader = $returnedHeaders[0]; + Assert::assertNotFalse( + (bool) \preg_match($expectedHeaderValue, $returnedHeader), + "'$expectedHeaderValue' does not match '$returnedHeader'" + ); + } + } + + /** + * @When /^user "([^"]*)" deletes everything from folder "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $folder + * @param bool $checkEachDelete + * + * @return void + * @throws Exception + */ + public function userDeletesEverythingInFolder( + string $user, + string $folder, + bool $checkEachDelete = false + ):void { + $user = $this->getActualUsername($user); + $responseXmlObject = $this->listFolderAndReturnResponseXml( + $user, + $folder, + '1' + ); + $elementList = $responseXmlObject->xpath("//d:response/d:href"); + if (\is_array($elementList) && \count($elementList)) { + \array_shift($elementList); //don't delete the folder itself + $davPrefix = "/" . $this->getFullDavFilesPath($user); + foreach ($elementList as $element) { + $element = \substr((string)$element, \strlen($davPrefix)); + if ($checkEachDelete) { + $this->userHasDeletedFile($user, "deleted", "file", $element); + } else { + $this->userDeletesFile($user, $element); + } + } + } + } + + /** + * @Given /^user "([^"]*)" has deleted everything from folder "([^"]*)"$/ + * + * @param string $user + * @param string $folder + * + * @return void + * @throws Exception + */ + public function userHasDeletedEverythingInFolder(string $user, string $folder):void { + $this->userDeletesEverythingInFolder($user, $folder, true); + } + + /** + * @When user :user downloads the preview of :path with width :width and height :height using the WebDAV API + * + * @param string $user + * @param string $path + * @param string $width + * @param string $height + * + * @return void + */ + public function downloadPreviewOfFiles(string $user, string $path, string $width, string $height):void { + $this->downloadPreviews( + $user, + $path, + null, + $width, + $height + ); + } + + /** + * @When user :user1 downloads the preview of :path of :user2 with width :width and height :height using the WebDAV API + * + * @param string $user1 + * @param string $path + * @param string $doDavRequestAsUser + * @param string $width + * @param string $height + * + * @return void + */ + public function downloadPreviewOfOtherUser(string $user1, string $path, string $doDavRequestAsUser, string $width, string $height):void { + $this->downloadPreviews( + $user1, + $path, + $doDavRequestAsUser, + $width, + $height + ); + } + + /** + * @Then the downloaded image for user :user should be :width pixels wide and :height pixels high + * + * @param string $user + * @param string $width + * @param string $height + * + * @return void + */ + public function imageDimensionsForAUserShouldBe(string $user, string $width, string $height):void { + if ($this->userResponseBodyContents[$user] === null) { + $this->userResponseBodyContents[$user] = $this->response->getBody()->getContents(); + } + $size = \getimagesizefromstring($this->userResponseBodyContents[$user]); + Assert::assertNotFalse($size, "could not get size of image"); + Assert::assertEquals($width, $size[0], "width not as expected"); + Assert::assertEquals($height, $size[1], "height not as expected"); + } + + /** + * @Then the downloaded image should be :width pixels wide and :height pixels high + * + * @param string $width + * @param string $height + * + * @return void + */ + public function imageDimensionsShouldBe(string $width, string $height): void { + if ($this->responseBodyContent === null) { + $this->responseBodyContent = $this->response->getBody()->getContents(); + } + $size = \getimagesizefromstring($this->responseBodyContent); + Assert::assertNotFalse($size, "could not get size of image"); + Assert::assertEquals($width, $size[0], "width not as expected"); + Assert::assertEquals($height, $size[1], "height not as expected"); + } + + /** + * @Then the requested JPEG image should have a quality value of :size + * + * @param string $value + * + * @return void + */ + public function jpgQualityValueShouldBe(string $value): void { + $this->responseBodyContent = $this->response->getBody()->getContents(); + // quality value is embedded in the string content for JPEG images + $qualityString = "quality = $value"; + Assert::assertStringContainsString($qualityString, $this->responseBodyContent); + } + + /** + * @Then the downloaded preview content should match with :preview fixtures preview content + * + * @param string $filename relative path from fixtures directory + * + * @return void + * @throws Exception + */ + public function theDownloadedPreviewContentShouldMatchWithFixturesPreviewContentFor(string $filename):void { + $expectedPreview = \file_get_contents(__DIR__ . "/../../fixtures/" . $filename); + Assert::assertEquals($expectedPreview, $this->responseBodyContent); + } + + /** + * @Given user :user has downloaded the preview of :path with width :width and height :height + * + * @param string $user + * @param string $path + * @param string $width + * @param string $height + * + * @return void + */ + public function userDownloadsThePreviewOfWithWidthAndHeight(string $user, string $path, string $width, string $height):void { + $this->downloadPreviewOfFiles($user, $path, $width, $height); + $this->theHTTPStatusCodeShouldBe(200); + $this->imageDimensionsShouldBe($width, $height); + // save response to user response dictionary for further comparisons + $this->userResponseBodyContents[$user] = $this->responseBodyContent; + } + + /** + * @Then as user :user the preview of :path with width :width and height :height should have been changed + * + * @param string $user + * @param string $path + * @param string $width + * @param string $height + * + * @return void + */ + public function asUserThePreviewOfPathWithHeightAndWidthShouldHaveBeenChanged(string $user, string $path, string $width, string $height):void { + $this->downloadPreviewOfFiles($user, $path, $width, $height); + $this->theHTTPStatusCodeShouldBe(200); + $newResponseBodyContents = $this->response->getBody()->getContents(); + Assert::assertNotEquals( + $newResponseBodyContents, + // different users can download files before and after an update is made to a file + // previous response content is fetched from user response body content array for that user + $this->userResponseBodyContents[$user], + __METHOD__ . " previous and current previews content is same but expected to be different", + ); + // update the saved content for the next comparison + $this->userResponseBodyContents[$user] = $newResponseBodyContents; + } + + /** + * @param string $user + * @param string $path + * + * @return string|null + */ + public function getFileIdForPath(string $user, string $path): ?string { + $user = $this->getActualUsername($user); + try { + return WebDavHelper::getFileIdForPath( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + $path, + $this->getStepLineRef(), + $this->getDavPathVersion() + ); + } catch (Exception $e) { + return null; + } + } + + /** + * @Given /^user "([^"]*)" has stored id of (file|folder) "([^"]*)"$/ + * @When /^user "([^"]*)" stores id of (file|folder) "([^"]*)"$/ + * + * @param string $user + * @param string $fileOrFolder + * @param string $path + * + * @return void + */ + public function userStoresFileIdForPath(string $user, string $fileOrFolder, string $path):void { + $this->storedFileID = $this->getFileIdForPath($user, $path); + } + + /** + * @Then /^user "([^"]*)" (file|folder) "([^"]*)" should have the previously stored id$/ + * + * @param string $user + * @param string $fileOrFolder + * @param string $path + * + * @return void + */ + public function userFileShouldHaveStoredId(string $user, string $fileOrFolder, string $path):void { + $user = $this->getActualUsername($user); + $currentFileID = $this->getFileIdForPath($user, $path); + Assert::assertEquals( + $currentFileID, + $this->storedFileID, + __METHOD__ + . " User '$user' $fileOrFolder '$path' does not have the previously stored id '{$this->storedFileID}', but has '$currentFileID'." + ); + } + + /** + * @Then /^the (?:Cal|Card)?DAV (exception|message|reason) should be "([^"]*)"$/ + * + * @param string $element exception|message|reason + * @param string $message + * + * @return void + * @throws Exception + */ + public function theDavElementShouldBe(string $element, string $message):void { + WebDavAssert::assertDavResponseElementIs( + $element, + $message, + $this->responseXml, + __METHOD__ + ); + } + + /** + * @param string $shouldOrNot (not|) + * @param TableNode $expectedFiles + * @param string|null $user + * @param string|null $method + * @param string|null $folderpath + * + * @return void + * @throws GuzzleException + */ + public function propfindResultShouldContainEntries( + string $shouldOrNot, + TableNode $expectedFiles, + ?string $user = null, + ?string $method = 'REPORT', + ?string $folderpath = '' + ):void { + $this->verifyTableNodeColumnsCount($expectedFiles, 1); + $elementRows = $expectedFiles->getRows(); + $should = ($shouldOrNot !== "not"); + foreach ($elementRows as $expectedFile) { + $fileFound = $this->findEntryFromPropfindResponse( + $expectedFile[0], + $user, + $method, + "files", + $folderpath + ); + if ($should) { + Assert::assertNotEmpty( + $fileFound, + "response does not contain the entry '$expectedFile[0]'" + ); + } else { + Assert::assertFalse( + $fileFound, + "response does contain the entry '$expectedFile[0]' but should not" + ); + } + } + } + + /** + * @Then /^the (?:propfind|search) result of user "([^"]*)" should (not|)\s?contain these (?:files|entries):$/ + * + * @param string $user + * @param string $shouldOrNot (not|) + * @param TableNode $expectedFiles + * + * @return void + * @throws Exception + */ + public function thePropfindResultShouldContainEntries( + string $user, + string $shouldOrNot, + TableNode $expectedFiles + ):void { + $user = $this->getActualUsername($user); + $this->propfindResultShouldContainEntries( + $shouldOrNot, + $expectedFiles, + $user + ); + } + + /** + * @Then /^the (?:propfind|search) result of user "([^"]*)" should not contain any (?:files|entries)$/ + * + * @param string $user + * + * @return void + * @throws Exception + */ + public function thePropfindResultShouldNotContainAnyEntries( + string $user + ):void { + $multistatusResults = $this->getMultistatusResultFromPropfindResult($user); + Assert::assertEmpty($multistatusResults, 'The propfind response was expected to be empty but was not'); + } + + /** + * @Then /^the (?:propfind|search) result of user "([^"]*)" should contain only these (?:files|entries):$/ + * + * @param string $user + * @param TableNode $expectedFiles + * + * @return void + * @throws Exception + */ + public function thePropfindResultShouldContainOnlyEntries( + string $user, + TableNode $expectedFiles + ):void { + $user = $this->getActualUsername($user); + + Assert::assertEquals( + \count($expectedFiles->getTable()), + $this->getNumberOfEntriesInPropfindResponse( + $user + ), + "The number of elements in the response doesn't matches with expected number of elements" + ); + $this->propfindResultShouldContainEntries( + '', + $expectedFiles, + $user + ); + } + + /** + * @Then the propfind/search result should contain :numFiles files/entries + * + * @param int $numFiles + * + * @return void + */ + public function propfindResultShouldContainNumEntries(int $numFiles):void { + //if we are using that step the second time in a scenario e.g. 'But ... should not' + //then don't parse the result again, because the result in a ResponseInterface + if (empty($this->responseXml)) { + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + } + Assert::assertIsArray( + $this->responseXml, + __METHOD__ . " responseXml is not an array" + ); + Assert::assertArrayHasKey( + "value", + $this->responseXml, + __METHOD__ . " responseXml does not have key 'value'" + ); + $multistatusResults = $this->responseXml["value"]; + if ($multistatusResults === null) { + $multistatusResults = []; + } + Assert::assertEquals( + (int) $numFiles, + \count($multistatusResults), + __METHOD__ + . " Expected result to contain '" + . (int) $numFiles + . "' files/entries, but got '" + . \count($multistatusResults) + . "' files/entries." + ); + } + + /** + * @Then the propfind/search result should contain any :expectedNumber of these files/entries: + * + * @param integer $expectedNumber + * @param TableNode $expectedFiles + * + * @return void + * @throws Exception + */ + public function theSearchResultShouldContainAnyOfTheseEntries( + int $expectedNumber, + TableNode $expectedFiles + ):void { + $this->theSearchResultOfUserShouldContainAnyOfTheseEntries( + $this->getCurrentUser(), + $expectedNumber, + $expectedFiles + ); + } + + /** + * @Then the propfind/search result of user :user should contain any :expectedNumber of these files/entries: + * + * @param string $user + * @param integer $expectedNumber + * @param TableNode $expectedFiles + * + * @return void + * @throws Exception + */ + public function theSearchResultOfUserShouldContainAnyOfTheseEntries( + string $user, + int $expectedNumber, + TableNode $expectedFiles + ):void { + $user = $this->getActualUsername($user); + $this->verifyTableNodeColumnsCount($expectedFiles, 1); + $this->propfindResultShouldContainNumEntries($expectedNumber); + $elementRows = $expectedFiles->getColumn(0); + // Remove any "/" from the front (or back) of the expected values passed + // into the step. findEntryFromPropfindResponse returns entries without + // any leading (or trailing) slash + $expectedEntries = \array_map( + function ($value) { + return \trim($value, "/"); + }, + $elementRows + ); + $resultEntries = $this->findEntryFromPropfindResponse(null, $user, "REPORT"); + foreach ($resultEntries as $resultEntry) { + Assert::assertContains($resultEntry, $expectedEntries); + } + } + + /** + * @When user :arg1 lists the resources in :path with depth :depth using the WebDAV API + * + * @param string $user + * @param string $path + * @param string $depth + * + * @return void + * @throws Exception + */ + public function userListsTheResourcesInPathWithDepthUsingTheWebdavApi(string $user, string $path, string $depth):void { + $response = $this->listFolder( + $user, + $path, + $depth + ); + $this->setResponse($response); + $this->setResponseXml(HttpRequestHelper::parseResponseAsXml($this->response)); + } + + /** + * @Then the last DAV response for user :user should contain these nodes/elements + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theLastDavResponseShouldContainTheseNodes(string $user, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["name"]); + foreach ($table->getHash() as $row) { + $path = $this->substituteInLineCodes($row['name']); + $res = $this->findEntryFromPropfindResponse($path, $user); + Assert::assertNotFalse($res, "expected $path to be in DAV response but was not found"); + } + } + + /** + * @Then the last DAV response for user :user should not contain these nodes/elements + * + * @param string $user + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theLastDavResponseShouldNotContainTheseNodes(string $user, TableNode $table):void { + $this->verifyTableNodeColumns($table, ["name"]); + foreach ($table->getHash() as $row) { + $path = $this->substituteInLineCodes($row['name']); + $res = $this->findEntryFromPropfindResponse($path, $user); + Assert::assertFalse($res, "expected $path to not be in DAV response but was found"); + } + } + + /** + * @Then the last public link DAV response should contain these nodes/elements + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theLastPublicDavResponseShouldContainTheseNodes(TableNode $table):void { + $user = $this->getLastPublicShareToken(); + $this->verifyTableNodeColumns($table, ["name"]); + $type = $this->usingOldDavPath ? "public-files" : "public-files-new"; + foreach ($table->getHash() as $row) { + $path = $this->substituteInLineCodes($row['name']); + $res = $this->findEntryFromPropfindResponse($path, $user, null, $type); + Assert::assertNotFalse($res, "expected $path to be in DAV response but was not found"); + } + } + + /** + * @Then the last public link DAV response should not contain these nodes/elements + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theLastPublicDavResponseShouldNotContainTheseNodes(TableNode $table):void { + $user = $this->getLastPublicShareToken(); + $this->verifyTableNodeColumns($table, ["name"]); + $type = $this->usingOldDavPath ? "public-files" : "public-files-new"; + foreach ($table->getHash() as $row) { + $path = $this->substituteInLineCodes($row['name']); + $res = $this->findEntryFromPropfindResponse($path, $user, null, $type); + Assert::assertFalse($res, "expected $path to not be in DAV response but was found"); + } + } + + /** + * @When the public lists the resources in the last created public link with depth :depth using the WebDAV API + * + * @param string $depth + * + * @return void + * @throws Exception + */ + public function thePublicListsTheResourcesInTheLastCreatedPublicLinkWithDepthUsingTheWebdavApi(string $depth):void { + $user = $this->getLastPublicShareToken(); + $response = $this->listFolder( + $user, + '/', + $depth, + null, + $this->usingOldDavPath ? "public-files" : "public-files-new" + ); + $this->setResponse($response); + $this->setResponseXml(HttpRequestHelper::parseResponseAsXml($this->response)); + } + + /** + * @param string|null $user + * + * @return array + */ + public function findEntryFromReportResponse(?string $user):array { + $responseXmlObj = $this->getResponseXmlObject(); + $responseResources = []; + $hrefs = $responseXmlObj->xpath('//d:href'); + foreach ($hrefs as $href) { + $hrefParts = \explode("/", (string)$href[0]); + if (\in_array($user, $hrefParts)) { + $entry = \urldecode(\end($hrefParts)); + \array_push($responseResources, $entry); + } else { + throw new Error("Expected user: $hrefParts[5] but found: $user"); + } + } + return $responseResources; + } + + /** + * parses a PROPFIND response from $this->response into xml + * and returns found search results if found else returns false + * + * @param string|null $user + * + * @return int + */ + public function getNumberOfEntriesInPropfindResponse( + ?string $user = null + ):int { + $multistatusResults = $this->getMultistatusResultFromPropfindResult($user); + return \count($multistatusResults); + } + + /** + * parses a PROPFIND response from $this->response + * and returns multistatus data from the response + * + * @param string|null $user + * + * @return array + */ + public function getMultistatusResultFromPropfindResult( + ?string $user = null + ):array { + //if we are using that step the second time in a scenario e.g. 'But ... should not' + //then don't parse the result again, because the result in a ResponseInterface + if (empty($this->responseXml)) { + $this->setResponseXml( + HttpRequestHelper::parseResponseAsXml($this->response) + ); + } + Assert::assertNotEmpty($this->responseXml, __METHOD__ . ' Response is empty'); + if ($user === null) { + $user = $this->getCurrentUser(); + } + + Assert::assertIsArray( + $this->responseXml, + __METHOD__ . " responseXml for user $user is not an array" + ); + Assert::assertArrayHasKey( + "value", + $this->responseXml, + __METHOD__ . " responseXml for user $user does not have key 'value'" + ); + $multistatus = $this->responseXml["value"]; + if ($multistatus == null) { + $multistatus = []; + } + return $multistatus; + } + + /** + * Escapes the given string for + * 1. Space --> %20 + * 2. Opening Small Bracket --> %28 + * 3. Closing Small Bracket --> %29 + * + * @param string $path - File path to parse + * + * @return string + */ + public function escapePath(string $path): string { + return \str_replace([" ", "(", ")"], ["%20", "%28", "%29"], $path); + } + + /** + * parses a PROPFIND response from $this->response into xml + * and returns found search results if found else returns false + * + * @param string|null $entryNameToSearch + * @param string|null $user + * @param string|null $method + * @param string $type + * @param string $folderPath + * + * @return string|array|boolean + * + * string if $entryNameToSearch is given and is found + * array if $entryNameToSearch is not given + * boolean false if $entryNameToSearch is given and is not found + * + * @throws GuzzleException + */ + public function findEntryFromPropfindResponse( + ?string $entryNameToSearch = null, + ?string $user = null, + ?string $method = null, + string $type = "files", + string $folderPath = '' + ) { + $trimmedEntryNameToSearch = ''; + // trim any leading "/" passed by the caller, we can just match the "raw" name + if ($entryNameToSearch != null) { + $trimmedEntryNameToSearch = \trim($entryNameToSearch, "/"); + } + // url encode for spaces and brackets that may appear in the filePath + $folderPath = $this->escapePath($folderPath); + // topWebDavPath should be something like /remote.php/webdav/ or + // /remote.php/dav/files/alice/ + $topWebDavPath = "/" . $this->getFullDavFilesPath($user) . "/" . $folderPath; + switch ($type) { + case "files": + break; + case "public-files": + case "public-files-old": + case "public-files-new": + $topWebDavPath = "/" . $this->getPublicLinkDavPath($user, $type) . "/"; + break; + default: + throw new Exception("error"); + } + $multistatusResults = $this->getMultistatusResultFromPropfindResult($user); + $results = []; + if ($multistatusResults !== null) { + foreach ($multistatusResults as $multistatusResult) { + $entryPath = $multistatusResult['value'][0]['value']; + if (OcisHelper::isTestingOnOcis() && $method === "REPORT") { + if ($entryNameToSearch !== null && str_ends_with($entryPath, $entryNameToSearch)) { + return $multistatusResult; + } else { + $spaceId = (WebDavHelper::$SPACE_ID_FROM_OCIS) ? WebDavHelper::$SPACE_ID_FROM_OCIS : WebDavHelper::getPersonalSpaceIdForUser( + $this->getBaseUrl(), + $user, + $this->getPasswordForUser($user), + $this->getStepLineRef() + ); + $topWebDavPath = "/remote.php/dav/spaces/" . $spaceId . "/" . $folderPath; + } + } + $entryName = \str_replace($topWebDavPath, "", $entryPath); + $entryName = \rawurldecode($entryName); + $entryName = \trim($entryName, "/"); + if ($trimmedEntryNameToSearch === $entryName) { + return $multistatusResult; + } + \array_push($results, $entryName); + } + } + if ($entryNameToSearch === null) { + return $results; + } + return false; + } + + /** + * Prevent creating two uploads and/or deletes with the same "stime" + * That is based on seconds in some implementations. + * This prevents duplication of etags or other problems with + * trashbin/versions save/restore. + * + * Set env var UPLOAD_DELETE_WAIT_TIME to 1 to activate a 1-second pause. + * By default, there is no pause. That allows testing of implementations + * which should be able to cope with multiple upload/delete actions in the + * same second. + * + * @return void + */ + public function pauseUploadDelete():void { + $time = \time(); + $uploadWaitTime = \getenv("UPLOAD_DELETE_WAIT_TIME"); + + $uploadWaitTime = $uploadWaitTime ? (int)$uploadWaitTime : 0; + + if (($this->lastUploadDeleteTime !== null) + && ($uploadWaitTime > 0) + && (($time - $this->lastUploadDeleteTime) < $uploadWaitTime) + ) { + \sleep($uploadWaitTime); + } + } + + /** + * reset settings if they were set in the scenario + * + * @AfterScenario + * + * @return void + * @throws Exception + */ + public function resetPreviousSettingsAfterScenario():void { + if ($this->previousAsyncSetting === "") { + SetupHelper::runOcc( + ['config:system:delete', 'dav.enable.async'], + $this->getStepLineRef() + ); + } elseif ($this->previousAsyncSetting !== null) { + SetupHelper::runOcc( + [ + 'config:system:set', + 'dav.enable.async', + '--type', + 'boolean', + '--value', + $this->previousAsyncSetting + ], + $this->getStepLineRef() + ); + } + if ($this->previousDavSlowdownSetting === "") { + SetupHelper::runOcc( + ['config:system:delete', 'dav.slowdown'], + $this->getStepLineRef() + ); + } elseif ($this->previousDavSlowdownSetting !== null) { + SetupHelper::runOcc( + [ + 'config:system:set', + 'dav.slowdown', + '--value', + $this->previousDavSlowdownSetting + ], + $this->getStepLineRef() + ); + } + } + + /** + * @Given /^the administrator has (enabled|disabled) the file version storage feature/ + * + * @param string $enabledOrDisabled + * + * @return void + * @throws Exception + */ + public function theAdministratorHasEnabledTheFileVersionStorage(string $enabledOrDisabled): void { + $switch = ($enabledOrDisabled !== "disabled"); + if ($switch) { + $value = 'true'; + } else { + $value = 'false'; + } + $this->runOcc( + [ + 'config:system:set', + 'file_storage.save_version_author', + '--type', + 'boolean', + '--value', + $value] + ); + } + + /** + * @Then the author of the created version with index :index should be :expectedUsername + * + * @param string $index + * @param string $expectedUsername + * + * @return void + * @throws Exception + */ + public function theAuthorOfEditedVersionFile(string $index, string $expectedUsername): void { + $expectedUserDisplayName = $this->getUserDisplayName($expectedUsername); + $resXml = $this->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->getResponse(), + __METHOD__ + ); + $this->setResponseXmlObject($resXml); + } + + // the username should be in oc:meta-version-edited-by + $xmlPart = $resXml->xpath("//oc:meta-version-edited-by//text()"); + if (!isset($xmlPart[$index - 1])) { + Assert::fail( + 'could not find version with index "' . $index . '" for oc:meta-version-edited-by property in response to user "' . $this->responseUser . '"' + ); + } + $actualUser = $xmlPart[$index - 1][0]; + Assert::assertEquals( + $expectedUsername, + $actualUser, + "Expected user of version with index $index in response to user '$this->responseUser' was '$expectedUsername', but got '$actualUser'" + ); + + // the user's display name should be in oc:meta-version-edited-by-name + $xmlPart = $resXml->xpath("//oc:meta-version-edited-by-name//text()"); + if (!isset($xmlPart[$index - 1])) { + Assert::fail( + 'could not find version with index "' . $index . '" for oc:meta-version-edited-by-name property in response to user "' . $this->responseUser . '"' + ); + } + $actualUserDisplayName = $xmlPart[$index - 1][0]; + Assert::assertEquals( + $expectedUserDisplayName, + $actualUserDisplayName, + "Expected display name of version with index $index in response to user '$this->responseUser' was '$expectedUsername', but got '$actualUser'" + ); + } +} diff --git a/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php b/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php new file mode 100644 index 000000000..62275ef27 --- /dev/null +++ b/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php @@ -0,0 +1,1396 @@ + + * @copyright Copyright (c) 2019, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * 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 + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; +use TestHelpers\Asserts\WebDav as WebDavTest; +use TestHelpers\HttpRequestHelper; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * Steps that relate to managing file/folder properties via WebDav + */ +class WebDavPropertiesContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + /** + * @var array map with user as key and another map as value, + * which has path as key and etag as value + */ + private $storedETAG = null; + + /** + * @When /^user "([^"]*)" gets the properties of (?:file|folder|entry) "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $path + * + * @return void + * @throws Exception + */ + public function userGetsThePropertiesOfFolder( + string $user, + string $path + ):void { + $this->featureContext->setResponseXmlObject( + $this->featureContext->listFolderAndReturnResponseXml( + $user, + $path, + '0' + ) + ); + } + + /** + * @When /^user "([^"]*)" gets the properties of (?:file|folder|entry) "([^"]*)" with depth (\d+) using the WebDAV API$/ + * + * @param string $user + * @param string $path + * @param string $depth + * + * @return void + * @throws Exception + */ + public function userGetsThePropertiesOfFolderWithDepth( + string $user, + string $path, + string $depth + ):void { + $this->featureContext->setResponseXmlObject( + $this->featureContext->listFolderAndReturnResponseXml( + $user, + $path, + $depth + ) + ); + } + + /** + * @When /^user "([^"]*)" gets the following properties of (?:file|folder|entry) "([^"]*)" using the WebDAV API$/ + * + * @param string $user + * @param string $path + * @param TableNode|null $propertiesTable + * + * @return void + * @throws Exception + */ + public function userGetsPropertiesOfFolder( + string $user, + string $path, + TableNode $propertiesTable + ):void { + $user = $this->featureContext->getActualUsername($user); + $properties = null; + $this->featureContext->verifyTableNodeColumns($propertiesTable, ["propertyName"]); + $this->featureContext->verifyTableNodeColumnsCount($propertiesTable, 1); + if ($propertiesTable instanceof TableNode) { + foreach ($propertiesTable->getColumnsHash() as $row) { + $properties[] = $row["propertyName"]; + } + } + $depth = "1"; + $this->featureContext->setResponseXmlObject( + $this->featureContext->listFolderAndReturnResponseXml( + $user, + $path, + $depth, + $properties + ) + ); + $this->featureContext->pushToLastStatusCodesArrays(); + } + + /** + * @param string $user + * @param string $path + * @param TableNode $propertiesTable + * @param string $depth + * + * @return void + * @throws Exception + */ + public function getFollowingCommentPropertiesOfFileUsingWebDAVPropfindApi( + string $user, + string $path, + TableNode $propertiesTable, + string $depth = "1" + ):void { + $properties = null; + $this->featureContext->verifyTableNodeColumns($propertiesTable, ["propertyName"]); + $this->featureContext->verifyTableNodeColumnsCount($propertiesTable, 1); + if ($propertiesTable instanceof TableNode) { + foreach ($propertiesTable->getColumnsHash() as $row) { + $properties[] = $row["propertyName"]; + } + } + + $user = $this->featureContext->getActualUsername($user); + $fileId = $this->featureContext->getFileIdForPath($user, $path); + $commentsPath = "/comments/files/$fileId/"; + $this->featureContext->setResponseXmlObject( + $this->featureContext->listFolderAndReturnResponseXml( + $user, + $commentsPath, + $depth, + $properties, + "comments" + ) + ); + } + + /** + * @When user :user gets the following comment properties of file :path using the WebDAV PROPFIND API + * + * @param string $user + * @param string $path + * @param TableNode $propertiesTable + * + * @return void + * @throws Exception + */ + public function userGetsFollowingCommentPropertiesOfFileUsingWebDAVPropfindApi(string $user, string $path, TableNode $propertiesTable) { + $this->getFollowingCommentPropertiesOfFileUsingWebDAVPropfindApi( + $user, + $path, + $propertiesTable + ); + } + + /** + * @When the user gets the following comment properties of file :arg1 using the WebDAV PROPFIND API + * + * @param string $path + * @param TableNode $propertiesTable + * + * @return void + * @throws Exception + */ + public function theUserGetsFollowingCommentPropertiesOfFileUsingWebDAVPropfindApi(string $path, TableNode $propertiesTable) { + $this->getFollowingCommentPropertiesOfFileUsingWebDAVPropfindApi( + $this->featureContext->getCurrentUser(), + $path, + $propertiesTable + ); + } + + /** + * @When /^the user gets the following properties of (?:file|folder|entry) "([^"]*)" using the WebDAV API$/ + * + * @param string $path + * @param TableNode|null $propertiesTable + * + * @return void + * @throws Exception + */ + public function theUserGetsPropertiesOfFolder(string $path, TableNode $propertiesTable) { + $this->userGetsPropertiesOfFolder( + $this->featureContext->getCurrentUser(), + $path, + $propertiesTable + ); + } + + /** + * @Given /^user "([^"]*)" has set the following properties of (?:file|folder|entry) "([^"]*)" using the WebDav API$/ + * + * if no namespace prefix is provided before property, default `oc:` prefix is set for added props + * only and everything rest on xml is set to prefix `d:` + * + * @param string $username + * @param string $path + * @param TableNode|null $propertiesTable with following columns with column header as: + * property: name of prop to be set + * value: value of prop to be set + * + * @return void + * @throws Exception + */ + public function userHasSetFollowingPropertiesUsingProppatch(string $username, string $path, TableNode $propertiesTable) { + $username = $this->featureContext->getActualUsername($username); + $this->featureContext->verifyTableNodeColumns($propertiesTable, ['propertyName', 'propertyValue']); + $properties = $propertiesTable->getColumnsHash(); + $this->featureContext->setResponse( + WebDavHelper::proppatchWithMultipleProps( + $this->featureContext->getBaseUrl(), + $username, + $this->featureContext->getPasswordForUser($username), + $path, + $properties, + $this->featureContext->getStepLineRef(), + $this->featureContext->getDavPathVersion() + ) + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @When user :user gets a custom property :propertyName with namespace :namespace of file :path + * + * @param string $user + * @param string $propertyName + * @param string $namespace namespace in form of "x1='http://whatever.org/ns'" + * @param string $path + * + * @return void + * @throws Exception + */ + public function userGetsPropertiesOfFile( + string $user, + string $propertyName, + string $namespace, + string $path + ):void { + $user = $this->featureContext->getActualUsername($user); + $properties = [ + $namespace => $propertyName + ]; + $this->featureContext->setResponse( + WebDavHelper::propfind( + $this->featureContext->getBaseUrl(), + $this->featureContext->getActualUsername($user), + $this->featureContext->getUserPassword($user), + $path, + $properties, + $this->featureContext->getStepLineRef(), + "0", + "files", + $this->featureContext->getDavPathVersion() + ) + ); + } + + /** + * @When /^the public gets the following properties of (?:file|folder|entry) "([^"]*)" in the last created public link using the WebDAV API$/ + * + * @param string $path + * @param TableNode $propertiesTable + * + * @return void + * @throws Exception + */ + public function publicGetsThePropertiesOfFolder(string $path, TableNode $propertiesTable):void { + $user = $this->featureContext->getLastPublicShareToken(); + $properties = null; + if ($propertiesTable instanceof TableNode) { + foreach ($propertiesTable->getRows() as $row) { + $properties[] = $row[0]; + } + } + $this->featureContext->setResponseXmlObject( + $this->featureContext->listFolderAndReturnResponseXml( + $user, + $path, + '0', + $properties, + $this->featureContext->getDavPathVersion() === 1 ? "public-files" : "public-files-new" + ) + ); + } + + /** + * @param string $user user id who sets the property + * @param string $propertyName name of property in Clark notation + * @param string $namespace namespace in form of "x1='http://whatever.org/ns'" + * @param string $path path on which to set properties to + * @param string $propertyValue property value + * + * @return void + * @throws Exception + */ + public function setPropertyWithNamespaceOfResource( + string $user, + string $propertyName, + string $namespace, + string $path, + string $propertyValue + ):void { + $user = $this->featureContext->getActualUsername($user); + $response = WebDavHelper::proppatch( + $this->featureContext->getBaseUrl(), + $this->featureContext->getActualUsername($user), + $this->featureContext->getUserPassword($user), + $path, + $propertyName, + $propertyValue, + $this->featureContext->getStepLineRef(), + $namespace, + $this->featureContext->getDavPathVersion() + ); + $this->featureContext->setResponse($response); + } + + /** + * @When /^user "([^"]*)" sets property "([^"]*)" with namespace "([^"]*)" of (?:file|folder|entry) "([^"]*)" to "([^"]*)" using the WebDAV API$/ + * + * @param string $user user id who sets the property + * @param string $propertyName name of property in Clark notation + * @param string $namespace namespace in form of "x1='http://whatever.org/ns'" + * @param string $path path on which to set properties to + * @param string $propertyValue property value + * + * @return void + * @throws Exception + */ + public function userSetsPropertyWithNamespaceOfEntryTo( + string $user, + string $propertyName, + string $namespace, + string $path, + string $propertyValue + ):void { + $this->setPropertyWithNamespaceOfResource( + $user, + $propertyName, + $namespace, + $path, + $propertyValue + ); + } + + /** + * @Given /^user "([^"]*)" has set property "([^"]*)" with namespace "([^"]*)" of (?:file|folder|entry) "([^"]*)" to "([^"]*)"$/ + * + * @param string $user user id who sets the property + * @param string $propertyName name of property in Clark notation + * @param string $namespace namespace in form of "x1='http://whatever.org/ns'" + * @param string $path path on which to set properties to + * @param string $propertyValue property value + * + * @return void + * @throws Exception + */ + public function userHasSetPropertyWithNamespaceOfEntryTo( + string $user, + string $propertyName, + string $namespace, + string $path, + string $propertyValue + ):void { + $this->setPropertyWithNamespaceOfResource( + $user, + $propertyName, + $namespace, + $path, + $propertyValue + ); + $this->featureContext->theHTTPStatusCodeShouldBeSuccess(); + } + + /** + * @Then /^the response should contain a custom "([^"]*)" property with namespace "([^"]*)" and value "([^"]*)"$/ + * + * @param string $propertyName + * @param string $namespaceString + * @param string $propertyValue + * + * @return void + * @throws Exception + */ + public function theResponseShouldContainACustomPropertyWithValue( + string $propertyName, + string $namespaceString, + string $propertyValue + ):void { + $this->featureContext->setResponseXmlObject( + HttpRequestHelper::getResponseXml( + $this->featureContext->getResponse(), + __METHOD__ + ) + ); + $responseXmlObject = $this->featureContext->getResponseXmlObject(); + //calculate the namespace prefix and namespace + $matches = []; + \preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches); + $nameSpace = $matches[2]; + $nameSpacePrefix = $matches[1]; + $responseXmlObject->registerXPathNamespace( + $nameSpacePrefix, + $nameSpace + ); + $xmlPart = $responseXmlObject->xpath( + "//d:prop/" . "$nameSpacePrefix:$propertyName" + ); + Assert::assertArrayHasKey( + 0, + $xmlPart, + "Cannot find property \"$propertyName\"" + ); + Assert::assertEquals( + $propertyValue, + $xmlPart[0]->__toString(), + "\"$propertyName\" has a value \"" . + $xmlPart[0]->__toString() . "\" but \"$propertyValue\" expected" + ); + } + + /** + * @Then /^the response should contain a custom "([^"]*)" property with namespace "([^"]*)" and complex value "(([^"\\]|\\.)*)"$/ + * + * @param string $propertyName + * @param string $namespaceString + * @param string $propertyValue + * + * @return void + * @throws Exception + */ + public function theResponseShouldContainACustomPropertyWithComplexValue( + string $propertyName, + string $namespaceString, + string $propertyValue + ):void { + // let's unescape quotes first + $propertyValue = \str_replace('\"', '"', $propertyValue); + $this->featureContext->setResponseXmlObject( + HttpRequestHelper::getResponseXml( + $this->featureContext->getResponse(), + __METHOD__ + ) + ); + $responseXmlObject = $this->featureContext->getResponseXmlObject(); + //calculate the namespace prefix and namespace + $matches = []; + \preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches); + $nameSpace = $matches[2]; + $nameSpacePrefix = $matches[1]; + $responseXmlObject->registerXPathNamespace( + $nameSpacePrefix, + $nameSpace + ); + $xmlPart = $responseXmlObject->xpath( + "//d:prop/" . "$nameSpacePrefix:$propertyName" . "/*" + ); + Assert::assertArrayHasKey( + 0, + $xmlPart, + "Cannot find property \"$propertyName\"" + ); + Assert::assertEquals( + $propertyValue, + $xmlPart[0]->asXML(), + "\"$propertyName\" has a value \"" . + $xmlPart[0]->asXML() . "\" but \"$propertyValue\" expected" + ); + } + + /** + * @Then the single response should contain a property :property with a child property :childProperty + * + * @param string $property + * @param string $childProperty + * + * @return void + * + * @throws Exception + */ + public function theSingleResponseShouldContainAPropertyWithChildProperty( + string $property, + string $childProperty + ):void { + $xmlPart = $this->featureContext->getResponseXmlObject()->xpath( + "//d:prop/$property/$childProperty" + ); + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find property \"$property/$childProperty\"" + ); + } + + /** + * @Then the single response should contain a property :key with value :value + * + * @param string $key + * @param string $expectedValue + * + * @return void + * @throws Exception + */ + public function theSingleResponseShouldContainAPropertyWithValue( + string $key, + string $expectedValue + ):void { + $this->checkSingleResponseContainsAPropertyWithValueAndAlternative( + $key, + $expectedValue, + $expectedValue + ); + } + + /** + * @Then the single response about the file owned by :user should contain a property :key with value :value + * + * @param string $user + * @param string $key + * @param string $expectedValue + * + * @return void + * @throws Exception + */ + public function theSingleResponseAboutTheFileOwnedByShouldContainAPropertyWithValue( + string $user, + string $key, + string $expectedValue + ):void { + $this->checkSingleResponseContainsAPropertyWithValueAndAlternative( + $key, + $expectedValue, + $expectedValue, + $user + ); + } + + /** + * @Then the single response should contain a property :key with value :value or with value :altValue + * + * @param string $key + * @param string $expectedValue + * @param string $altExpectedValue + * + * @return void + * @throws Exception + */ + public function theSingleResponseShouldContainAPropertyWithValueAndAlternative( + string $key, + string $expectedValue, + string $altExpectedValue + ):void { + $this->checkSingleResponseContainsAPropertyWithValueAndAlternative( + $key, + $expectedValue, + $altExpectedValue + ); + } + + /** + * @param string $key + * @param string $expectedValue + * @param string $altExpectedValue + * @param string|null $user + * + * @return void + * @throws Exception + */ + public function checkSingleResponseContainsAPropertyWithValueAndAlternative( + string $key, + string $expectedValue, + string $altExpectedValue, + ?string $user = null + ):void { + $xmlPart = $this->featureContext->getResponseXmlObject()->xpath( + "//d:prop/$key" + ); + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find property \"$key\"" + ); + $value = $xmlPart[0]->__toString(); + $expectedValue = $this->featureContext->substituteInLineCodes( + $expectedValue, + $user + ); + $expectedValue = "#^$expectedValue$#"; + $altExpectedValue = "#^$altExpectedValue$#"; + if (\preg_match($expectedValue, $value) !== 1 + && \preg_match($altExpectedValue, $value) !== 1 + ) { + Assert::fail( + "Property \"$key\" found with value \"$value\", " . + "expected \"$expectedValue\" or \"$altExpectedValue\"" + ); + } + } + + /** + * @Then the value of the item :xpath in the response should be :value + * + * @param string $xpath + * @param string $expectedValue + * + * @return void + * @throws Exception + */ + public function assertValueOfItemInResponseIs(string $xpath, string $expectedValue):void { + $this->assertValueOfItemInResponseAboutUserIs( + $xpath, + null, + $expectedValue + ); + } + + /** + * @Then the value of the item :xpath in the response about user :user should be :value + * + * @param string $xpath + * @param string|null $user + * @param string $expectedValue + * + * @return void + * @throws Exception + */ + public function assertValueOfItemInResponseAboutUserIs(string $xpath, ?string $user, string $expectedValue):void { + $resXml = $this->featureContext->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->featureContext->getResponse(), + __METHOD__ + ); + } + $xmlPart = $resXml->xpath($xpath); + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find item with xpath \"$xpath\"" + ); + $value = $xmlPart[0]->__toString(); + $user = $this->featureContext->getActualUsername($user); + $expectedValue = $this->featureContext->substituteInLineCodes( + $expectedValue, + $user + ); + + // The expected value can contain /%base_path%/ which can be empty some time + // This will result in urls such as //remote.php, so replace that + $expectedValue = preg_replace("/\/\/remote\.php/i", "/remote.php", $expectedValue); + Assert::assertEquals( + $expectedValue, + $value, + "item \"$xpath\" found with value \"$value\", " . + "expected \"$expectedValue\"" + ); + } + + /** + * @Then the value of the item :xpath in the response about user :user should be :value1 or :value2 + * + * @param string $xpath + * @param string|null $user + * @param string $expectedValue1 + * @param string $expectedValue2 + * + * @return void + * @throws Exception + */ + public function assertValueOfItemInResponseAboutUserIsEitherOr(string $xpath, ?string $user, string $expectedValue1, string $expectedValue2):void { + if (!$expectedValue2) { + $expectedValue2 = $expectedValue1; + } + $resXml = $this->featureContext->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->featureContext->getResponse(), + __METHOD__ + ); + } + $xmlPart = $resXml->xpath($xpath); + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find item with xpath \"$xpath\"" + ); + $value = $xmlPart[0]->__toString(); + $user = $this->featureContext->getActualUsername($user); + $expectedValue1 = $this->featureContext->substituteInLineCodes( + $expectedValue1, + $user + ); + + $expectedValue2 = $this->featureContext->substituteInLineCodes( + $expectedValue2, + $user + ); + + // The expected value can contain /%base_path%/ which can be empty some time + // This will result in urls such as //remote.php, so replace that + $expectedValue1 = preg_replace("/\/\/remote\.php/i", "/remote.php", $expectedValue1); + $expectedValue2 = preg_replace("/\/\/remote\.php/i", "/remote.php", $expectedValue2); + $expectedValues = [$expectedValue1, $expectedValue2]; + $isExpectedValueInMessage = \in_array($value, $expectedValues); + Assert::assertTrue($isExpectedValueInMessage, "The actual value \"$value\" is not one of the expected values: \"$expectedValue1\" or \"$expectedValue2\""); + } + + /** + * @Then the value of the item :xpath in the response should match :value + * + * @param string $xpath + * @param string $pattern + * + * @return void + * @throws Exception + */ + public function assertValueOfItemInResponseRegExp(string $xpath, string $pattern):void { + $this->assertValueOfItemInResponseToUserRegExp( + $xpath, + null, + $pattern + ); + } + + /** + * @Then /^as a public the lock discovery property "([^"]*)" of the (?:file|folder|entry) "([^"]*)" should match "([^"]*)"$/ + * + * @param string $xpath + * @param string $path + * @param string $pattern + * + * @return void + * @throws Exception + */ + public function publicGetsThePropertiesOfFolderAndAssertValueOfItemInResponseRegExp(string $xpath, string $path, string $pattern):void { + $propertiesTable = new TableNode([['propertyName'],['d:lockdiscovery']]); + $this->publicGetsThePropertiesOfFolder($path, $propertiesTable); + + $this->featureContext->theHTTPStatusCodeShouldBe('200'); + $this->assertValueOfItemInResponseToUserRegExp( + $xpath, + null, + $pattern + ); + } + + /** + * @Then there should be an entry with href containing :expectedHref in the response to user :user + * + * @param string $expectedHref + * @param string $user + * + * @return void + * @throws Exception + */ + public function assertEntryWithHrefMatchingRegExpInResponseToUser(string $expectedHref, string $user):void { + $resXml = $this->featureContext->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->featureContext->getResponse(), + __METHOD__ + ); + } + + $user = $this->featureContext->getActualUsername($user); + $expectedHref = $this->featureContext->substituteInLineCodes( + $expectedHref, + $user, + ['preg_quote' => ['/']] + ); + + $index = 0; + while (true) { + $index++; + $xpath = "//d:response[$index]/d:href"; + $xmlPart = $resXml->xpath($xpath); + // If we have run out of entries in the response, then fail the test + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find any entry having href with value $expectedHref in response to $user" + ); + $value = $xmlPart[0]->__toString(); + $decodedValue = \rawurldecode($value); + // for folders, decoded value will be like: "/owncloud/core/remote.php/webdav/strängé folder/" + // expected href should be like: "remote.php/webdav/strängé folder/" + // for files, decoded value will be like: "/owncloud/core/remote.php/webdav/strängé folder/file.txt" + // expected href should be like: "remote.php/webdav/strängé folder/file.txt" + $explodeDecoded = \explode('/', $decodedValue); + // get the first item of the expected href. + // i.e remote.php from "remote.php/webdav/strängé folder/file.txt" + // or dav from "dav/spaces/%spaceid%/C++ file.cpp" + $explodeExpected = \explode('/', $expectedHref); + $remotePhpIndex = \array_search($explodeExpected[0], $explodeDecoded); + if ($remotePhpIndex) { + $explodedHrefPartArray = \array_slice($explodeDecoded, $remotePhpIndex); + $actualHrefPart = \implode('/', $explodedHrefPartArray); + if ($this->featureContext->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES) { + // for spaces webdav, space id is included in the href + // space id from our helper is returned as d8c029e0\-2bc9\-4b9a\-8613\-c727e5417f05 + // so we've to remove "\" before every "-" + $expectedHref = str_replace('\-', '-', $expectedHref); + $expectedHref = str_replace('\$', '$', $expectedHref); + $expectedHref = str_replace('\!', '!', $expectedHref); + } + if ($actualHrefPart === $expectedHref) { + break; + } + } + } + } + + /** + * @Then the value of the item :xpath in the response to user :user should match :value + * + * @param string $xpath + * @param string|null $user + * @param string $pattern + * + * @return void + * @throws Exception + */ + public function assertValueOfItemInResponseToUserRegExp(string $xpath, ?string $user, string $pattern):void { + $resXml = $this->featureContext->getResponseXmlObject(); + if ($resXml === null) { + $resXml = HttpRequestHelper::getResponseXml( + $this->featureContext->getResponse(), + __METHOD__ + ); + } + $xmlPart = $resXml->xpath($xpath); + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find item with xpath \"$xpath\"" + ); + $user = $this->featureContext->getActualUsername($user); + $value = $xmlPart[0]->__toString(); + $pattern = $this->featureContext->substituteInLineCodes( + $pattern, + $user, + ['preg_quote' => ['/']] + ); + Assert::assertMatchesRegularExpression( + $pattern, + $value, + "item \"$xpath\" found with value \"$value\", " . + "expected to match regex pattern: \"$pattern\"" + ); + } + + /** + * @Then /^as user "([^"]*)" the lock discovery property "([^"]*)" of the (?:file|folder|entry) "([^"]*)" should match "([^"]*)"$/ + * + * @param string|null $user + * @param string $xpath + * @param string $path + * @param string $pattern + * + * @return void + * @throws Exception + */ + public function userGetsPropertiesOfFolderAndAssertValueOfItemInResponseToUserRegExp(string $user, string $xpath, string $path, string $pattern):void { + $propertiesTable = new TableNode([['propertyName'],['d:lockdiscovery']]); + $this->userGetsPropertiesOfFolder( + $user, + $path, + $propertiesTable + ); + + $this->featureContext->theHTTPStatusCodeShouldBe('200'); + $this->assertValueOfItemInResponseToUserRegExp( + $xpath, + $user, + $pattern + ); + } + + /** + * @Then the item :xpath in the response should not exist + * + * @param string $xpath + * + * @return void + * @throws Exception + */ + public function assertItemInResponseDoesNotExist(string $xpath):void { + $xmlPart = $this->featureContext->getResponseXmlObject()->xpath($xpath); + Assert::assertFalse( + isset($xmlPart[0]), + "Found item with xpath \"$xpath\" but it should not exist" + ); + } + + /** + * @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should contain a property "([^"]*)" with value "([^"]*)" or with value "([^"]*)"$/ + * + * @param string $user + * @param string $path + * @param string $property + * @param string $expectedValue + * @param string $altExpectedValue + * + * @return void + * @throws Exception + */ + public function asUserFolderShouldContainAPropertyWithValueOrWithValue( + string $user, + string $path, + string $property, + string $expectedValue, + string $altExpectedValue + ):void { + $this->featureContext->setResponseXmlObject( + $this->featureContext->listFolderAndReturnResponseXml( + $user, + $path, + '0', + [$property] + ) + ); + $this->theSingleResponseShouldContainAPropertyWithValueAndAlternative( + $property, + $expectedValue, + $altExpectedValue + ); + } + + /** + * @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should contain a property "([^"]*)" with value "([^"]*)"$/ + * + * @param string $user + * @param string $path + * @param string $property + * @param string $value + * + * @return void + * @throws Exception + */ + public function asUserFolderShouldContainAPropertyWithValue( + string $user, + string $path, + string $property, + string $value + ):void { + $this->asUserFolderShouldContainAPropertyWithValueOrWithValue( + $user, + $path, + $property, + $value, + $value + ); + } + + /** + * @Then the single response should contain a property :key with value like :regex + * + * @param string $key + * @param string $regex + * + * @return void + * @throws Exception + */ + public function theSingleResponseShouldContainAPropertyWithValueLike( + string $key, + string $regex + ):void { + $xmlPart = $this->featureContext->getResponseXmlObject()->xpath( + "//d:prop/$key" + ); + Assert::assertTrue( + isset($xmlPart[0]), + "Cannot find property \"$key\"" + ); + $value = $xmlPart[0]->__toString(); + Assert::assertMatchesRegularExpression( + $regex, + $value, + "Property \"$key\" found with value \"$value\", expected \"$regex\"" + ); + } + + /** + * @Then the response should contain a share-types property with + * + * @param TableNode $table + * + * @return void + * @throws Exception + */ + public function theResponseShouldContainAShareTypesPropertyWith(TableNode $table):void { + $this->featureContext->verifyTableNodeColumnsCount($table, 1); + WebdavTest::assertResponseContainsShareTypes( + $this->featureContext->getResponseXmlObject(), + $table->getRows() + ); + } + + /** + * @Then the response should contain an empty property :property + * + * @param string $property + * + * @return void + * @throws Exception + */ + public function theResponseShouldContainAnEmptyProperty(string $property):void { + $xmlPart = $this->featureContext->getResponseXmlObject()->xpath( + "//d:prop/$property" + ); + Assert::assertCount( + 1, + $xmlPart, + "Cannot find property \"$property\"" + ); + Assert::assertEmpty( + $xmlPart[0], + "Property \"$property\" is not empty" + ); + } + + /** + * @param string $user + * @param string $path + * @param string|null $storePath + * + * @return void + * @throws Exception + */ + public function storeEtagOfElement(string $user, string $path, ?string $storePath = ""):void { + if ($storePath === "") { + $storePath = $path; + } + $user = $this->featureContext->getActualUsername($user); + $propertiesTable = new TableNode([['propertyName'],['getetag']]); + $this->userGetsPropertiesOfFolder( + $user, + $path, + $propertiesTable + ); + $this->storedETAG[$user][$storePath] + = $this->featureContext->getEtagFromResponseXmlObject(); + } + + /** + * @When user :user stores etag of element :path using the WebDAV API + * + * @param string $user + * @param string $path + * + * @return void + * @throws Exception + */ + public function userStoresEtagOfElement(string $user, string $path):void { + $this->storeEtagOfElement( + $user, + $path + ); + } + + /** + * @Given user :user has stored etag of element :path on path :storePath + * + * @param string $user + * @param string $path + * @param string $storePath + * + * @return void + * @throws Exception + */ + public function userStoresEtagOfElementOnPath(string $user, string $path, string $storePath):void { + $user = $this->featureContext->getActualUsername($user); + $this->storeEtagOfElement( + $user, + $path, + $storePath + ); + if ($storePath == "") { + $storePath = $path; + } + if ($this->storedETAG[$user][$storePath] === null || $this->storedETAG[$user][$path] === "") { + throw new Exception("Expected stored etag to be some string but found null!"); + } + } + + /** + * @Given user :user has stored etag of element :path + * + * @param string $user + * @param string $path + * + * @return void + * @throws Exception + */ + public function userHasStoredEtagOfElement(string $user, string $path):void { + $user = $this->featureContext->getActualUsername($user); + $this->storeEtagOfElement( + $user, + $path + ); + if ($this->storedETAG[$user][$path] === "" || $this->storedETAG[$user][$path] === null) { + throw new Exception("Expected stored etag to be some string but found null!"); + } + } + + /** + * @Then /^the properties response should contain an etag$/ + * + * @return void + * @throws Exception + */ + public function thePropertiesResponseShouldContainAnEtag():void { + Assert::assertTrue( + $this->featureContext->isEtagValid(), + __METHOD__ + . " getetag not found in response" + ); + } + + /** + * @Then as user :username the last response should have the following properties + * + * only supports new DAV version + * + * @param string $username + * @param TableNode $expectedPropTable with following columns: + * resource: full path of resource(file/folder/entry) from root of your oc storage + * property: expected name of property to be asserted, eg: status, href, customPropName + * value: expected value of expected property + * + * @return void + * @throws Exception + */ + public function theResponseShouldHavePropertyWithValue(string $username, TableNode $expectedPropTable):void { + $username = $this->featureContext->getActualUsername($username); + $this->featureContext->verifyTableNodeColumns($expectedPropTable, ['resource', 'propertyName', 'propertyValue']); + $responseXmlObject = $this->featureContext->getResponseXmlObject(); + + $hrefSplittedUptoUsername = \explode("/", (string)$responseXmlObject->xpath("//d:href")[0]); + $xmlHrefSplittedArray = \array_slice( + $hrefSplittedUptoUsername, + 0, + \array_search($username, $hrefSplittedUptoUsername) + 1 + ); + $xmlHref = \implode("/", $xmlHrefSplittedArray); + foreach ($expectedPropTable->getColumnsHash() as $col) { + if ($col["propertyName"] === "status") { + $xmlPart = $responseXmlObject->xpath( + "//d:href[.='" . + $xmlHref . $col["resource"] . + "']/following-sibling::d:propstat//d:" . + $col["propertyName"] + ); + } else { + $xmlPart = $responseXmlObject->xpath( + "//d:href[.= '" . + $xmlHref . $col["resource"] . + "']/..//oc:" . $col["propertyName"] + ); + } + Assert::assertEquals( + $col["propertyValue"], + $xmlPart[0], + __METHOD__ + . " Expected '" . $col["propertyValue"] . "' but got '" . $xmlPart[0] . "'" + ); + } + } + + /** + * @param string $path + * @param string $user + * + * @return string + * @throws Exception + */ + public function getCurrentEtagOfElement(string $path, string $user):string { + $user = $this->featureContext->getActualUsername($user); + $propertiesTable = new TableNode([['propertyName'],['getetag']]); + $this->userGetsPropertiesOfFolder( + $user, + $path, + $propertiesTable + ); + return $this->featureContext->getEtagFromResponseXmlObject(); + } + + /** + * @param string $path + * @param string $user + * @param string $messageStart + * + * @return string + */ + public function getStoredEtagOfElement(string $path, string $user, string $messageStart = ''):string { + if ($messageStart === '') { + $messageStart = __METHOD__; + } + Assert::assertArrayHasKey( + $user, + $this->storedETAG, + $messageStart + . " Trying to check etag of element $path of user $user but the user does not have any stored etags" + ); + Assert::assertArrayHasKey( + $path, + $this->storedETAG[$user], + $messageStart + . " Trying to check etag of element $path of user $user but the user does not have a stored etag for the element" + ); + return $this->storedETAG[$user][$path]; + } + + /** + * @Then these etags should not have changed: + * + * @param TableNode $etagTable + * + * @return void + * @throws Exception + */ + public function theseEtagsShouldNotHaveChanged(TableNode $etagTable):void { + $this->featureContext->verifyTableNodeColumns($etagTable, ["user", "path"]); + $this->featureContext->verifyTableNodeColumnsCount($etagTable, 2); + $changedEtagCount = 0; + $changedEtagMessage = __METHOD__; + foreach ($etagTable->getColumnsHash() as $row) { + $user = $row["user"]; + $path = $row["path"]; + $user = $this->featureContext->getActualUsername($user); + $actualEtag = $this->getCurrentEtagOfElement($path, $user); + $storedEtag = $this->getStoredEtagOfElement($path, $user, __METHOD__); + if ($actualEtag !== $storedEtag) { + $changedEtagCount = $changedEtagCount + 1; + $changedEtagMessage + .= "\nThe etag '$storedEtag' of element '$path' of user '$user' changed to '$actualEtag'."; + } + } + Assert::assertEquals(0, $changedEtagCount, $changedEtagMessage); + } + + /** + * @Then the etag of element :path of user :user should not have changed + * + * @param string $path + * @param string $user + * + * @return void + * @throws Exception + */ + public function etagOfElementOfUserShouldNotHaveChanged(string $path, string $user):void { + $user = $this->featureContext->getActualUsername($user); + $actualEtag = $this->getCurrentEtagOfElement($path, $user); + $storedEtag = $this->getStoredEtagOfElement($path, $user, __METHOD__); + Assert::assertEquals( + $storedEtag, + $actualEtag, + __METHOD__ + . " The etag of element '$path' of user '$user' was not expected to change." + . " The stored etag was '$storedEtag' but got '$actualEtag' from the response" + ); + } + + /** + * @Then these etags should have changed: + * + * @param TableNode $etagTable + * + * @return void + * @throws Exception + */ + public function theseEtagsShouldHaveChanged(TableNode $etagTable):void { + $this->featureContext->verifyTableNodeColumns($etagTable, ["user", "path"]); + $this->featureContext->verifyTableNodeColumnsCount($etagTable, 2); + $unchangedEtagCount = 0; + $unchangedEtagMessage = __METHOD__; + foreach ($etagTable->getColumnsHash() as $row) { + $user = $row["user"]; + $path = $row["path"]; + $user = $this->featureContext->getActualUsername($user); + $actualEtag = $this->getCurrentEtagOfElement($path, $user); + $storedEtag = $this->getStoredEtagOfElement($path, $user, __METHOD__); + if ($actualEtag === $storedEtag) { + $unchangedEtagCount = $unchangedEtagCount + 1; + $unchangedEtagMessage + .= "\nThe etag '$storedEtag' of element '$path' of user '$user' did not change."; + } + } + Assert::assertEquals(0, $unchangedEtagCount, $unchangedEtagMessage); + } + + /** + * @Then the etag of element :path of user :user should have changed + * + * @param string $path + * @param string $user + * + * @return void + * @throws Exception + */ + public function etagOfElementOfUserShouldHaveChanged(string $path, string $user):void { + $user = $this->featureContext->getActualUsername($user); + $actualEtag = $this->getCurrentEtagOfElement($path, $user); + $storedEtag = $this->getStoredEtagOfElement($path, $user, __METHOD__); + Assert::assertNotEquals( + $storedEtag, + $actualEtag, + __METHOD__ + . " The etag of element '$path' of user '$user' was expected to change." + . " The stored etag was '$storedEtag' and also got '$actualEtag' from the response" + ); + } + + /** + * @Then the etag of element :path of user :user on server :server should have changed + * + * @param string $path + * @param string $user + * @param string $server + * + * @return void + * @throws Exception + */ + public function theEtagOfElementOfUserOnServerShouldHaveChanged( + string $path, + string $user, + string $server + ):void { + $previousServer = $this->featureContext->usingServer($server); + $this->etagOfElementOfUserShouldHaveChanged($path, $user); + $this->featureContext->usingServer($previousServer); + } + + /** + * @Then the etag of element :path of user :user on server :server should not have changed + * + * @param string $path + * @param string $user + * @param string $server + * + * @return void + * @throws Exception + */ + public function theEtagOfElementOfUserOnServerShouldNotHaveChanged( + string $path, + string $user, + string $server + ):void { + $previousServer = $this->featureContext->usingServer($server); + $this->etagOfElementOfUserShouldNotHaveChanged($path, $user); + $this->featureContext->usingServer($previousServer); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/bootstrap.php b/tests/acceptance/features/bootstrap/bootstrap.php index a8302894b..b6e18a60e 100644 --- a/tests/acceptance/features/bootstrap/bootstrap.php +++ b/tests/acceptance/features/bootstrap/bootstrap.php @@ -20,19 +20,44 @@ * */ -$pathToCore = \getenv('PATH_TO_CORE'); -if ($pathToCore === false) { - $pathToCore = "../core"; -} +use Composer\Autoload\ClassLoader; -require_once $pathToCore . '/tests/acceptance/features/bootstrap/bootstrap.php'; +$classLoader = new ClassLoader(); -$classLoader = new \Composer\Autoload\ClassLoader(); -$classLoader->addPsr4( - "", - $pathToCore . "/tests/acceptance/features/bootstrap", - true -); $classLoader->addPsr4("TestHelpers\\", __DIR__ . "/../../../TestHelpers", true); $classLoader->register(); + +// Sleep for 10 milliseconds +const STANDARD_SLEEP_TIME_MILLISEC = 10; +const STANDARD_SLEEP_TIME_MICROSEC = STANDARD_SLEEP_TIME_MILLISEC * 1000; + +// Long timeout for use in code that needs to wait for known slow UI +const LONG_UI_WAIT_TIMEOUT_MILLISEC = 60000; +// Default timeout for use in code that needs to wait for the UI +const STANDARD_UI_WAIT_TIMEOUT_MILLISEC = 10000; +// Minimum timeout for use in code that needs to wait for the UI +const MINIMUM_UI_WAIT_TIMEOUT_MILLISEC = 500; +const MINIMUM_UI_WAIT_TIMEOUT_MICROSEC = MINIMUM_UI_WAIT_TIMEOUT_MILLISEC * 1000; + +// Minimum timeout for emails +const EMAIL_WAIT_TIMEOUT_SEC = 10; +const EMAIL_WAIT_TIMEOUT_MILLISEC = EMAIL_WAIT_TIMEOUT_SEC * 1000; + +// Default number of times to retry where retries are useful +const STANDARD_RETRY_COUNT = 5; +// Minimum number of times to retry where retries are useful +const MINIMUM_RETRY_COUNT = 2; + +// The remote server-under-test might or might not happen to have this directory. +// If it does not exist, then the tests may end up creating it. +const ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER = "tests/acceptance"; + +// The following directory should NOT already exist on the remote server-under-test. +// Acceptance tests are free to do anything needed in this directory, and to +// delete it during or at the end of testing. +const TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER = ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER . "/server_tmp"; + +// The following directory is created, used, and deleted by tests that need to +// use some "local external storage" on the server. +const LOCAL_STORAGE_DIR_ON_REMOTE_SERVER = TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER . "/local_storage"; diff --git a/tests/acceptance/filesForUpload/'single'quotes.txt b/tests/acceptance/filesForUpload/'single'quotes.txt new file mode 100644 index 000000000..96abcb4f0 --- /dev/null +++ b/tests/acceptance/filesForUpload/'single'quotes.txt @@ -0,0 +1,9 @@ +a file with a single quote i its name + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/data.tar.gz b/tests/acceptance/filesForUpload/data.tar.gz new file mode 100644 index 000000000..05c74a335 Binary files /dev/null and b/tests/acceptance/filesForUpload/data.tar.gz differ diff --git a/tests/acceptance/filesForUpload/data.zip b/tests/acceptance/filesForUpload/data.zip new file mode 100644 index 000000000..d79780ead Binary files /dev/null and b/tests/acceptance/filesForUpload/data.zip differ diff --git a/tests/acceptance/filesForUpload/davtest.txt b/tests/acceptance/filesForUpload/davtest.txt new file mode 100644 index 000000000..09c3a96be --- /dev/null +++ b/tests/acceptance/filesForUpload/davtest.txt @@ -0,0 +1 @@ +Dav-Test \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/example.gif b/tests/acceptance/filesForUpload/example.gif new file mode 100644 index 000000000..9639afcdb Binary files /dev/null and b/tests/acceptance/filesForUpload/example.gif differ diff --git a/tests/acceptance/filesForUpload/file_to_overwrite.txt b/tests/acceptance/filesForUpload/file_to_overwrite.txt new file mode 100644 index 000000000..f45c3ef31 --- /dev/null +++ b/tests/acceptance/filesForUpload/file_to_overwrite.txt @@ -0,0 +1 @@ +BLABLABLA diff --git a/tests/acceptance/filesForUpload/lorem-big.txt b/tests/acceptance/filesForUpload/lorem-big.txt new file mode 100644 index 000000000..7c3df7df5 --- /dev/null +++ b/tests/acceptance/filesForUpload/lorem-big.txt @@ -0,0 +1,93 @@ +a big lorem file + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/lorem.txt b/tests/acceptance/filesForUpload/lorem.txt new file mode 100644 index 000000000..9d3b7a654 --- /dev/null +++ b/tests/acceptance/filesForUpload/lorem.txt @@ -0,0 +1,7 @@ +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/new-'single'quotes.txt b/tests/acceptance/filesForUpload/new-'single'quotes.txt new file mode 100644 index 000000000..603f2187a --- /dev/null +++ b/tests/acceptance/filesForUpload/new-'single'quotes.txt @@ -0,0 +1,9 @@ +a file with a single quote i its name, but different to the original one + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/new-data.tar.gz b/tests/acceptance/filesForUpload/new-data.tar.gz new file mode 100644 index 000000000..05c74a335 Binary files /dev/null and b/tests/acceptance/filesForUpload/new-data.tar.gz differ diff --git a/tests/acceptance/filesForUpload/new-data.zip b/tests/acceptance/filesForUpload/new-data.zip new file mode 100644 index 000000000..d79780ead Binary files /dev/null and b/tests/acceptance/filesForUpload/new-data.zip differ diff --git a/tests/acceptance/filesForUpload/new-lorem-big.txt b/tests/acceptance/filesForUpload/new-lorem-big.txt new file mode 100644 index 000000000..2c1c631e0 --- /dev/null +++ b/tests/acceptance/filesForUpload/new-lorem-big.txt @@ -0,0 +1,93 @@ +a big lorem file but different to the original one + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/new-lorem.txt b/tests/acceptance/filesForUpload/new-lorem.txt new file mode 100644 index 000000000..b935db537 --- /dev/null +++ b/tests/acceptance/filesForUpload/new-lorem.txt @@ -0,0 +1,9 @@ +lorem file that is different from the original one + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/new-strängé filename (duplicate #2 &).txt b/tests/acceptance/filesForUpload/new-strängé filename (duplicate #2 &).txt new file mode 100644 index 000000000..ac6c70ca5 --- /dev/null +++ b/tests/acceptance/filesForUpload/new-strängé filename (duplicate #2 &).txt @@ -0,0 +1,9 @@ +a file with funny characters in the file name but different from the original one + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/simple.odt b/tests/acceptance/filesForUpload/simple.odt new file mode 100644 index 000000000..c6b8b94bf Binary files /dev/null and b/tests/acceptance/filesForUpload/simple.odt differ diff --git a/tests/acceptance/filesForUpload/simple.pdf b/tests/acceptance/filesForUpload/simple.pdf new file mode 100644 index 000000000..305e543db Binary files /dev/null and b/tests/acceptance/filesForUpload/simple.pdf differ diff --git a/tests/acceptance/filesForUpload/strängé filename (duplicate #2 &).txt b/tests/acceptance/filesForUpload/strängé filename (duplicate #2 &).txt new file mode 100644 index 000000000..a771bf79e --- /dev/null +++ b/tests/acceptance/filesForUpload/strängé filename (duplicate #2 &).txt @@ -0,0 +1,9 @@ +a file with funny characters in the file name + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/testavatar.jpg b/tests/acceptance/filesForUpload/testavatar.jpg new file mode 100644 index 000000000..fb9e54042 Binary files /dev/null and b/tests/acceptance/filesForUpload/testavatar.jpg differ diff --git a/tests/acceptance/filesForUpload/testavatar.png b/tests/acceptance/filesForUpload/testavatar.png new file mode 100644 index 000000000..bd9c3cb1e Binary files /dev/null and b/tests/acceptance/filesForUpload/testavatar.png differ diff --git a/tests/acceptance/filesForUpload/textfile.txt b/tests/acceptance/filesForUpload/textfile.txt new file mode 100644 index 000000000..efffdeff1 --- /dev/null +++ b/tests/acceptance/filesForUpload/textfile.txt @@ -0,0 +1,3 @@ +This is a testfile. + +Cheers. \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/zerobyte.txt b/tests/acceptance/filesForUpload/zerobyte.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/acceptance/filesForUpload/zzzz-must-be-last-file-in-folder.txt b/tests/acceptance/filesForUpload/zzzz-must-be-last-file-in-folder.txt new file mode 100644 index 000000000..dcc19d17f --- /dev/null +++ b/tests/acceptance/filesForUpload/zzzz-must-be-last-file-in-folder.txt @@ -0,0 +1,11 @@ +This must be the last skeleton file in this folder, when sorted alphabetically. +Tests for performing actions on the last file in the folder try to find and use +this file name. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/filesForUpload/zzzz-zzzz-will-be-at-the-end-of-the-folder-when-uploaded.txt b/tests/acceptance/filesForUpload/zzzz-zzzz-will-be-at-the-end-of-the-folder-when-uploaded.txt new file mode 100644 index 000000000..1db21f1fb --- /dev/null +++ b/tests/acceptance/filesForUpload/zzzz-zzzz-will-be-at-the-end-of-the-folder-when-uploaded.txt @@ -0,0 +1,11 @@ +This file has a name so it would be the last after the upload, when sorted alphabetically. +Tests for performing actions on the last file in the folder try to find and use +this file name. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae +dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est +qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora +incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum +exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? \ No newline at end of file diff --git a/tests/acceptance/lint-expected-failures.sh b/tests/acceptance/lint-expected-failures.sh new file mode 100755 index 000000000..a1f31ce4d --- /dev/null +++ b/tests/acceptance/lint-expected-failures.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +log_error() { + echo -e "\e[31m$1\e[0m" +} + +log_info() { + echo -e "\e[34m$1\e[0m" +} + +log_success() { + echo -e "\e[32m$1\e[0m" +} + +declare -A scenarioLines + +if [ -n "${EXPECTED_FAILURES_FILE}" ] +then + if [ -f "${EXPECTED_FAILURES_FILE}" ] + then + log_info "Checking expected failures in ${EXPECTED_FAILURES_FILE}" + else + log_error "Expected failures file ${EXPECTED_FAILURES_FILE} not found" + log_error "Check the setting of EXPECTED_FAILURES_FILE environment variable" + exit 1 + fi + FINAL_EXIT_STATUS=0 + # If the last line of the expected-failures file ends without a newline character + # then that line may not get processed by some of the bash code in this script + # So check that the last character in the file is a newline + if [ "$(tail -c1 "${EXPECTED_FAILURES_FILE}" | wc -l)" -eq 0 ] + then + log_error "Expected failures file ${EXPECTED_FAILURES_FILE} must end with a newline" + log_error "Put a newline at the end of the last line and try again" + FINAL_EXIT_STATUS=1 + fi + # Check the expected-failures file to ensure that the lines are self-consistent + # In most cases the features that are being run are in owncloud/core, + # so assume that by default. + FEATURE_FILE_REPO="owncloud/core" + FEATURE_FILE_PATH="tests/acceptance/features" + LINE_NUMBER=0 + while read -r INPUT_LINE + do + LINE_NUMBER=$(("$LINE_NUMBER" + 1)) + + # Ignore comment lines (starting with hash) + if [[ "${INPUT_LINE}" =~ ^# ]] + then + continue + fi + # A line of text in the feature file can be used to indicate that the + # features being run are actually from some other repo. For example: + # "The expected failures in this file are from features in the owncloud/ocis repo." + # Write a line near the top of the expected-failures file to "declare" this, + # overriding the default "owncloud/core" + FEATURE_FILE_SPEC_LINE_FOUND="false" + if [[ "${INPUT_LINE}" =~ features[[:blank:]]in[[:blank:]]the[[:blank:]]([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)[[:blank:]]repo ]]; then + FEATURE_FILE_REPO="${BASH_REMATCH[1]}" + log_info "Features are expected to be in the ${FEATURE_FILE_REPO} repo\n" + FEATURE_FILE_SPEC_LINE_FOUND="true" + fi + if [[ "${INPUT_LINE}" =~ repo[[:blank:]]in[[:blank:]]the[[:blank:]]([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)[[:blank:]]folder[[:blank:]]tree ]]; then + FEATURE_FILE_PATH="${BASH_REMATCH[1]}" + log_info "Features are expected to be in the ${FEATURE_FILE_PATH} folder tree\n" + FEATURE_FILE_SPEC_LINE_FOUND="true" + fi + if [[ $FEATURE_FILE_SPEC_LINE_FOUND == "true" ]]; then + continue + fi + # Match lines that have "- [someSuite/someName.feature:n]" pattern on start + # the part inside the brackets is the suite, feature and line number of the expected failure. + if [[ "${INPUT_LINE}" =~ ^-[[:space:]]\[([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+\.feature:[0-9]+)] ]]; then + SUITE_SCENARIO_LINE="${BASH_REMATCH[1]}" + elif [[ + # report for lines like: " - someSuite/someName.feature:n" + "${INPUT_LINE}" =~ ^[[:space:]]*-[[:space:]][a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+\.feature:[0-9]+[[:space:]]*$ || + # report for lines starting with: "[someSuite/someName.feature:n]" + "${INPUT_LINE}" =~ ^[[:space:]]*\[([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+\.feature:[0-9]+)] + ]]; then + log_error "> Line ${LINE_NUMBER}: Not in the correct format." + log_error " + Actual Line : '${INPUT_LINE}'" + log_error " - Expected Format : '- [suite/scenario.feature:line_number](scenario_line_url)'" + FINAL_EXIT_STATUS=1 + continue + else + # otherwise, ignore the line + continue + fi + # Find the link in round-brackets that should be after the SUITE_SCENARIO_LINE + if [[ "${INPUT_LINE}" =~ \(([a-zA-Z0-9:/.#_-]+)\) ]]; then + ACTUAL_LINK="${BASH_REMATCH[1]}" + else + log_error "Line ${LINE_NUMBER}: ${INPUT_LINE} : Link is empty" + FINAL_EXIT_STATUS=1 + continue + fi + if [[ -n "${scenarioLines[${SUITE_SCENARIO_LINE}]:-}" ]]; + then + log_error "> Line ${LINE_NUMBER}: Scenario line ${SUITE_SCENARIO_LINE} is duplicated" + FINAL_EXIT_STATUS=1 + fi + scenarioLines[${SUITE_SCENARIO_LINE}]="exists" + OLD_IFS=${IFS} + IFS=':' + read -ra FEATURE_PARTS <<< "${SUITE_SCENARIO_LINE}" + IFS=${OLD_IFS} + SUITE_FEATURE="${FEATURE_PARTS[0]}" + FEATURE_LINE="${FEATURE_PARTS[1]}" + EXPECTED_LINK="https://github.com/${FEATURE_FILE_REPO}/blob/master/${FEATURE_FILE_PATH}/${SUITE_FEATURE}#L${FEATURE_LINE}" + if [[ "${ACTUAL_LINK}" != "${EXPECTED_LINK}" ]]; then + log_error "> Line ${LINE_NUMBER}: Link is not correct for ${SUITE_SCENARIO_LINE}" + log_error " + Actual link : ${ACTUAL_LINK}" + log_error " - Expected link : ${EXPECTED_LINK}" + FINAL_EXIT_STATUS=1 + fi + + done < "${EXPECTED_FAILURES_FILE}" +else + log_error "Environment variable EXPECTED_FAILURES_FILE must be defined to be the file to check" + exit 1 +fi + +if [ ${FINAL_EXIT_STATUS} == 1 ] +then + log_error "\nErrors were found in the expected failures file - see the messages above!" +else + log_success "\nNo problems were found in the expected failures file." +fi +exit ${FINAL_EXIT_STATUS} diff --git a/tests/acceptance/run.sh b/tests/acceptance/run.sh new file mode 100755 index 000000000..cd9180ef3 --- /dev/null +++ b/tests/acceptance/run.sh @@ -0,0 +1,1376 @@ +#!/usr/bin/env bash +[[ "${DEBUG}" == "true" ]] && set -x + +# from http://stackoverflow.com/a/630387 +SCRIPT_PATH="`dirname \"$0\"`" # relative +SCRIPT_PATH="`( cd \"${SCRIPT_PATH}\" && pwd )`" # absolutized and normalized + +echo 'Script path: '${SCRIPT_PATH} + +OC_PATH=${SCRIPT_PATH}/../../ +OCC=${OC_PATH}occ + +# Allow optionally passing in the path to the behat program. +# This gives flexibility for callers that have installed their own behat +if [ -z "${BEHAT_BIN}" ] +then + BEHAT=${OC_PATH}vendor-bin/behat/vendor/bin/behat +else + BEHAT=${BEHAT_BIN} +fi +BEHAT_TAGS_OPTION_FOUND=false + +if [ -n "${STEP_THROUGH}" ] +then + STEP_THROUGH_OPTION="--step-through" +fi + +# The following environment variables can be specified: +# +# ACCEPTANCE_TEST_TYPE - see "--type" description +# BEHAT_FEATURE - see "--feature" description +# BEHAT_FILTER_TAGS - see "--tags" description +# BEHAT_SUITE - see "--suite" description +# BEHAT_YML - see "--config" description +# BROWSER - see "--browser" description +# NORERUN - see "--norerun" description +# RERUN_FAILED_WEBUI_SCENARIOS - opposite of NORERUN +# RUN_PART and DIVIDE_INTO_NUM_PARTS - see "--part" description +# SHOW_OC_LOGS - see "--show-oc-logs" description +# TESTING_REMOTE_SYSTEM - see "--remote" description +# EXPECTED_FAILURES_FILE - a file that contains a list of the scenarios that are expected to fail + +if [ -n "${EXPECTED_FAILURES_FILE}" ] +then + # Check the expected-failures file + ${SCRIPT_PATH}/lint-expected-failures.sh + LINT_STATUS=$? + if [ ${LINT_STATUS} -ne 0 ] + then + echo "Error: expected failures file ${EXPECTED_FAILURES_FILE} is invalid" + exit ${LINT_STATUS} + fi +fi + +# Default to testing a local system +if [ -z "${TESTING_REMOTE_SYSTEM}" ] +then + TESTING_REMOTE_SYSTEM=false +fi + +# Default to not show ownCloud logs +if [ -z "${SHOW_OC_LOGS}" ] +then + SHOW_OC_LOGS=false +fi + +# Default to re-run failed webUI scenarios +if [ -z "${RERUN_FAILED_WEBUI_SCENARIOS}" ] +then + RERUN_FAILED_WEBUI_SCENARIOS=true +fi + +# Allow callers to specify NORERUN=true as an environment variable +if [ "${NORERUN}" = true ] +then + RERUN_FAILED_WEBUI_SCENARIOS=false +fi + +# Default to API tests +# Note: if a specific feature or suite is also specified, then the acceptance +# test type is deduced from the suite name, and this environment variable +# ACCEPTANCE_TEST_TYPE is overridden. +if [ -z "${ACCEPTANCE_TEST_TYPE}" ] +then + ACCEPTANCE_TEST_TYPE="api" +fi + +# Look for command line options for: +# -c or --config - specify a behat.yml to use +# --feature - specify a single feature to run +# --suite - specify a single suite to run +# --type - api, cli or webui - if no individual feature or suite is specified, then +# specify the type of acceptance tests to run. Default api. +# --tags - specify tags for scenarios to run (or not) +# --browser - for webUI tests, which browser to use. "chrome", "firefox", +# "internet explorer" and "MicrosoftEdge" are possible. +# --remote - the server under test is remote, so we cannot locally enable the +# testing app. We have to assume it is already enabled. +# --show-oc-logs - tail the ownCloud log after the test run +# --norerun - do not rerun failed webUI scenarios +# --loop - loop tests for given number of times. Only use it for debugging purposes +# --part - run a subset of scenarios, need two numbers. +# first number: which part to run +# second number: in how many parts to divide the set of scenarios +# --step-through - pause after each test step + +# Command line options processed here will override environment variables that +# might have been set by the caller, or in the code above. +while [[ $# -gt 0 ]] +do + key="$1" + case ${key} in + -c|--config) + BEHAT_YML="$2" + shift + ;; + --feature) + BEHAT_FEATURE="$2" + shift + ;; + --suite) + BEHAT_SUITE="$2" + shift + ;; + --loop) + BEHAT_RERUN_TIMES="$2" + shift + ;; + --type) + # Lowercase the parameter value, so the user can provide "API", "CLI", "webUI" etc + ACCEPTANCE_TEST_TYPE="${2,,}" + shift + ;; + --tags) + BEHAT_FILTER_TAGS="$2" + BEHAT_TAGS_OPTION_FOUND=true + shift + ;; + --browser) + BROWSER="$2" + shift + ;; + --part) + RUN_PART="$2" + DIVIDE_INTO_NUM_PARTS="$3" + if [ ${RUN_PART} -gt ${DIVIDE_INTO_NUM_PARTS} ] + then + echo "cannot run part ${RUN_PART} of ${DIVIDE_INTO_NUM_PARTS}" + exit 1 + fi + shift 2 + ;; + --remote) + TESTING_REMOTE_SYSTEM=true + ;; + --show-oc-logs) + SHOW_OC_LOGS=true + ;; + --norerun) + RERUN_FAILED_WEBUI_SCENARIOS=false + ;; + --step-through) + STEP_THROUGH_OPTION="--step-through" + ;; + *) + # A "random" parameter is presumed to be a feature file to run. + # Typically that will be specified at the end, or as the only + # parameter. + BEHAT_FEATURE="$1" + ;; + esac + shift +done + +# Save the current language and set the language to "C" +# We want to have it all in english to be able to parse outputs +OLD_LANG=${LANG} +export LANG=C + +# @param $1 admin authentication string username:password +# @param $2 occ url +# @param $3 command +# sets $REMOTE_OCC_STDOUT and $REMOTE_OCC_STDERR from returned xml data +# @return occ return code given in the xml data +function remote_occ() { + if [ "${TEST_OCIS}" == "true" ] || [ "${TEST_REVA}" == "true" ] + then + return 0 + fi + COMMAND=`echo $3 | xargs` + CURL_OCC_RESULT=`curl -k -s -u $1 $2 -d "command=${COMMAND}"` + # xargs is (miss)used to trim the output + RETURN=`echo ${CURL_OCC_RESULT} | xmllint --xpath "string(ocs/data/code)" - | xargs` + # We could not find a proper return of the testing app, so something went wrong + if [ -z "${RETURN}" ] + then + RETURN=1 + REMOTE_OCC_STDERR=${CURL_OCC_RESULT} + else + REMOTE_OCC_STDOUT=`echo ${CURL_OCC_RESULT} | xmllint --xpath "string(ocs/data/stdOut)" - | xargs` + REMOTE_OCC_STDERR=`echo ${CURL_OCC_RESULT} | xmllint --xpath "string(ocs/data/stdErr)" - | xargs` + fi + return ${RETURN} +} + +# @param $1 admin authentication string username:password +# @param $2 occ url (without /bulk appendix) +# @param $3 commands +# exists with 1 and sets $REMOTE_OCC_STDERR if any of the occ commands returned a non-zero code +function remote_bulk_occ() { + if [ "${TEST_OCIS}" == "true" ] || [ "${TEST_REVA}" == "true" ] + then + return 0 + fi + CURL_OCC_RESULT=`curl -k -s -u $1 $2/bulk -d "${3}"` + COUNT_RESULTS=`echo ${CURL_OCC_RESULT} | xmllint --xpath "ocs/data/element/code" - | wc -l` + + RETURN=0 + REMOTE_OCC_STDERR="" + for ((n=1;n<=${COUNT_RESULTS};n++)) + do + EXIT_CODE=`echo ${CURL_OCC_RESULT} | xmllint --xpath "string((ocs/data/element/code)[${n}])" -` + if [ ${EXIT_CODE} -ne 0 ] + then + REMOTE_OCC_STDERR+=`echo ${CURL_OCC_RESULT} | xmllint --xpath "string((ocs/data/element/stdErr)[${n}])" - | xargs` + REMOTE_OCC_STDERR+="\n" + RETURN=1 + fi + + done + return ${RETURN} +} + +# @param $1 admin authentication string username:password +# @param $2 occ url +# @param $3 directory to create, relative to the server's root +# sets $REMOTE_OCC_STDOUT and $REMOTE_OCC_STDERR from returned xml data +# @return return code given in the xml data +function remote_dir() { + COMMAND=`echo $3 | xargs` + CURL_OCC_RESULT=`curl -k -s -u $1 $2 -d "dir=${COMMAND}"` + # xargs is (miss)used to trim the output + HTTP_STATUS=`echo ${CURL_OCC_RESULT} | xmllint --xpath "string(ocs/meta/statuscode)" - | xargs` + # We could not find a proper return of the testing app, so something went wrong + if [ -z "${HTTP_STATUS}" ] + then + RETURN=1 + REMOTE_OCC_STDERR=${CURL_OCC_RESULT} + else + if [ "${HTTP_STATUS}" = 200 ] + then + RETURN=0 + else + RETURN=1 + REMOTE_OCC_STDERR=${CURL_OCC_RESULT} + fi + fi + return ${RETURN} +} + +# $1 admin auth as needed for curl +# $2 full URL of the occ command in the testing app +# $3 system|app +# $4 app-name if app used +# $5 setting name +# $6 type of the variable to set it back correctly (optional) +# adds a new element to the array PREVIOUS_SETTINGS +# PREVIOUS_SETTINGS will be an array of strings containing: +# URL;system|app;app-name;setting-name;value;variable-type +function save_config_setting() { + remote_occ $1 $2 "--no-warnings config:$3:get $4 $5" + PREVIOUS_SETTINGS+=("$2;$3;$4;$5;${REMOTE_OCC_STDOUT};$6") +} + +declare -a PREVIOUS_SETTINGS + +# get the sub path from an URL +# $1 the full URL including the protocol +# echos the path without trailing slash +function get_path_from_url() { + PROTOCOL="$(echo $1 | grep :// | sed -e's,^\(.*://\).*,\1,g')" + URL="$(echo ${1/$PROTOCOL/})" + PATH="$(echo ${URL} | grep / | cut -d/ -f2-)" + echo ${PATH%/} +} + +# check that server is up and working correctly +# $1 the full URL including the protocol +# exits if server is not working correctly (by pinging status.php), otherwise returns +function assert_server_up() { + curl -k -sSf -L $1/status.php -o /dev/null + if [[ $? -eq 0 ]] + then + return + else + echo >&2 "Server on $1 is down or not working correctly." + exit 98 + fi +} + +# check that testing app is installed +# $1 the full URL including the protocol +# if testing app is not installed, this returns 500 status code +# we are not sending request with auth, so if it is installed 401 Not Authorized is returned. +# anyway, if it's not 500, assume testing app is installed and move on. +# Otherwise, print to stderr and exit. +function assert_testing_app_enabled() { + CURL_RESULT=`curl -s $1/ocs/v2.php/apps/testing/api/v1/app/testing -o /dev/null -w "%{http_code}"` + if [[ ${CURL_RESULT} -eq 500 ]] + then + echo >&2 "Testing app is not enabled on the server on $1." + echo >&2 "Please install and enable it to run the tests." + exit 98 + else + return + fi +} + +# check if certain apache_module is enabled +# $1 admin authentication string username:password +# $2 the full url to the testing app on the server to check +# $3 Module to check for +# $4 text description of the server being checked, e.g. "local", "remote" +# return 0 if given module is enabled, else return with 1 +function check_apache_module_enabled() { + if [ "${TEST_OCIS}" == "true" ] || [ "${TEST_REVA}" == "true" ] + then + return 0 + fi + # test if mod_rewrite is enabled + CURL_RESULT=`curl -k -s -u $1 $2apache_modules/$3` + STATUS_CODE=`echo ${CURL_RESULT} | xmllint --xpath "string(ocs/meta/statuscode)" -` + if [[ ${STATUS_CODE} -ne 200 ]] + then + echo -n "Could not reliably determine if '$3' module is enabled on the $4 server, because " + echo ${CURL_RESULT} | xmllint --xpath "string(ocs/meta/message)" - + echo "" + return 1 + fi + return 0 +} + +# Provide a default admin username and password. +# But let the caller pass them if they wish +if [ -z "${ADMIN_USERNAME}" ] +then + ADMIN_USERNAME="admin" +fi + +if [ -z "${ADMIN_PASSWORD}" ] +then + ADMIN_PASSWORD="admin" +fi + +ADMIN_AUTH="${ADMIN_USERNAME}:${ADMIN_PASSWORD}" + +export ADMIN_USERNAME +export ADMIN_PASSWORD + +if [ -z "${BEHAT_RERUN_TIMES}" ] +then + BEHAT_RERUN_TIMES=1 +fi + +function env_alt_home_enable { + remote_occ ${ADMIN_AUTH} ${OCC_URL} "config:app:set testing enable_alt_user_backend --value yes" +} + +function env_alt_home_clear { + remote_occ ${ADMIN_AUTH} ${OCC_URL} "app:disable testing" || { echo "Unable to disable testing app" >&2; exit 1; } +} + +# expected variables +# -------------------- +# $SUITE_FEATURE_TEXT - human readable which test to run +# $BEHAT_SUITE_OPTION - suite setting with "--suite" or empty if all suites have to be run +# $BEHAT_FEATURE - feature file, or empty +# $BEHAT_FILTER_TAGS - list of tags +# $BEHAT_TAGS_OPTION_FOUND +# $BROWSER_TEXT +# $BROWSER_VERSION_TEXT +# $PLATFORM_TEXT +# $TEST_LOG_FILE +# $BEHAT - behat executable +# $BEHAT_YML +# $RUNNING_WEBUI_TESTS +# $RERUN_FAILED_WEBUI_SCENARIOS +# +# set arrays +# --------------- +# $UNEXPECTED_FAILED_SCENARIOS array of scenarios that failed unexpectedly +# $UNEXPECTED_PASSED_SCENARIOS array of scenarios that passed unexpectedly (while running with expected-failures.txt) + +declare -a UNEXPECTED_FAILED_SCENARIOS +declare -a UNEXPECTED_PASSED_SCENARIOS +declare -a UNEXPECTED_BEHAT_EXIT_STATUSES + +function run_behat_tests() { + echo "Running ${SUITE_FEATURE_TEXT} tests tagged ${BEHAT_FILTER_TAGS} ${BROWSER_TEXT}${BROWSER_VERSION_TEXT}${PLATFORM_TEXT}" | tee ${TEST_LOG_FILE} + + if [ "${REPLACE_USERNAMES}" == "true" ] + then + echo "Usernames and attributes in tests are being replaced:" + cat ${SCRIPT_PATH}/usernames.json + fi + + ${BEHAT} --colors --strict ${STEP_THROUGH_OPTION} -c ${BEHAT_YML} -f pretty ${BEHAT_SUITE_OPTION} --tags ${BEHAT_FILTER_TAGS} ${BEHAT_FEATURE} -v 2>&1 | tee -a ${TEST_LOG_FILE} + + BEHAT_EXIT_STATUS=${PIPESTATUS[0]} + + # remove nullbytes from the test log + TEMP_CONTENT=$(tr < ${TEST_LOG_FILE} -d '\000') + OLD_IFS="${IFS}" + IFS="" + echo ${TEMP_CONTENT} > ${TEST_LOG_FILE} + IFS="${OLD_IFS}" + + # Find the count of scenarios that passed + SCENARIO_RESULTS_COLORED=`grep -Ea '^[0-9]+[[:space:]]scenario(|s)[[:space:]]\(' ${TEST_LOG_FILE}` + SCENARIO_RESULTS=$(echo "${SCENARIO_RESULTS_COLORED}" | sed "s/\x1b[^m]*m//g") + if [ ${BEHAT_EXIT_STATUS} -eq 0 ] + then + # They (SCENARIO_RESULTS) all passed, so just get the first number. + # The text looks like "1 scenario (1 passed)" or "123 scenarios (123 passed)" + [[ ${SCENARIO_RESULTS} =~ ([0-9]+) ]] + SCENARIOS_THAT_PASSED=$((SCENARIOS_THAT_PASSED + BASH_REMATCH[1])) + else + # "Something went wrong" with the Behat run (non-zero exit status). + # If there were "ordinary" test fails, then we process that later. Maybe they are all "expected failures". + # But if there were steps in a feature file that are undefined, we want to fail immediately. + # So exit the tests and do not lint expected failures when undefined steps exist. + if [[ ${SCENARIO_RESULTS} == *"undefined"* ]] + then + echo -e "\033[31m Undefined steps: There were some undefined steps found." + exit 1 + fi + # If there were no scenarios in the requested suite or feature that match + # the requested combination of tags, then Behat exits with an error status + # and reports "No scenarios" in its output. + # This can happen, for example, when running core suites from an app and + # requesting some tag combination that does not happen frequently. Then + # sometimes there may not be any matching scenarios in one of the suites. + # In this case, consider the test has passed. + MATCHING_COUNT=`grep -ca '^No scenarios$' ${TEST_LOG_FILE}` + if [ ${MATCHING_COUNT} -eq 1 ] + then + echo "Information: no matching scenarios were found." + BEHAT_EXIT_STATUS=0 + else + # Find the count of scenarios that passed and failed + SCENARIO_RESULTS_COLORED=`grep -Ea '^[0-9]+[[:space:]]scenario(|s)[[:space:]]\(' ${TEST_LOG_FILE}` + SCENARIO_RESULTS=$(echo "${SCENARIO_RESULTS_COLORED}" | sed "s/\x1b[^m]*m//g") + if [[ ${SCENARIO_RESULTS} =~ [0-9]+[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+ ]] + then + # Some passed and some failed, we got the second and third numbers. + # The text looked like "15 scenarios (6 passed, 9 failed)" + SCENARIOS_THAT_PASSED=$((SCENARIOS_THAT_PASSED + BASH_REMATCH[1])) + SCENARIOS_THAT_FAILED=$((SCENARIOS_THAT_FAILED + BASH_REMATCH[2])) + elif [[ ${SCENARIO_RESULTS} =~ [0-9]+[^0-9]+([0-9]+)[^0-9]+ ]] + then + # All failed, we got the second number. + # The text looked like "4 scenarios (4 failed)" + SCENARIOS_THAT_FAILED=$((SCENARIOS_THAT_FAILED + BASH_REMATCH[1])) + fi + fi + fi + + FAILED_SCENARIO_PATHS_COLORED=`awk '/Failed scenarios:/',0 ${TEST_LOG_FILE} | grep -a feature` + # There will be some ANSI escape codes for color in the FEATURE_COLORED var. + # Strip them out so we can pass just the ordinary feature details to Behat. + # Thanks to https://en.wikipedia.org/wiki/Tee_(command) and + # https://stackoverflow.com/questions/23416278/how-to-strip-ansi-escape-sequences-from-a-variable + # for ideas. + FAILED_SCENARIO_PATHS=$(echo "${FAILED_SCENARIO_PATHS_COLORED}" | sed "s/\x1b[^m]*m//g") + + # If something else went wrong, and there were no failed scenarios, + # then the awk, grep, sed command sequence above ends up with an empty string. + # Unset FAILED_SCENARIO_PATHS to avoid later code thinking that there might be + # one failed scenario. + if [ -z "${FAILED_SCENARIO_PATHS}" ] + then + unset FAILED_SCENARIO_PATHS + fi + + if [ -n "${EXPECTED_FAILURES_FILE}" ] + then + if [ -n "${BEHAT_SUITE_TO_RUN}" ] + then + echo "Checking expected failures for suite ${BEHAT_SUITE_TO_RUN}" + else + echo "Checking expected failures" + fi + + # Check that every failed scenario is in the list of expected failures + for FAILED_SCENARIO_PATH in ${FAILED_SCENARIO_PATHS} + do + SUITE_PATH=`dirname ${FAILED_SCENARIO_PATH}` + SUITE=`basename ${SUITE_PATH}` + SCENARIO=`basename ${FAILED_SCENARIO_PATH}` + SUITE_SCENARIO="${SUITE}/${SCENARIO}" + grep "\[${SUITE_SCENARIO}\]" "${EXPECTED_FAILURES_FILE}" > /dev/null + if [ $? -ne 0 ] + then + echo "Error: Scenario ${SUITE_SCENARIO} failed but was not expected to fail." + UNEXPECTED_FAILED_SCENARIOS+=("${SUITE_SCENARIO}") + fi + done + + # Check that every scenario in the list of expected failures did fail + while read SUITE_SCENARIO + do + # Ignore comment lines (starting with hash) + if [[ "${SUITE_SCENARIO}" =~ ^# ]] + then + continue + fi + # Match lines that have [someSuite/someName.feature:n] - the part inside the + # brackets is the suite, feature and line number of the expected failure. + # Else ignore the line. + if [[ "${SUITE_SCENARIO}" =~ \[([a-zA-Z0-9-]+/[a-zA-Z0-9-]+\.feature:[0-9]+)] ]]; then + SUITE_SCENARIO="${BASH_REMATCH[1]}" + else + continue + fi + if [ -n "${BEHAT_SUITE_TO_RUN}" ] + then + # If the expected failure is not in the suite that is currently being run, + # then do not try and check that it failed. + REGEX_TO_MATCH="^${BEHAT_SUITE_TO_RUN}/" + if ! [[ "${SUITE_SCENARIO}" =~ ${REGEX_TO_MATCH} ]] + then + continue + fi + fi + + # look for the expected suite-scenario at the end of a line in the + # FAILED_SCENARIO_PATHS - for example looking for apiComments/comments.feature:9 + # we want to match lines like: + # tests/acceptance/features/apiComments/comments.feature:9 + # but not lines like:: + # tests/acceptance/features/apiComments/comments.feature:902 + echo "${FAILED_SCENARIO_PATHS}" | grep ${SUITE_SCENARIO}$ > /dev/null + if [ $? -ne 0 ] + then + echo "Info: Scenario ${SUITE_SCENARIO} was expected to fail but did not fail." + UNEXPECTED_PASSED_SCENARIOS+=("${SUITE_SCENARIO}") + fi + done < ${EXPECTED_FAILURES_FILE} + else + for FAILED_SCENARIO_PATH in ${FAILED_SCENARIO_PATHS} + do + SUITE_PATH=$(dirname "${FAILED_SCENARIO_PATH}") + SUITE=$(basename "${SUITE_PATH}") + SCENARIO=$(basename "${FAILED_SCENARIO_PATH}") + SUITE_SCENARIO="${SUITE}/${SCENARIO}" + UNEXPECTED_FAILED_SCENARIOS+=("${SUITE_SCENARIO}") + done + fi + + if [ ${BEHAT_EXIT_STATUS} -ne 0 ] && [ ${#FAILED_SCENARIO_PATHS[@]} -eq 0 ] + then + # Behat had some problem and there were no failed scenarios reported + # So the problem is something else. + # Possibly there were missing step definitions. Or Behat crashed badly, or... + UNEXPECTED_BEHAT_EXIT_STATUSES+=("${SUITE_FEATURE_TEXT} had behat exit status ${BEHAT_EXIT_STATUS}") + fi + + # With webUI tests, we try running failed tests again. + if [ ${#UNEXPECTED_FAILED_SCENARIOS[@]} -gt 0 ] && [ "${RUNNING_WEBUI_TESTS}" = true ] && [ "${RERUN_FAILED_WEBUI_SCENARIOS}" = true ] + then + echo "webUI test run failed with exit status: ${BEHAT_EXIT_STATUS}" + FAILED_SCENARIO_PATHS_COLORED=`awk '/Failed scenarios:/',0 ${TEST_LOG_FILE} | grep feature` + # There will be some ANSI escape codes for color in the FEATURE_COLORED var. + # Strip them out so we can pass just the ordinary feature details to Behat. + # Thanks to https://en.wikipedia.org/wiki/Tee_(command) and + # https://stackoverflow.com/questions/23416278/how-to-strip-ansi-escape-sequences-from-a-variable + # for ideas. + FAILED_SCENARIO_PATHS=$(echo "${FAILED_SCENARIO_PATHS_COLORED}" | sed "s/\x1b[^m]*m//g") + + # If something else went wrong, and there were no failed scenarios, + # then the awk, grep, sed command sequence above ends up with an empty string. + # Unset FAILED_SCENARIO_PATHS to avoid later code thinking that there might be + # one failed scenario. + if [ -z "${FAILED_SCENARIO_PATHS}" ] + then + unset FAILED_SCENARIO_PATHS + FAILED_SCENARIO_PATHS=() + fi + + for FAILED_SCENARIO_PATH in ${FAILED_SCENARIO_PATHS} + do + SUITE_PATH=`dirname ${FAILED_SCENARIO_PATH}` + SUITE=`basename ${SUITE_PATH}` + SCENARIO=`basename ${FAILED_SCENARIO_PATH}` + SUITE_SCENARIO="${SUITE}/${SCENARIO}" + + if [ -n "${EXPECTED_FAILURES_FILE}" ] + then + grep -x ${SUITE_SCENARIO} ${EXPECTED_FAILURES_FILE} > /dev/null + if [ $? -eq 0 ] + then + echo "Notice: Scenario ${SUITE_SCENARIO} is expected to fail so do not rerun it." + continue + fi + fi + + echo "Rerun failed scenario: ${FAILED_SCENARIO_PATH}" + ${BEHAT} --colors --strict -c ${BEHAT_YML} -f pretty ${BEHAT_SUITE_OPTION} --tags ${BEHAT_FILTER_TAGS} ${FAILED_SCENARIO_PATH} -v 2>&1 | tee -a ${TEST_LOG_FILE} + BEHAT_EXIT_STATUS=${PIPESTATUS[0]} + if [ ${BEHAT_EXIT_STATUS} -eq 0 ] + then + # The scenario was not expected to fail but had failed and is present in the + # unexpected_failures list. We've checked the scenario with a re-run and + # it passed. So remove it from the unexpected_failures list. + for i in "${!UNEXPECTED_FAILED_SCENARIOS[@]}" + do + if [ "${UNEXPECTED_FAILED_SCENARIOS[i]}" == "$SUITE_SCENARIO" ] + then + unset "UNEXPECTED_FAILED_SCENARIOS[i]" + fi + done + else + echo "webUI test rerun failed with exit status: ${BEHAT_EXIT_STATUS}" + # The scenario is not expected to fail but is failing also after the rerun. + # Since it is already reported in the unexpected_failures list, there is no + # need to touch that again. Continue processing the next scenario to rerun. + fi + done + fi + + if [ "${BEHAT_TAGS_OPTION_FOUND}" != true ] + then + # The behat run specified to skip scenarios tagged @skip + # Report them in a dry-run so they can be seen + # Big red error output is displayed if there are no matching scenarios - send it to null + DRY_RUN_FILE=$(mktemp) + SKIP_TAGS="${TEST_TYPE_TAG}&&@skip" + ${BEHAT} --dry-run --colors -c ${BEHAT_YML} -f pretty ${BEHAT_SUITE_OPTION} --tags "${SKIP_TAGS}" ${BEHAT_FEATURE} 1>${DRY_RUN_FILE} 2>/dev/null + if grep -q -m 1 'No scenarios' "${DRY_RUN_FILE}" + then + # If there are no skip scenarios, then no need to report that + : + else + echo "" + echo "The following tests were skipped because they are tagged @skip:" + cat "${DRY_RUN_FILE}" | tee -a ${TEST_LOG_FILE} + fi + rm -f "${DRY_RUN_FILE}" + fi +} + +function teardown() { + # Enable any apps that were disabled for the test run + for i in "${!APPS_TO_REENABLE[@]}" + do + read -r -a APP <<< "${APPS_TO_REENABLE[$i]}" + remote_occ ${ADMIN_AUTH} ${APP[0]} "--no-warnings app:enable ${APP[1]}" + done + + # Disable any apps that were enabled for the test run + for i in "${!APPS_TO_REDISABLE[@]}" + do + read -r -a APP <<< "${APPS_TO_REDISABLE[$i]}" + remote_occ ${ADMIN_AUTH} ${APP[0]} "--no-warnings app:disable ${APP[1]}" + done + + # Put back settings that were set for the test-run + for i in "${!PREVIOUS_SETTINGS[@]}" + do + PREVIOUS_IFS=${IFS} + IFS=';' + read -r -a SETTING <<< "${PREVIOUS_SETTINGS[$i]}" + IFS=${PREVIOUS_IFS} + if [ -z "${SETTING[4]}" ] + then + remote_occ ${ADMIN_AUTH} ${SETTING[0]} "config:${SETTING[1]}:delete ${SETTING[2]} ${SETTING[3]}" + else + TYPE_STRING="" + if [ -n "${SETTING[5]}" ] + then + #place the space here not in the command line, so that there is no space if the string is empty + TYPE_STRING=" --type ${SETTING[5]}" + fi + remote_occ ${ADMIN_AUTH} ${SETTING[0]} "config:${SETTING[1]}:set ${SETTING[2]} ${SETTING[3]} --value=${SETTING[4]}${TYPE_STRING}" + fi + done + + # Put back state of the testing app + if [ "${TESTING_ENABLED_BY_SCRIPT}" = true ] + then + ${OCC} app:disable testing + fi + + if [ "${OC_TEST_ALT_HOME}" = "1" ] + then + env_alt_home_clear + fi + + if [ "${TEST_WITH_PHPDEVSERVER}" == "true" ] + then + kill ${PHPPID} + kill ${PHPPID_FED} + fi + + if [ "${SHOW_OC_LOGS}" = true ] + then + tail "${OC_PATH}/data/owncloud.log" + fi + + # Reset the original language + export LANG=${OLD_LANG} + + rm -f "${TEST_LOG_FILE}" + + echo "runsh: Exit code of main run: ${BEHAT_EXIT_STATUS}" +} + +declare -x TEST_SERVER_URL +declare -x TEST_SERVER_FED_URL +declare -x TEST_WITH_PHPDEVSERVER +[[ -z "${TEST_SERVER_URL}" && -z "${TEST_SERVER_FED_URL}" ]] && TEST_WITH_PHPDEVSERVER="true" + +if [ -z "${IPV4_URL}" ] +then + IPV4_URL="${TEST_SERVER_URL}" +fi + +if [ -z "${IPV6_URL}" ] +then + IPV6_URL="${TEST_SERVER_URL}" +fi + +if [ "${TEST_WITH_PHPDEVSERVER}" != "true" ] +then + # The endpoint to use to do occ commands via the testing app + # set it already here, so it can be used for remote_occ + # we know the TEST_SERVER_URL already + TESTING_APP_URL="${TEST_SERVER_URL}/ocs/v2.php/apps/testing/api/v1/" + OCC_URL="${TESTING_APP_URL}occ" + # test that server is up and running, and testing app is enabled. + assert_server_up ${TEST_SERVER_URL} + if [ "${TEST_OCIS}" != "true" ] && [ "${TEST_REVA}" != "true" ] + then + assert_testing_app_enabled ${TEST_SERVER_URL} + fi + + if [ -n "${TEST_SERVER_FED_URL}" ] + then + TESTING_APP_FED_URL="${TEST_SERVER_FED_URL}/ocs/v2.php/apps/testing/api/v1/" + OCC_FED_URL="${TESTING_APP_FED_URL}occ" + # test that fed server is up and running, and testing app is enabled. + assert_server_up ${TEST_SERVER_FED_URL} + if [ "${TEST_OCIS}" != "true" ] && [ "${TEST_REVA}" != "true" ] + then + assert_testing_app_enabled ${TEST_SERVER_URL} + fi + fi + + echo "Not using php inbuilt server for running scenario ..." + echo "Updating .htaccess for proper rewrites" + #get the sub path of the webserver and set the correct RewriteBase + WEBSERVER_PATH=$(get_path_from_url ${TEST_SERVER_URL}) + HTACCESS_UPDATE_FAILURE_MSG="Could not update .htaccess in local server. Some tests might fail as a result." + remote_occ ${ADMIN_AUTH} ${OCC_URL} "config:system:set htaccess.RewriteBase --value /${WEBSERVER_PATH}/" + remote_occ ${ADMIN_AUTH} ${OCC_URL} "maintenance:update:htaccess" + [[ $? -eq 0 ]] || { echo "${HTACCESS_UPDATE_FAILURE_MSG}"; } + # check if mod_rewrite module is enabled + check_apache_module_enabled ${ADMIN_AUTH} ${TESTING_APP_URL} "mod_rewrite" "local" + + if [ -n "${TEST_SERVER_FED_URL}" ] + then + WEBSERVER_PATH=$(get_path_from_url ${TEST_SERVER_FED_URL}) + remote_occ ${ADMIN_AUTH} ${OCC_FED_URL} "config:system:set htaccess.RewriteBase --value /${WEBSERVER_PATH}/" + remote_occ ${ADMIN_AUTH} ${OCC_FED_URL} "maintenance:update:htaccess" + [[ $? -eq 0 ]] || { echo "${HTACCESS_UPDATE_FAILURE_MSG/local/federated}"; } + # check if mod_rewrite module is enabled + check_apache_module_enabled ${ADMIN_AUTH} ${TESTING_APP_FED_URL} "mod_rewrite" "remote" + fi +else + echo "Using php inbuilt server for running scenario ..." + + # Avoid port collision on jenkins - use $EXECUTOR_NUMBER + declare -x EXECUTOR_NUMBER + [[ -z "${EXECUTOR_NUMBER}" ]] && EXECUTOR_NUMBER=0 + + PORT=$((8080 + ${EXECUTOR_NUMBER})) + echo ${PORT} + php -S localhost:${PORT} -t "${OC_PATH}" & + PHPPID=$! + echo ${PHPPID} + + PORT_FED=$((8180 + ${EXECUTOR_NUMBER})) + echo ${PORT_FED} + php -S localhost:${PORT_FED} -t ../.. & + PHPPID_FED=$! + echo ${PHPPID_FED} + + export TEST_SERVER_URL="http://localhost:${PORT}" + export TEST_SERVER_FED_URL="http://localhost:${PORT_FED}" + + # The endpoint to use to do occ commands via the testing app + TESTING_APP_URL="${TEST_SERVER_URL}/ocs/v2.php/apps/testing/api/v1/" + OCC_URL="${TESTING_APP_URL}occ" + + # Give time for the PHP dev server to become available + # because we want to use it to get and change settings with the testing app + sleep 5 +fi + +# If a feature file has been specified but no suite, then deduce the suite +if [ -n "${BEHAT_FEATURE}" ] && [ -z "${BEHAT_SUITE}" ] +then + SUITE_PATH=`dirname ${BEHAT_FEATURE}` + BEHAT_SUITE=`basename ${SUITE_PATH}` +fi + +if [ -z "${BEHAT_YML}" ] +then + # Look for a behat.yml somewhere below the current working directory + # This saves app acceptance tests being forced to specify BEHAT_YML + BEHAT_YML="config/behat.yml" + if [ ! -f "${BEHAT_YML}" ] + then + BEHAT_YML="acceptance/config/behat.yml" + fi + if [ ! -f "${BEHAT_YML}" ] + then + BEHAT_YML="tests/acceptance/config/behat.yml" + fi + # If no luck above, then use the core behat.yml that should live below this script + if [ ! -f "${BEHAT_YML}" ] + then + BEHAT_YML="${SCRIPT_PATH}/config/behat.yml" + fi +fi + +BEHAT_CONFIG_DIR=$(dirname "${BEHAT_YML}") +ACCEPTANCE_DIR=$(dirname "${BEHAT_CONFIG_DIR}") +BEHAT_FEATURES_DIR="${ACCEPTANCE_DIR}/features" + +declare -a BEHAT_SUITES +if [[ -n "${BEHAT_SUITE}" ]] +then + BEHAT_SUITES+=(${BEHAT_SUITE}) +else + if [[ -n "${RUN_PART}" ]] + then + ALL_SUITES=`find ${BEHAT_FEATURES_DIR}/ -type d -iname ${ACCEPTANCE_TEST_TYPE}* | sort | rev | cut -d"/" -f1 | rev` + COUNT_ALL_SUITES=`echo "${ALL_SUITES}" | wc -l` + #divide the suites letting it round down (could be zero) + MIN_SUITES_PER_RUN=$((${COUNT_ALL_SUITES} / ${DIVIDE_INTO_NUM_PARTS})) + #some jobs might need an extra suite + MAX_SUITES_PER_RUN=$((${MIN_SUITES_PER_RUN} + 1)) + # the remaining number of suites that need to be distributed (could be zero) + REMAINING_SUITES=$((${COUNT_ALL_SUITES} - (${DIVIDE_INTO_NUM_PARTS} * ${MIN_SUITES_PER_RUN}))) + + if [[ ${RUN_PART} -le ${REMAINING_SUITES} ]] + then + SUITES_THIS_RUN=${MAX_SUITES_PER_RUN} + SUITES_IN_PREVIOUS_RUNS=$((${MAX_SUITES_PER_RUN} * (${RUN_PART} - 1))) + else + SUITES_THIS_RUN=${MIN_SUITES_PER_RUN} + SUITES_IN_PREVIOUS_RUNS=$((((${MAX_SUITES_PER_RUN} * ${REMAINING_SUITES}) + (${MIN_SUITES_PER_RUN} * (${RUN_PART} - ${REMAINING_SUITES} - 1))))) + fi + + if [ ${SUITES_THIS_RUN} -eq 0 ] + then + echo "there are only ${COUNT_ALL_SUITES} suites, nothing to do in part ${RUN_PART}" + teardown + exit 0 + fi + + COUNT_FINISH_AND_TODO_SUITES=$((${SUITES_IN_PREVIOUS_RUNS} + ${SUITES_THIS_RUN})) + BEHAT_SUITES+=(`echo "${ALL_SUITES}" | head -n ${COUNT_FINISH_AND_TODO_SUITES} | tail -n ${SUITES_THIS_RUN}`) + fi +fi + +# If the suite name begins with "webUI" or the user explicitly specified type "webui" +# then we will setup for webUI testing and run tests tagged as @webUI, +# otherwise it is API tests. +# Currently, running both API and webUI tests in a single run is not supported. +if [[ "${BEHAT_SUITE}" == webUI* ]] || [ "${ACCEPTANCE_TEST_TYPE}" = "webui" ] +then + TEST_TYPE_TAG="@webUI" + TEST_TYPE_TEXT="webUI" + RUNNING_API_TESTS=false + RUNNING_CLI_TESTS=false + RUNNING_WEBUI_TESTS=true +elif [[ "${BEHAT_SUITE}" == cli* ]] || [ "${ACCEPTANCE_TEST_TYPE}" = "cli" ] +then + TEST_TYPE_TAG="@cli" + TEST_TYPE_TEXT="cli" + RUNNING_API_TESTS=false + RUNNING_CLI_TESTS=true + RUNNING_WEBUI_TESTS=false +else + TEST_TYPE_TAG="@api" + TEST_TYPE_TEXT="API" + RUNNING_API_TESTS=true + RUNNING_CLI_TESTS=false + RUNNING_WEBUI_TESTS=false +fi + +# Always have one of "@api", "@cli" or "@webUI" filter tags +if [ -z "${BEHAT_FILTER_TAGS}" ] +then + BEHAT_FILTER_TAGS="${TEST_TYPE_TAG}" +else + # Be nice to the caller + # Remove any extra "&&" at the end of their tags list + BEHAT_FILTER_TAGS="${BEHAT_FILTER_TAGS%&&}" + # Remove any extra "&&" at the beginning of their tags list + BEHAT_FILTER_TAGS="${BEHAT_FILTER_TAGS#&&}" + BEHAT_FILTER_TAGS="${BEHAT_FILTER_TAGS}&&${TEST_TYPE_TAG}" +fi + +# EMAIL_HOST defines where the system-under-test can find the email server (inbucket) +# for sending email. +if [ -z "${EMAIL_HOST}" ] +then + EMAIL_HOST="127.0.0.1" +fi + +# LOCAL_INBUCKET_HOST defines where this test script can find the Inbucket server +# for sending email. When testing a remote system, the Inbucket server somewhere +# "in the middle" might have a different host name from the point of view of +# the test script. +if [ -z "${LOCAL_EMAIL_HOST}" ] +then + LOCAL_EMAIL_HOST="${EMAIL_HOST}" +fi + +if [ -z "${EMAIL_SMTP_PORT}" ] +then + EMAIL_SMTP_PORT="2500" +fi + +if [ -z "${SELENIUM_HOST}" ] +then + SELENIUM_HOST=localhost +fi + +if [ -z "${SELENIUM_PORT}" ] +then + SELENIUM_PORT=4445 +fi + +if [ -z "${BROWSER}" ] +then + BROWSER="chrome" +fi + +# Check if we can rely on a local ./occ command or if we are testing +# a remote instance (e.g. inside docker). +# If we have a remote instance we cannot enable the testing app and +# we have to hope it is enabled already, by other ways +if [ "${TESTING_REMOTE_SYSTEM}" = false ] +then + # Enable testing app + PREVIOUS_TESTING_APP_STATUS=$(${OCC} --no-warnings app:list "^testing$") + if [[ "${PREVIOUS_TESTING_APP_STATUS}" =~ ^Disabled: ]] + then + ${OCC} app:enable testing || { echo "Unable to enable testing app" >&2; exit 1; } + TESTING_ENABLED_BY_SCRIPT=true; + else + TESTING_ENABLED_BY_SCRIPT=false; + fi +else + TESTING_ENABLED_BY_SCRIPT=false; +fi + +# table of settings to be remembered and set +#system|app;app-name;setting-name;value;variable-type +declare -a SETTINGS +SETTINGS+=("system;;mail_domain;foobar.com") +SETTINGS+=("system;;mail_from_address;owncloud") +SETTINGS+=("system;;mail_smtpmode;smtp") +SETTINGS+=("system;;mail_smtphost;${EMAIL_HOST}") +SETTINGS+=("system;;mail_smtpport;${EMAIL_SMTP_PORT}") +SETTINGS+=("app;core;backgroundjobs_mode;webcron") +SETTINGS+=("system;;sharing.federation.allowHttpFallback;true;boolean") +SETTINGS+=("system;;grace_period.demo_key.show_popup;false;boolean") +SETTINGS+=("app;core;enable_external_storage;yes") +SETTINGS+=("system;;files_external_allow_create_new_local;true") +SETTINGS+=("system;;skeletondirectory;;") + +# Set various settings +for URL in ${OCC_URL} ${OCC_FED_URL} +do + declare SETTINGS_CMDS='[' + for i in "${!SETTINGS[@]}" + do + PREVIOUS_IFS=${IFS} + IFS=';' + read -r -a SETTING <<< "${SETTINGS[$i]}" + IFS=${PREVIOUS_IFS} + + save_config_setting "${ADMIN_AUTH}" "${URL}" "${SETTING[0]}" "${SETTING[1]}" "${SETTING[2]}" "${SETTING[4]}" + + TYPE_STRING="" + if [ -n "${SETTING[4]}" ] + then + #place the space here not in the command line, so that there is no space if the string is empty + TYPE_STRING=" --type ${SETTING[4]}" + fi + + SETTINGS_CMDS+="{\"command\": \"config:${SETTING[0]}:set ${SETTING[1]} ${SETTING[2]} --value=${SETTING[3]}${TYPE_STRING}\"}," + done + SETTINGS_CMDS=${SETTINGS_CMDS%?} # removes the last comma + SETTINGS_CMDS+="]" + remote_bulk_occ ${ADMIN_AUTH} ${URL} "${SETTINGS_CMDS}" + if [ $? -ne 0 ] + then + echo -e "Could not set some settings on ${URL}. Result:\n${REMOTE_OCC_STDERR}" + teardown + exit 1 + fi +done + +#set the skeleton folder +if [ -n "${SKELETON_DIR}" ] +then + for URL in ${OCC_URL} ${OCC_FED_URL} + do + remote_occ ${ADMIN_AUTH} ${URL} "config:system:set skeletondirectory --value=${SKELETON_DIR}" + done +fi + +#Enable and disable apps as required for default +if [ -z "${APPS_TO_DISABLE}" ] +then + APPS_TO_DISABLE="firstrunwizard notifications" +fi + +if [ -z "${APPS_TO_ENABLE}" ] +then + APPS_TO_ENABLE="" +fi + +declare -a APPS_TO_REENABLE; + +for URL in ${OCC_URL} ${OCC_FED_URL} + do + for APP_TO_DISABLE in ${APPS_TO_DISABLE}; do + remote_occ ${ADMIN_AUTH} ${URL} "--no-warnings app:list ^${APP_TO_DISABLE}$" + PREVIOUS_APP_STATUS=${REMOTE_OCC_STDOUT} + if [[ "${PREVIOUS_APP_STATUS}" =~ ^Enabled: ]] + then + APPS_TO_REENABLE+=("${URL} ${APP_TO_DISABLE}"); + remote_occ ${ADMIN_AUTH} ${URL} "--no-warnings app:disable ${APP_TO_DISABLE}" + fi + done +done + +declare -a APPS_TO_REDISABLE; + +for URL in ${OCC_URL} ${OCC_FED_URL} + do + for APP_TO_ENABLE in ${APPS_TO_ENABLE}; do + remote_occ ${ADMIN_AUTH} ${URL} "--no-warnings app:list ^${APP_TO_ENABLE}$" + PREVIOUS_APP_STATUS=${REMOTE_OCC_STDOUT} + if [[ "${PREVIOUS_APP_STATUS}" =~ ^Disabled: ]] + then + APPS_TO_REDISABLE+=("${URL} ${APP_TO_ENABLE}"); + remote_occ ${ADMIN_AUTH} ${URL} "--no-warnings app:enable ${APP_TO_ENABLE}" + fi + done +done + +if [ "${OC_TEST_ALT_HOME}" = "1" ] +then + env_alt_home_enable +fi + +# We need to skip some tests in certain browsers. +if [ "${BROWSER}" == "internet explorer" ] || [ "${BROWSER}" == "MicrosoftEdge" ] || [ "${BROWSER}" == "firefox" ] +then + BROWSER_IN_CAPITALS=${BROWSER//[[:blank:]]/} + BROWSER_IN_CAPITALS=${BROWSER_IN_CAPITALS^^} + BEHAT_FILTER_TAGS="${BEHAT_FILTER_TAGS}&&~@skipOn${BROWSER_IN_CAPITALS}" +fi + +# Skip tests tagged with the current oC version +# One, two or three parts of the version can be used +# e.g. +# @skipOnOcV10.0.4 +# @skipOnOcV10.0 +# @skipOnOcV10 + +remote_occ ${ADMIN_AUTH} ${OCC_URL} "config:system:get version" +OWNCLOUD_VERSION_THREE_DIGIT=`echo ${REMOTE_OCC_STDOUT} | cut -d"." -f1-3` +BEHAT_FILTER_TAGS='~@skipOnOcV'${OWNCLOUD_VERSION_THREE_DIGIT}'&&'${BEHAT_FILTER_TAGS} +OWNCLOUD_VERSION_TWO_DIGIT=`echo ${OWNCLOUD_VERSION_THREE_DIGIT} | cut -d"." -f1-2` +BEHAT_FILTER_TAGS='~@skipOnOcV'${OWNCLOUD_VERSION_TWO_DIGIT}'&&'${BEHAT_FILTER_TAGS} +OWNCLOUD_VERSION_ONE_DIGIT=`echo ${OWNCLOUD_VERSION_TWO_DIGIT} | cut -d"." -f1` +BEHAT_FILTER_TAGS='~@skipOnOcV'${OWNCLOUD_VERSION_ONE_DIGIT}'&&'${BEHAT_FILTER_TAGS} + +function version { echo "$@" | awk -F. '{ printf("%d%03d%03d\n", $1,$2,$3); }'; } + +# Skip tests for OC versions greater than 10.8.0 +if [ $(version $OWNCLOUD_VERSION_THREE_DIGIT) -gt $(version "10.8.0") ]; then + BEHAT_FILTER_TAGS='~@skipOnAllVersionsGreaterThanOcV10.8.0&&'${BEHAT_FILTER_TAGS} +fi + +if [ -n "${TEST_SERVER_FED_URL}" ] +then + remote_occ ${ADMIN_AUTH} ${OCC_FED_URL} "config:system:get version" + OWNCLOUD_FED_VERSION=`echo ${REMOTE_OCC_STDOUT} | cut -d"." -f1-3` + BEHAT_FILTER_TAGS='~@skipOnFedOcV'${OWNCLOUD_FED_VERSION}'&&'${BEHAT_FILTER_TAGS} + OWNCLOUD_FED_VERSION=`echo ${OWNCLOUD_FED_VERSION} | cut -d"." -f1-2` + BEHAT_FILTER_TAGS='~@skipOnFedOcV'${OWNCLOUD_FED_VERSION}'&&'${BEHAT_FILTER_TAGS} + OWNCLOUD_FED_VERSION=`echo ${OWNCLOUD_FED_VERSION} | cut -d"." -f1` + BEHAT_FILTER_TAGS='~@skipOnFedOcV'${OWNCLOUD_FED_VERSION}'&&'${BEHAT_FILTER_TAGS} +fi + +# If we are running remote only tests add another skip '@skipWhenTestingRemoteSystems' +if [ "${TESTING_REMOTE_SYSTEM}" = true ] +then + BEHAT_FILTER_TAGS='~@skipWhenTestingRemoteSystems&&'${BEHAT_FILTER_TAGS} +fi + +if [ "${OC_TEST_ON_OBJECTSTORE}" = "1" ] +then + BEHAT_FILTER_TAGS="${BEHAT_FILTER_TAGS}&&~@skip_on_objectstore" +fi + +# If the caller did not mention specific tags, skip the skipped tests by default +if [ "${BEHAT_TAGS_OPTION_FOUND}" = false ] +then + # If the caller has already specified specifically to run "@skip" scenarios + # then do not append "not @skip" + if [[ ! ${BEHAT_FILTER_TAGS} =~ "&&@skip&&" ]] + then + BEHAT_FILTER_TAGS="${BEHAT_FILTER_TAGS}&&~@skip" + fi +fi + +if [ -n "${BROWSER_VERSION}" ] +then + BROWSER_VERSION_EXTRA_CAPABILITIES_STRING='"browserVersion":"'${BROWSER_VERSION}'",' + BROWSER_VERSION_SELENIUM_STRING='"version": "'${BROWSER_VERSION}'", ' + BROWSER_VERSION_TEXT='('${BROWSER_VERSION}') ' +else + BROWSER_VERSION_EXTRA_CAPABILITIES_STRING="" + BROWSER_VERSION_SELENIUM_STRING="" + BROWSER_VERSION_TEXT="" +fi + +if [ -n "${PLATFORM}" ] +then + PLATFORM_SELENIUM_STRING='"platform": "'${PLATFORM}'", ' + PLATFORM_TEXT="on platform '${PLATFORM}' " +else + PLATFORM_SELENIUM_STRING="" + PLATFORM_TEXT="" +fi + +if [ "${RUNNING_API_TESTS}" = true ] +then + EXTRA_CAPABILITIES="" + BROWSER_TEXT="" +elif [ "${RUNNING_CLI_TESTS}" = true ] +then + EXTRA_CAPABILITIES="" + BROWSER_TEXT="" +else + BROWSER_TEXT="on browser '${BROWSER}' " + + # If we are running in some automated CI, use the provided details + if [ -n "${CI_REPO}" ] + then + CAPABILITIES_NAME_TEXT="${CI_REPO} - ${CI_BRANCH}" + else + # Otherwise this is a non-CI run, probably a local developer run + CAPABILITIES_NAME_TEXT="ownCloud non-CI" + fi + + # If SauceLabs credentials have been passed, then use them + if [ -n "${SAUCE_USERNAME}" ] && [ -n "${SAUCE_ACCESS_KEY}" ] + then + SAUCE_CREDENTIALS="${SAUCE_USERNAME}:${SAUCE_ACCESS_KEY}@" + else + SAUCE_CREDENTIALS="" + fi + + if [ "${BROWSER}" == "firefox" ] + then + # Set the screen resolution so that hopefully draggable elements will be visible + # FF gives problems if the destination element is not visible + EXTRA_CAPABILITIES='"screenResolution":"1920x1080",' + + # This selenium version works for Firefox after V47 + # We no longer need to support testing of Firefox V47 or earlier + EXTRA_CAPABILITIES='"seleniumVersion":"3.4.0",'${EXTRA_CAPABILITIES} + fi + + if [ "${BROWSER}" == "internet explorer" ] + then + EXTRA_CAPABILITIES='"iedriverVersion": "3.4.0","requiresWindowFocus":true,"screenResolution":"1920x1080",' + fi + + EXTRA_CAPABILITIES=${EXTRA_CAPABILITIES}${BROWSER_VERSION_EXTRA_CAPABILITIES_STRING}'"maxDuration":"3600"' + export BEHAT_PARAMS='{"extensions" : {"Behat\\MinkExtension" : {"browser_name": "'${BROWSER}'", "base_url" : "'${TEST_SERVER_URL}'", "selenium2":{"capabilities": {"marionette":null, "browser": "'${BROWSER}'", '${BROWSER_VERSION_SELENIUM_STRING}${PLATFORM_SELENIUM_STRING}'"name": "'${CAPABILITIES_NAME_TEXT}'", "extra_capabilities": {'${EXTRA_CAPABILITIES}'}}, "wd_host":"http://'${SAUCE_CREDENTIALS}${SELENIUM_HOST}':'${SELENIUM_PORT}'/wd/hub"}}, "SensioLabs\\Behat\\PageObjectExtension" : {}}}' +fi + +echo ${EXTRA_CAPABILITIES} +echo ${BEHAT_PARAMS} + +export IPV4_URL +export IPV6_URL +export FILES_FOR_UPLOAD="${SCRIPT_PATH}/filesForUpload/" + +if [ "${TEST_OCIS}" != "true" ] && [ "${TEST_REVA}" != "true" ] +then + # We are testing on an ownCloud core server. + # Tell the tests to wait 1 second between each upload/delete action + # to avoid problems with actions that depend on timestamps in seconds. + export UPLOAD_DELETE_WAIT_TIME=1 +fi + +TEST_LOG_FILE=$(mktemp) +SCENARIOS_THAT_PASSED=0 +SCENARIOS_THAT_FAILED=0 + +if [ ${#BEHAT_SUITES[@]} -eq 0 ] && [ -z "${BEHAT_FEATURE}" ] +then + SUITE_FEATURE_TEXT="all ${TEST_TYPE_TEXT}" + run_behat_tests +else + if [ -n "${BEHAT_SUITE}" ] + then + SUITE_FEATURE_TEXT="${BEHAT_SUITE}" + fi + + if [ -n "${BEHAT_FEATURE}" ] + then + # If running a whole feature, it will be something like login.feature + # If running just a single scenario, it will also have the line number + # like login.feature:36 - which will be parsed correctly like a "file" + # by basename. + BEHAT_FEATURE_FILE=`basename ${BEHAT_FEATURE}` + SUITE_FEATURE_TEXT="${SUITE_FEATURE_TEXT} ${BEHAT_FEATURE_FILE}" + fi +fi + +for i in "${!BEHAT_SUITES[@]}" + do + BEHAT_SUITE_TO_RUN="${BEHAT_SUITES[$i]}" + BEHAT_SUITE_OPTION="--suite=${BEHAT_SUITE_TO_RUN}" + SUITE_FEATURE_TEXT="${BEHAT_SUITES[$i]}" + for rerun_number in $(seq 1 ${BEHAT_RERUN_TIMES}) + do + if ((${BEHAT_RERUN_TIMES} > 1)) + then + echo -e "\nTest repeat $rerun_number of ${BEHAT_RERUN_TIMES}" + fi + run_behat_tests + done +done + +TOTAL_SCENARIOS=$((SCENARIOS_THAT_PASSED + SCENARIOS_THAT_FAILED)) + +echo "runsh: Total ${TOTAL_SCENARIOS} scenarios (${SCENARIOS_THAT_PASSED} passed, ${SCENARIOS_THAT_FAILED} failed)" + +teardown + +# 3 types of things can have gone wrong: +# - some scenario failed (and it was not expected to fail) +# - some scenario passed (but it was expected to fail) +# - Behat exited with non-zero status because of some other error +# If any of these happened then report about it and exit with status 1 (error) + +if [ ${#UNEXPECTED_FAILED_SCENARIOS[@]} -gt 0 ] +then + UNEXPECTED_FAILURE=true +else + UNEXPECTED_FAILURE=false +fi + +if [ ${#UNEXPECTED_PASSED_SCENARIOS[@]} -gt 0 ] +then + UNEXPECTED_SUCCESS=true +else + UNEXPECTED_SUCCESS=false +fi + +if [ ${#UNEXPECTED_BEHAT_EXIT_STATUSES[@]} -gt 0 ] +then + UNEXPECTED_BEHAT_EXIT_STATUS=true +else + UNEXPECTED_BEHAT_EXIT_STATUS=false +fi + +# If we got some unexpected success, and we only ran a single feature or scenario +# then the fact that some expected failures did not happen might be because those +# scenarios were never even run. +# Filter the UNEXPECTED_PASSED_SCENARIOS to remove scenarios that were not run. +if [ "${UNEXPECTED_SUCCESS}" = true ] +then + ACTUAL_UNEXPECTED_PASS=() + # if running a single feature or a single scenario + if [[ -n "${BEHAT_FEATURE}" ]] + then + for unexpected_passed_value in "${UNEXPECTED_PASSED_SCENARIOS[@]}" + do + # check only for the running feature + if [[ $BEHAT_FEATURE == *":"* ]] + then + BEHAT_FEATURE_WITH_LINE_NUM=$BEHAT_FEATURE + else + LINE_NUM=$(echo ${unexpected_passed_value} | cut -d":" -f2) + BEHAT_FEATURE_WITH_LINE_NUM=$BEHAT_FEATURE:$LINE_NUM + fi + if [[ $BEHAT_FEATURE_WITH_LINE_NUM == *"${unexpected_passed_value}" ]] + then + ACTUAL_UNEXPECTED_PASS+=("${unexpected_passed_value}") + fi + done + else + ACTUAL_UNEXPECTED_PASS=("${UNEXPECTED_PASSED_SCENARIOS[@]}") + fi + + if [ ${#ACTUAL_UNEXPECTED_PASS[@]} -eq 0 ] + then + UNEXPECTED_SUCCESS=false + fi +fi + +if [ "${UNEXPECTED_FAILURE}" = false ] && [ "${UNEXPECTED_SUCCESS}" = false ] && [ "${UNEXPECTED_BEHAT_EXIT_STATUS}" = false ] +then + FINAL_EXIT_STATUS=0 +else + FINAL_EXIT_STATUS=1 +fi + +if [ -n "${EXPECTED_FAILURES_FILE}" ] +then + echo "runsh: Exit code after checking expected failures: ${FINAL_EXIT_STATUS}" +fi + +if [ "${UNEXPECTED_FAILURE}" = true ] +then + tput setaf 3; echo "runsh: Total unexpected failed scenarios throughout the test run:" + tput setaf 1; printf "%s\n" "${UNEXPECTED_FAILED_SCENARIOS[@]}" +else + tput setaf 2; echo "runsh: There were no unexpected failures." +fi + +if [ "${UNEXPECTED_SUCCESS}" = true ] +then + tput setaf 3; echo "runsh: Total unexpected passed scenarios throughout the test run:" + tput setaf 1; printf "%s\n" "${ACTUAL_UNEXPECTED_PASS[@]}" +else + tput setaf 2; echo "runsh: There were no unexpected success." +fi + +if [ "${UNEXPECTED_BEHAT_EXIT_STATUS}" = true ] +then + tput setaf 3; echo "runsh: The following Behat test runs exited with non-zero status:" + tput setaf 1; printf "%s\n" "${UNEXPECTED_BEHAT_EXIT_STATUSES[@]}" +fi + +# sync the file-system so all output will be flushed to storage. +# In drone we sometimes see that the last lines of output are missing from the +# drone log. +sync + +# If we are running in drone CI, then sleep for a bit to (hopefully) let the +# drone agent send all the output to the drone server. +if [ -n "${CI_REPO}" ] +then + echo "sleeping for 30 seconds at end of test run" + sleep 30 +fi + +exit ${FINAL_EXIT_STATUS} diff --git a/tests/parallelDeployAcceptance/features/bootstrap/bootstrap.php b/tests/parallelDeployAcceptance/features/bootstrap/bootstrap.php index ec7e80734..4a3991c26 100644 --- a/tests/parallelDeployAcceptance/features/bootstrap/bootstrap.php +++ b/tests/parallelDeployAcceptance/features/bootstrap/bootstrap.php @@ -20,18 +20,14 @@ * */ -$pathToCore = \getenv('PATH_TO_CORE'); -if ($pathToCore === false) { - $pathToCore = "../core"; -} +use Composer\Autoload\ClassLoader; -require_once $pathToCore . '/tests/acceptance/features/bootstrap/bootstrap.php'; - -$classLoader = new \Composer\Autoload\ClassLoader(); +$classLoader = new ClassLoader(); $classLoader->addPsr4( "", - $pathToCore . "/tests/acceptance/features/bootstrap", + __DIR__ . "/../../../tests/acceptance/features/bootstrap", true ); +$classLoader->addPsr4("TestHelpers\\", __DIR__ . "/../../../TestHelpers", true); $classLoader->register(); diff --git a/vendor-bin/behat/composer.json b/vendor-bin/behat/composer.json index 999f00df8..56b0324d1 100644 --- a/vendor-bin/behat/composer.json +++ b/vendor-bin/behat/composer.json @@ -9,6 +9,7 @@ }, "require": { "behat/behat": "^3.9", + "behat/gherkin": "^4.9", "behat/mink": "1.7.1", "friends-of-behat/mink-extension": "^2.5", "ciaranmcnulty/behat-stepthroughextension" : "dev-master",