From b5fba5649957b4beca807d1fb18a6cfa32ffd6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 16:11:20 +0200 Subject: [PATCH 01/31] Add basic files for the automated acceptance test system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acceptance tests verify that a Nextcloud server works as expected from the point of view of an end-user. They are specified as user stories using Behat paired with Mink, which provides web browser automation. Mink supports several browser emulators, but the system is set up to use Selenium, as it is FOSS and the one that better reflects the use of a web browser by an end-user (as, in fact, it controls real web browsers). Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/composer.json | 9 ++++++ build/acceptance/config/behat.yml | 14 ++++++++++ .../features/bootstrap/FeatureContext.php | 28 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 build/acceptance/composer.json create mode 100644 build/acceptance/config/behat.yml create mode 100644 build/acceptance/features/bootstrap/FeatureContext.php diff --git a/build/acceptance/composer.json b/build/acceptance/composer.json new file mode 100644 index 00000000000..a361adaa40d --- /dev/null +++ b/build/acceptance/composer.json @@ -0,0 +1,9 @@ +{ + "require-dev": { + "behat/behat": "^3.0", + "behat/mink": "^1.5", + "behat/mink-extension": "*", + "behat/mink-selenium2-driver": "*", + "phpunit/phpunit": "~4.6" + } +} diff --git a/build/acceptance/config/behat.yml b/build/acceptance/config/behat.yml new file mode 100644 index 00000000000..01feef51608 --- /dev/null +++ b/build/acceptance/config/behat.yml @@ -0,0 +1,14 @@ +default: + autoload: + '': %paths.base%/../features/bootstrap + suites: + default: + paths: + - %paths.base%/../features + contexts: + - FeatureContext + extensions: + Behat\MinkExtension: + sessions: + default: + selenium2: ~ diff --git a/build/acceptance/features/bootstrap/FeatureContext.php b/build/acceptance/features/bootstrap/FeatureContext.php new file mode 100644 index 00000000000..b309a7e04b0 --- /dev/null +++ b/build/acceptance/features/bootstrap/FeatureContext.php @@ -0,0 +1,28 @@ +. + * + */ + +use Behat\Behat\Context\Context; + +class FeatureContext implements Context { + +} From 4c620f1fcbd57c3405e3fe65e55892eb0dccccc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 16:11:34 +0200 Subject: [PATCH 02/31] Add helper context to isolate the test server with Docker containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scenarios in acceptance tests must be independent one of each other. That is, the execution of one scenario can not affect the execution of another scenario, nor it can depend on the result of the execution of a different scenario. Each scenario must be isolated and self-contained. As the acceptance tests are run against a Nextcloud server the server must be in a known and predefined initial state each time a scenario begins. The NextcloudTestServerContext is introduced to automatically set up the Nextcloud test server for each scenario. This can be achieved using Docker containers. Before an scenario begins a new Docker container with a Nextcloud server is run; the scenario is then run against the server provided by the container. When the scenario ends the container is destroyed. As long as the Nextcloud server uses local data storage each scenario is thus isolated from the rest. The NextcloudTestServerContext also notifies its sibling RawMinkContexts about the base URL of the Nextcloud test server being used in each scenario. Although it uses the Behat context system, NextcloudTestServerContext is not really part of the acceptance tests, but a provider of core features needed by them; it can be seen as part of a Nextcloud acceptance test library. Therefore, those classes are stored in the "core" directory instead of the "bootstrap" directory. Besides its own (quite limited) autoload configuration, Behat also uses the Composer autoloader, so the "core" directory has to be added there for its classes to be found by Behat. Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/composer.json | 5 + build/acceptance/config/behat.yml | 2 + .../core/NextcloudTestServerContext.php | 156 +++++++++++++ .../core/NextcloudTestServerDockerHelper.php | 206 ++++++++++++++++++ build/acceptance/features/core/Utils.php | 59 +++++ 5 files changed, 428 insertions(+) create mode 100644 build/acceptance/features/core/NextcloudTestServerContext.php create mode 100644 build/acceptance/features/core/NextcloudTestServerDockerHelper.php create mode 100644 build/acceptance/features/core/Utils.php diff --git a/build/acceptance/composer.json b/build/acceptance/composer.json index a361adaa40d..87b6ba4a22c 100644 --- a/build/acceptance/composer.json +++ b/build/acceptance/composer.json @@ -5,5 +5,10 @@ "behat/mink-extension": "*", "behat/mink-selenium2-driver": "*", "phpunit/phpunit": "~4.6" + }, + "autoload": { + "psr-4": { + "": "features/core" + } } } diff --git a/build/acceptance/config/behat.yml b/build/acceptance/config/behat.yml index 01feef51608..2ac1c077537 100644 --- a/build/acceptance/config/behat.yml +++ b/build/acceptance/config/behat.yml @@ -6,6 +6,8 @@ default: paths: - %paths.base%/../features contexts: + - NextcloudTestServerContext + - FeatureContext extensions: Behat\MinkExtension: diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/build/acceptance/features/core/NextcloudTestServerContext.php new file mode 100644 index 00000000000..cdd07dab168 --- /dev/null +++ b/build/acceptance/features/core/NextcloudTestServerContext.php @@ -0,0 +1,156 @@ +. + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; + +/** + * Behat context to run each scenario against a clean Nextcloud server. + * + * Before each scenario is run, this context sets up a fresh Nextcloud server + * with predefined data and configuration. Thanks to this every scenario is + * independent from the others and they all know the initial state of the + * server. + * + * This context is expected to be used along with RawMinkContext contexts (or + * subclasses). As the server address can be different for each scenario, this + * context automatically sets the "base_url" parameter of all its sibling + * RawMinkContexts; just add NextcloudTestServerContext to the context list of a + * suite in "behat.yml". + * + * The Nextcloud server is set up by running a new Docker container; the Docker + * image used by the container must provide a Nextcloud server ready to be used + * by the tests. By default, the image "nextcloud-local-test-acceptance" is + * used, although that can be customized using the "dockerImageName" parameter + * in "behat.yml". In the same way, the range of ports in which the Nextcloud + * server will be published in the local host (by default, "15000-16000") can be + * customized using the "hostPortRangeForContainer" parameter. + * + * Note that using Docker containers as a regular user requires giving access to + * the Docker daemon to that user. Unfortunately, that makes possible for that + * user to get root privileges for the system. Please see the + * NextcloudTestServerDockerHelper documentation for further information on this + * issue. + */ +class NextcloudTestServerContext implements Context { + + /** + * @var NextcloudTestServerDockerHelper + */ + private $dockerHelper; + + /** + * Creates a new NextcloudTestServerContext. + * + * @param string $dockerImageName the name of the Docker image that provides + * the Nextcloud test server. + * @param string $hostPortRangeForContainer the range of local ports in the + * host in which the port 80 of the container can be published. + */ + public function __construct($dockerImageName = "nextcloud-local-test-acceptance", $hostPortRangeForContainer = "15000-16000") { + $this->dockerHelper = new NextcloudTestServerDockerHelper($dockerImageName, $hostPortRangeForContainer); + } + + /** + * @BeforeScenario + * + * Sets up the Nextcloud test server before each scenario. + * + * It starts the Docker container and, once ready, it sets the "base_url" + * parameter of the sibling RawMinkContexts to "http://" followed by the IP + * address and port of the container; if the Docker container can not be + * started after some time an exception is thrown (as it is just a warning + * for the test runner and nothing to be explicitly catched a plain base + * Exception is used). + * + * @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope the + * BeforeScenario hook scope. + * @throws \Exception if the Docker container can not be started. + */ + public function startNextcloudTestServer(BeforeScenarioScope $scope) { + $this->dockerHelper->createAndStartContainer(); + + $serverAddress = $this->dockerHelper->getNextcloudTestServerAddress(); + + $isServerReadyCallback = function() use ($serverAddress) { + return $this->isServerReady($serverAddress); + }; + $timeout = 10; + $timeoutStep = 0.5; + if (!Utils::waitFor($isServerReadyCallback, $timeout, $timeoutStep)) { + throw new Exception("Docker container for Nextcloud could not be started"); + } + + $this->setBaseUrlInSiblingRawMinkContexts($scope, "http://" . $serverAddress . "/index.php"); + } + + /** + * @AfterScenario + * + * Cleans up the Nextcloud test server after each scenario. + * + * It stops and removes the Docker container; if the Docker container can + * not be removed after some time an exception is thrown (as it is just a + * warning for the test runner and nothing to be explicitly catched a plain + * base Exception is used). + * + * @throws \Exception if the Docker container can not be removed. + */ + public function stopNextcloudTestServer() { + $this->dockerHelper->stopAndRemoveContainer(); + + $wasContainerRemovedCallback = function() { + return !$this->dockerHelper->isContainerRegistered(); + }; + $timeout = 10; + $timeoutStep = 0.5; + if (!Utils::waitFor($wasContainerRemovedCallback, $timeout, $timeoutStep)) { + throw new Exception("Docker container for Nextcloud (" . $this->dockerHelper->getContainerName() . ") could not be removed"); + } + } + + private function isServerReady($serverAddress) { + $curlHandle = curl_init("http://" . $serverAddress); + + // Returning the transfer as the result of curl_exec prevents the + // transfer from being written to the output. + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + + $transfer = curl_exec($curlHandle); + + curl_close($curlHandle); + + return $transfer !== false; + } + + private function setBaseUrlInSiblingRawMinkContexts(BeforeScenarioScope $scope, $baseUrl) { + $environment = $scope->getEnvironment(); + + foreach ($environment->getContexts() as $context) { + if ($context instanceof Behat\MinkExtension\Context\RawMinkContext) { + $context->setMinkParameter("base_url", $baseUrl); + } + } + } + +} diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php new file mode 100644 index 00000000000..7ed159ead7b --- /dev/null +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -0,0 +1,206 @@ +. + * + */ + +/** + * Helper to manage the Docker container for the Nextcloud test server. + * + * The NextcloudTestServerDockerHelper abstracts the calls to the Docker Command + * Line Interface (the "docker" command) to run, get information from, and + * destroy containers. It is not a generic abstraction, but one tailored + * specifically to the Nextcloud test server; a Docker image that provides an + * installed and ready to run Nextcloud server with the configuration and data + * expected by the acceptance tests must be available in the system. The + * Nextcloud server must use a local storage so all the changes it makes are + * confined to its running container. + * + * Also, the Nextcloud server installed in the Docker image is expected to see + * "127.0.0.1" as a trusted domain (which would be the case if it was installed + * by running "occ maintenance:install"). Therefore, the Nextcloud server is + * accessed through a local port in the host system mapped to the port 80 of the + * Docker container; if the Nextcloud server was instead accessed directly + * through its IP address it would complain that it was being accessed from an + * untrusted domain and refuse to work until the admin whitelisted it. The IP + * address and port to access the Nextcloud server can be got from + * "getNextcloudTestServerAddress". + * + * For better compatibility, Docker CLI commands used internally follow the + * pre-1.13 syntax (also available in 1.13 and newer). For example, + * "docker start" instead of "docker container start". + * + * In any case, the "docker" command requires special permissions to talk to the + * Docker daemon, and those permissions are typically available only to the root + * user. However, you should NOT run the acceptance tests as root, but as a + * regular user instead. Please see the Docker documentation to find out how to + * give access to a regular user to the Docker daemon: + * https://docs.docker.com/engine/installation/linux/linux-postinstall/ + * + * Note, however, that being able to communicate with the Docker daemon is the + * same as being able to get root privileges for the system. Therefore, you must + * give access to the Docker daemon (and thus run the acceptance tests as) ONLY + * to trusted and secure users: + * https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface + * + * All the public methods that use the 'docker' command throw an exception if + * the command can not be executed or if it does not have enough permissions to + * connect to the Docker daemon; as, due to the current use of this class, it is + * just a warning for the test runner and nothing to be explicitly catched a + * plain base Exception is used. + */ +class NextcloudTestServerDockerHelper { + + /** + * @var string + */ + private $imageName; + + /** + * @var string + */ + private $hostPortRangeForContainer; + + /** + * @var string + */ + private $containerName; + + /** + * Creates a new NextcloudTestServerDockerHelper. + * + * @param string $imageName the name of the Docker image that provides the + * Nextcloud test server. + * @param string $hostPortRangeForContainer the range of local ports in the + * host in which the port 80 of the container can be published. + */ + public function __construct($imageName = "nextcloud-local-test-acceptance", $hostPortRangeForContainer = "15000-16000") { + $this->imageName = $imageName; + $this->hostPortRangeForContainer = $hostPortRangeForContainer; + $this->containerName = null; + } + + /** + * Creates and starts the container. + * + * Note that, even if the container has started, the server it contains may + * not have started yet when this method returns. + * + * @throws \Exception if the Docker command failed to execute. + */ + public function createAndStartContainer() { + $moreEntropy = true; + $this->containerName = uniqid($this->imageName . "-", $moreEntropy); + + // There is no need to start the web server as root, so it is started + // directly as www-data instead. + // The port 80 of the container is mapped to a free port from a range in + // the host system; due to this it can be accessed from the host using + // the "127.0.0.1" IP address, which prevents Nextcloud from complaining + // that it is being accessed from an untrusted domain. + $this->executeDockerCommand("run --detach --user=www-data --publish 127.0.0.1:" . $this->hostPortRangeForContainer . ":80 --name=" . $this->containerName . " " . $this->imageName); + } + + /** + * Stops and removes the container. + * + * @throws \Exception if the Docker command failed to execute. + */ + public function stopAndRemoveContainer() { + // Although the Nextcloud image does not define a volume "--volumes" is + // used anyway just in case any of its ancestor images does. + $this->executeDockerCommand("rm --volumes --force " . $this->containerName); + } + + /** + * Returns the container name. + * + * If the container has not been created yet the container name will be + * null. + * + * @return string the container name. + */ + public function getContainerName() { + return $this->containerName; + } + + /** + * Returns the IP address and port of the Nextcloud test server (which is + * mapped to a local port in the host). + * + * @return string the IP address and port as "$ipAddress:$port". + * @throws \Exception if the Docker command failed to execute or the + * container is not running. + */ + public function getNextcloudTestServerAddress() { + return $this->executeDockerCommand("port " . $this->containerName . " 80"); + } + + /** + * Returns whether the container is running or not. + * + * @return boolean true if the container is running, false otherwise. + * @throws \Exception if the Docker command failed to execute. + */ + public function isContainerRunning() { + // By default, "docker ps" only shows running containers, and the + // "--quiet" option only shows the ID of the matching containers, + // without table headers. Therefore, if the container is not running the + // output will be empty (not even a new line, as the last line of output + // returned by "executeDockerCommand" does not include a trailing new + // line character). + return $this->executeDockerCommand("ps --quiet --filter 'name=" . $this->containerName . "'") !== ""; + } + + /** + * Returns whether the container exists (no matter its state) or not. + * + * @return boolean true if the container exists, false otherwise. + * @throws \Exception if the Docker command failed to execute. + */ + public function isContainerRegistered() { + // With the "--quiet" option "docker ps" only shows the ID of the + // matching containers, without table headers. Therefore, if the + // container does not exist the output will be empty (not even a new + // line, as the last line of output returned by "executeDockerCommand" + // does not include a trailing new line character). + return $this->executeDockerCommand("ps --all --quiet --filter 'name=" . $this->containerName . "'") !== ""; + } + + /** + * Executes the given Docker command. + * + * @return string the last line of output, without trailing new line + * character. + * @throws \Exception if the Docker command failed to execute. + */ + private function executeDockerCommand($dockerCommand) { + $output = array(); + $returnValue = 0; + $lastLine = exec("docker " . $dockerCommand . " 2>&1", $output, $returnValue); + + if ($returnValue !== 0) { + throw new Exception("Failed to execute 'docker " . $dockerCommand . "': " . implode("\n", $output)); + } + + return $lastLine; + } + +} diff --git a/build/acceptance/features/core/Utils.php b/build/acceptance/features/core/Utils.php new file mode 100644 index 00000000000..5dc52cd7377 --- /dev/null +++ b/build/acceptance/features/core/Utils.php @@ -0,0 +1,59 @@ +. + * + */ + +class Utils { + + /** + * Waits at most $timeout seconds for the given condition to be true, + * checking it again every $timeoutStep seconds. + * + * Note that the timeout is no longer taken into account when a condition is + * met; that is, true will be returned if the condition is met before the + * timeout expires, but also if it is met exactly when the timeout expires. + * For example, even if the timeout is set to 0, the condition will be + * checked at least once, and true will be returned in that case if the + * condition was met. + * + * @param \Closure $conditionCallback the condition to wait for, as a + * function that returns a boolean. + * @param float $timeout the number of seconds (decimals allowed) to wait at + * most for the condition to be true. + * @param float $timeoutStep the number of seconds (decimals allowed) to + * wait before checking the condition again. + * @return boolean true if the condition is met before (or exactly when) the + * timeout expires, false otherwise. + */ + public static function waitFor($conditionCallback, $timeout, $timeoutStep) { + $elapsedTime = 0; + $conditionMet = false; + + while (!($conditionMet = $conditionCallback()) && $elapsedTime < $timeout) { + usleep($timeoutStep * 1000000); + + $elapsedTime += $timeoutStep; + } + + return $conditionMet; + } + +} From 7c07f01d59dae4a8c804bad3699baef3084cf128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 16:18:05 +0200 Subject: [PATCH 03/31] Add actors for test scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An actor plays the role of an end-user in the test scenario. As such, each actor has its own web browser session used to perform the actions specified by the steps of the scenario. Only one actor is active at a time in a test scenario, and the current actor can be set through the "I act as XXX" step; from then on, all the steps are performed by that actor, until a different actor is set by calling "I act as XXX" again. If no actor was explicitly set in a scenario then the default actor, unsurprisingly named "default", is the one used. The ActorContext class is added to provide automatic support for all that. To use the ActorContext, besides adding it to the context list in "behat.yml", a Mink session for each actor used in the features must be specified in "behat.yml". Once done other Contexts just need to implement the ActorAwareInterface (which can be done simply by using the ActorAware trait) to have access to the current Actor object of the test scenario; as the Actor object provides its own session other Contexts do not need to extend from RawMinkContext. The ActorContext is itself a RawMinkContext, so it automatically receives the base URL of the Nextcloud test server run by NextcloudTestServerContext and propagates that base URL to all the actors. Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/config/behat.yml | 1 + .../features/bootstrap/FeatureContext.php | 4 +- build/acceptance/features/core/Actor.php | 92 +++++++++++++ build/acceptance/features/core/ActorAware.php | 38 ++++++ .../features/core/ActorAwareInterface.php | 31 +++++ .../acceptance/features/core/ActorContext.php | 127 ++++++++++++++++++ 6 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 build/acceptance/features/core/Actor.php create mode 100644 build/acceptance/features/core/ActorAware.php create mode 100644 build/acceptance/features/core/ActorAwareInterface.php create mode 100644 build/acceptance/features/core/ActorContext.php diff --git a/build/acceptance/config/behat.yml b/build/acceptance/config/behat.yml index 2ac1c077537..186a57c2023 100644 --- a/build/acceptance/config/behat.yml +++ b/build/acceptance/config/behat.yml @@ -6,6 +6,7 @@ default: paths: - %paths.base%/../features contexts: + - ActorContext - NextcloudTestServerContext - FeatureContext diff --git a/build/acceptance/features/bootstrap/FeatureContext.php b/build/acceptance/features/bootstrap/FeatureContext.php index b309a7e04b0..0598cbae959 100644 --- a/build/acceptance/features/bootstrap/FeatureContext.php +++ b/build/acceptance/features/bootstrap/FeatureContext.php @@ -23,6 +23,8 @@ use Behat\Behat\Context\Context; -class FeatureContext implements Context { +class FeatureContext implements Context, ActorAwareInterface { + + use ActorAware; } diff --git a/build/acceptance/features/core/Actor.php b/build/acceptance/features/core/Actor.php new file mode 100644 index 00000000000..0fea9be20c0 --- /dev/null +++ b/build/acceptance/features/core/Actor.php @@ -0,0 +1,92 @@ +. + * + */ + +/** + * An actor in a test scenario. + * + * Every Actor object is intended to be used only in a single test scenario. + * An Actor can control its web browser thanks to the Mink Session received when + * it was created, so in each scenario each Actor must have its own Mink + * Session; the same Mink Session can be used by different Actors in different + * scenarios, but never by different Actors in the same scenario. + * + * The test servers used in an scenario can change between different test runs, + * so an Actor stores the base URL for the current test server being used; in + * most cases the tests are specified using relative paths that can be converted + * to the appropriate absolute URL using locatePath() in the step + * implementation. + */ +class Actor { + + /** + * @var \Behat\Mink\Session + */ + private $session; + + /** + * @var string + */ + private $baseUrl; + + /** + * Creates a new Actor. + * + * @param \Behat\Mink\Session $session the Mink Session used to control its + * web browser. + * @param string $baseUrl the base URL used when solving relative URLs. + */ + public function __construct(\Behat\Mink\Session $session, $baseUrl) { + $this->session = $session; + $this->baseUrl = $baseUrl; + } + + /** + * Sets the base URL. + * + * @param string $baseUrl the base URL used when solving relative URLs. + */ + public function setBaseUrl($baseUrl) { + $this->baseUrl = $baseUrl; + } + + /** + * Returns the Mink Session used to control its web browser. + * + * @return \Behat\Mink\Session the Mink Session used to control its web + * browser. + */ + public function getSession() { + return $this->session; + } + + /** + * Returns the full path for the given relative path based on the base URL. + * + * @param string relativePath the relative path. + * @return string the full path. + */ + public function locatePath($relativePath) { + return $this->baseUrl . $relativePath; + } + +} diff --git a/build/acceptance/features/core/ActorAware.php b/build/acceptance/features/core/ActorAware.php new file mode 100644 index 00000000000..f1d355c1b0e --- /dev/null +++ b/build/acceptance/features/core/ActorAware.php @@ -0,0 +1,38 @@ +. + * + */ + +trait ActorAware { + + /** + * @var Actor + */ + private $actor; + + /** + * @param Actor $actor + */ + public function setCurrentActor(Actor $actor) { + $this->actor = $actor; + } + +} diff --git a/build/acceptance/features/core/ActorAwareInterface.php b/build/acceptance/features/core/ActorAwareInterface.php new file mode 100644 index 00000000000..9363bc3e607 --- /dev/null +++ b/build/acceptance/features/core/ActorAwareInterface.php @@ -0,0 +1,31 @@ +. + * + */ + +interface ActorAwareInterface { + + /** + * @param Actor $actor + */ + public function setCurrentActor(Actor $actor); + +} diff --git a/build/acceptance/features/core/ActorContext.php b/build/acceptance/features/core/ActorContext.php new file mode 100644 index 00000000000..e655911af67 --- /dev/null +++ b/build/acceptance/features/core/ActorContext.php @@ -0,0 +1,127 @@ +. + * + */ + +use Behat\Behat\Hook\Scope\BeforeStepScope; +use Behat\MinkExtension\Context\RawMinkContext; + +/** + * Behat context to set the actor used in sibling contexts. + * + * This helper context provides a step definition ("I act as XXX") to change the + * current actor of the scenario, which makes possible to use different browser + * sessions in the same scenario. + * + * Sibling contexts that want to have access to the current actor of the + * scenario must implement the ActorAwareInterface; this can be done just by + * using the ActorAware trait. + * + * Besides updating the current actor in sibling contexts the ActorContext also + * propagates its inherited "base_url" Mink parameter to the Actors as needed. + * + * Every actor used in the scenarios must have a corresponding Mink session + * declared in "behat.yml" with the same name as the actor. All used sessions + * are stopped after each scenario is run. + */ +class ActorContext extends RawMinkContext { + + /** + * @var array + */ + private $actors; + + /** + * @var Actor + */ + private $currentActor; + + /** + * Sets a Mink parameter. + * + * When the "base_url" parameter is set its value is propagated to all the + * Actors. + * + * @param string $name the name of the parameter. + * @param string $value the value of the parameter. + */ + public function setMinkParameter($name, $value) { + parent::setMinkParameter($name, $value); + + if ($name === "base_url") { + foreach ($this->actors as $actor) { + $actor->setBaseUrl($value); + } + } + } + + /** + * @BeforeScenario + * + * Initializes the Actors for the new Scenario with the default Actor. + * + * Other Actors are added (and their Mink Sessions started) only when they + * are used in an "I act as XXX" step. + */ + public function initializeActors() { + $this->actors = array(); + + $this->actors["default"] = new Actor($this->getSession(), $this->getMinkParameter("base_url")); + + $this->currentActor = $this->actors["default"]; + } + + /** + * @BeforeStep + */ + public function setCurrentActorInSiblingActorAwareContexts(BeforeStepScope $scope) { + $environment = $scope->getEnvironment(); + + foreach ($environment->getContexts() as $context) { + if ($context instanceof ActorAwareInterface) { + $context->setCurrentActor($this->currentActor); + } + } + } + + /** + * @Given I act as :actorName + */ + public function iActAs($actorName) { + if (!array_key_exists($actorName, $this->actors)) { + $this->actors[$actorName] = new Actor($this->getSession($actorName), $this->getMinkParameter("base_url")); + } + + $this->currentActor = $this->actors[$actorName]; + } + + /** + * @AfterScenario + * + * Stops all the Mink Sessions used in the last Scenario. + */ + public function cleanUpSessions() { + foreach ($this->actors as $actor) { + $actor->getSession()->stop(); + } + } + +} From b22997796b735005d13aa390dc78e47e09940dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 16:25:22 +0200 Subject: [PATCH 04/31] Add wrappers to adapt the element finding system of Mink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mink elements (including the document element) provide a "find(selector, locator)" method to look for child elements in their web browser session. The Locator class is added to be able to store the selector and locator in a single object; it also provides a fluent API to ease the definition of Mink locators, specially those using the "named" selector. The method "find(locator, timeout, timeoutStep)" is added to Actor objects; it is simply a wrapper over Mink's "find(selector, locator)" method, although it throws an exception if the element can not be found instead of returning null, and it also makes possible to automatically retry to find the element for certain amount of time. Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/features/core/Actor.php | 99 ++++++ build/acceptance/features/core/Locator.php | 329 ++++++++++++++++++ .../features/core/NoSuchElementException.php | 37 ++ 3 files changed, 465 insertions(+) create mode 100644 build/acceptance/features/core/Locator.php create mode 100644 build/acceptance/features/core/NoSuchElementException.php diff --git a/build/acceptance/features/core/Actor.php b/build/acceptance/features/core/Actor.php index 0fea9be20c0..2445a09bffc 100644 --- a/build/acceptance/features/core/Actor.php +++ b/build/acceptance/features/core/Actor.php @@ -35,6 +35,12 @@ * most cases the tests are specified using relative paths that can be converted * to the appropriate absolute URL using locatePath() in the step * implementation. + * + * An Actor can find elements in its Mink Session using its find() method; it is + * a wrapper over the find() method provided by Mink that extends it with + * several features: the element can be looked for based on a Locator object, an + * exception is thrown if the element is not found, and, optionally, it is + * possible to try again to find the element several times before giving up. */ class Actor { @@ -89,4 +95,97 @@ class Actor { return $this->baseUrl . $relativePath; } + /** + * Finds an element in the Mink Session of this Actor. + * + * The given element locator is relative to its ancestor (either another + * locator or an actual element); if it has no ancestor then the base + * document element is used. + * + * Sometimes an element may not be found simply because it has not appeared + * yet; for those cases this method supports trying again to find the + * element several times before giving up. The timeout parameter controls + * how much time to wait, at most, to find the element; the timeoutStep + * parameter controls how much time to wait before trying again to find the + * element. If ancestor locators need to be found the timeout is applied + * individually to each one, that is, if the timeout is 10 seconds the + * method will wait up to 10 seconds to find the ancestor of the ancestor + * and, then, up to 10 seconds to find the ancestor and, then, up to 10 + * seconds to find the element. By default the timeout is 0, so the element + * and its ancestor will be looked for just once; the default time to wait + * before retrying is half a second. + * + * In any case, if the element, or its ancestors, can not be found a + * NoSuchElementException is thrown. + * + * @param Locator $elementLocator the locator for the element. + * @param float $timeout the number of seconds (decimals allowed) to wait at + * most for the element to appear. + * @param float $timeoutStep the number of seconds (decimals allowed) to + * wait before trying to find the element again. + * @return \Behat\Mink\Element\Element the element found. + * @throws NoSuchElementException if the element, or its ancestor, can not + * be found. + */ + public function find($elementLocator, $timeout = 0, $timeoutStep = 0.5) { + $element = null; + $selector = $elementLocator->getSelector(); + $locator = $elementLocator->getLocator(); + $ancestorElement = $this->findAncestorElement($elementLocator, $timeout, $timeoutStep); + + $findCallback = function() use (&$element, $selector, $locator, $ancestorElement) { + $element = $ancestorElement->find($selector, $locator); + + return $element !== null; + }; + if (!Utils::waitFor($findCallback, $timeout, $timeoutStep)) { + throw new NoSuchElementException($elementLocator->getDescription() . " could not be found"); + } + + return $element; + } + + /** + * Returns the ancestor element from which the given locator will be looked + * for. + * + * If the ancestor of the given locator is another locator the element for + * the ancestor locator is found and returned. If the ancestor of the given + * locator is already an element that element is the one returned. If the + * given locator has no ancestor then the base document element is returned. + * + * The timeout is used only when finding the element for the ancestor + * locator; if the timeout expires a NoSuchElementException is thrown. + * + * @param Locator $elementLocator the locator for the element to get its + * ancestor. + * @param float $timeout the number of seconds (decimals allowed) to wait at + * most for the ancestor element to appear. + * @param float $timeoutStep the number of seconds (decimals allowed) to + * wait before trying to find the ancestor element again. + * @return \Behat\Mink\Element\Element the ancestor element found. + * @throws NoSuchElementException if the ancestor element can not be found. + */ + private function findAncestorElement($elementLocator, $timeout, $timeoutStep) { + $ancestorElement = $elementLocator->getAncestor(); + if ($ancestorElement instanceof Locator) { + try { + $ancestorElement = $this->find($ancestorElement, $timeout, $timeoutStep); + } catch (NoSuchElementException $exception) { + // Little hack to show the stack of ancestor elements that could + // not be found, as Behat only shows the message of the last + // exception in the chain. + $message = $exception->getMessage() . "\n" . + $elementLocator->getDescription() . " could not be found"; + throw new NoSuchElementException($message, $exception); + } + } + + if ($ancestorElement === null) { + $ancestorElement = $this->getSession()->getPage(); + } + + return $ancestorElement; + } + } diff --git a/build/acceptance/features/core/Locator.php b/build/acceptance/features/core/Locator.php new file mode 100644 index 00000000000..0ebae9b8fb1 --- /dev/null +++ b/build/acceptance/features/core/Locator.php @@ -0,0 +1,329 @@ +. + * + */ + +/** + * Data object for the information needed to locate an element in a web page + * using Mink. + * + * Locators can be created directly using the constructor, or through a more + * fluent interface with Locator::forThe(). + */ +class Locator { + + /** + * @var string + */ + private $description; + + /** + * @var string + */ + private $selector; + + /** + * @var string|array + */ + private $locator; + + /** + * @var null|Locator|\Behat\Mink\Element\ElementInterface + */ + private $ancestor; + + /** + * Starting point for the fluent interface to create Locators. + * + * @return LocatorBuilder + */ + public static function forThe() { + return new LocatorBuilder(); + } + + /** + * @param string $description + * @param string $selector + * @param string|array $locator + * @param null|Locator|\Behat\Mink\Element\ElementInterface $ancestor + */ + public function __construct($description, $selector, $locator, $ancestor = null) { + $this->description = $description; + $this->selector = $selector; + $this->locator = $locator; + $this->ancestor = $ancestor; + } + + /** + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * @return string + */ + public function getSelector() { + return $this->selector; + } + + /** + * @return string|array + */ + public function getLocator() { + return $this->locator; + } + + /** + * @return null|Locator|\Behat\Mink\Element\ElementInterface + */ + public function getAncestor() { + return $this->ancestor; + } + +} + +class LocatorBuilder { + + /** + * @param string $selector + * @param string|array $locator + * @return LocatorBuilderSecondStep + */ + public function customSelector($selector, $locator) { + return new LocatorBuilderSecondStep($selector, $locator); + } + + /** + * @param string $cssExpression + * @return LocatorBuilderSecondStep + */ + public function css($cssExpression) { + return $this->customSelector("css", $cssExpression); + } + + /** + * @param string $xpathExpression + * @return LocatorBuilderSecondStep + */ + public function xpath($xpathExpression) { + return $this->customSelector("xpath", $xpathExpression); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function id($value) { + return $this->customSelector("named", array("id", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function idOrName($value) { + return $this->customSelector("named", array("id_or_name", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function link($value) { + return $this->customSelector("named", array("link", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function button($value) { + return $this->customSelector("named", array("button", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function linkOrButton($value) { + return $this->customSelector("named", array("link_or_button", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function content($value) { + return $this->customSelector("named", array("content", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function field($value) { + return $this->customSelector("named", array("field", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function selectField($value) { + return $this->customSelector("named", array("select", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function checkbox($value) { + return $this->customSelector("named", array("checkbox", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function radioButton($value) { + return $this->customSelector("named", array("radio", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function fileInput($value) { + return $this->customSelector("named", array("file", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function optionGroup($value) { + return $this->customSelector("named", array("optgroup", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function option($value) { + return $this->customSelector("named", array("option", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function fieldSet($value) { + return $this->customSelector("named", array("fieldset", $value)); + } + + /** + * @param string $value + * @return LocatorBuilderSecondStep + */ + public function table($value) { + return $this->customSelector("named", array("table", $value)); + } + +} + +class LocatorBuilderSecondStep { + + /** + * @var string + */ + private $selector; + + /** + * @var string|array + */ + private $locator; + + /** + * @param string $selector + * @param string|array $locator + */ + public function __construct($selector, $locator) { + $this->selector = $selector; + $this->locator = $locator; + } + + /** + * @param Locator|\Behat\Mink\Element\ElementInterface $ancestor + * @return LocatorBuilderThirdStep + */ + public function descendantOf($ancestor) { + return new LocatorBuilderThirdStep($this->selector, $this->locator, $ancestor); + } + + /** + * @param string $description + * @return Locator + */ + public function describedAs($description) { + return new Locator($description, $this->selector, $this->locator); + } + +} + +class LocatorBuilderThirdStep { + + /** + * @var string + */ + private $selector; + + /** + * @var string|array + */ + private $locator; + + /** + * @var Locator|\Behat\Mink\Element\ElementInterface + */ + private $ancestor; + + /** + * @param string $selector + * @param string|array $locator + * @param Locator|\Behat\Mink\Element\ElementInterface $ancestor + */ + public function __construct($selector, $locator, $ancestor) { + $this->selector = $selector; + $this->locator = $locator; + $this->ancestor = $ancestor; + } + + /** + * @param string $description + * @return Locator + */ + public function describedAs($description) { + return new Locator($description, $this->selector, $this->locator, $this->ancestor); + } + +} diff --git a/build/acceptance/features/core/NoSuchElementException.php b/build/acceptance/features/core/NoSuchElementException.php new file mode 100644 index 00000000000..5f8270d2a49 --- /dev/null +++ b/build/acceptance/features/core/NoSuchElementException.php @@ -0,0 +1,37 @@ +. + * + */ + +/** + * Exception to signal that the element looked for could not be found. + */ +class NoSuchElementException extends \Exception { + + /** + * @param string $message + * @param null|\Exception $previous + */ + public function __construct($message, \Exception $previous = null) { + parent::__construct($message, 0, $previous); + } + +} From 6a15d9da9cbf3012a87a8172da9331c512bc014e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 16:31:23 +0200 Subject: [PATCH 05/31] Add script to set up and run the acceptance tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acceptance tests require several elements to be set up in order to be run. Besides those PHP packages that it depends on, like Behat or Mink, it requires a running Selenium server and a Docker image with the Nextcloud server to be tested available in the system. The "run.sh" script takes care of preparing all the needed elements and then run the acceptance tests; once finished, either normally or due to an error, it also cleans up the temporal elements created/started by the script and the acceptance tests. The Docker image with the Nextcloud server to be tested is created from the Nextcloud code in the greatparent directory each time "run.sh" is executed; the code is copied inside the image, so once the acceptance tests are started the code in the greatparent directory can be modified without affecting them. As it is based on the current code at the time of the launch that image is created and destroyed each time the acceptance tests are run. However, the image that it is based on, which is created using "docker/nextcloud-local-parent/Dockerfile", does not change between runs, so it is kept built in the system to speed up the launch of acceptance tests. Signed-off-by: Daniel Calviño Sánchez --- .../docker/nextcloud-local-parent/Dockerfile | 52 ++++ build/acceptance/run.sh | 272 ++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 build/acceptance/docker/nextcloud-local-parent/Dockerfile create mode 100755 build/acceptance/run.sh diff --git a/build/acceptance/docker/nextcloud-local-parent/Dockerfile b/build/acceptance/docker/nextcloud-local-parent/Dockerfile new file mode 100644 index 00000000000..5e2fd5277bd --- /dev/null +++ b/build/acceptance/docker/nextcloud-local-parent/Dockerfile @@ -0,0 +1,52 @@ +# This Dockerfile builds an image of a system in which a Nextcloud server could +# be installed. It is based on the Dockerfile for Nextcloud 11 on Apache +# (https://github.com/nextcloud/docker/blob/843d309ee62b9d2704e6141d2103f9ded97e35b6/11.0/apache/Dockerfile), +# although without the download and copy of a specific Nextcloud version; there +# is no volume either at "/var/www/html" to make possible to create a new image +# from a container based on this image that includes the installed Nextcloud +# server of the container (as the command to generate a new image from a +# container, "docker commit", does not include in the new image any data stored +# in volumes mounted inside the container). + +FROM php:7.1-apache + +RUN apt-get update && apt-get install -y \ + bzip2 \ + libcurl4-openssl-dev \ + libfreetype6-dev \ + libicu-dev \ + libjpeg-dev \ + libldap2-dev \ + libmcrypt-dev \ + libmemcached-dev \ + libpng12-dev \ + libpq-dev \ + libxml2-dev \ + && rm -rf /var/lib/apt/lists/* + +# https://docs.nextcloud.com/server/9/admin_manual/installation/source_installation.html +RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \ + && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \ + && docker-php-ext-install gd exif intl mbstring mcrypt ldap mysqli opcache pdo_mysql pdo_pgsql pgsql zip + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=60'; \ + echo 'opcache.fast_shutdown=1'; \ + echo 'opcache.enable_cli=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini +RUN a2enmod rewrite + +# PECL extensions +RUN set -ex \ + && pecl install APCu-5.1.8 \ + && pecl install memcached-3.0.2 \ + && pecl install redis-3.1.1 \ + && docker-php-ext-enable apcu redis memcached +RUN a2enmod rewrite + +CMD ["apache2-foreground"] diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh new file mode 100755 index 00000000000..a30d9fd8f3a --- /dev/null +++ b/build/acceptance/run.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash + +# @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com) +# +# @license GNU AGPL version 3 or any later version +# +# This program 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 (at your option) 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 . + +# Helper script to run the acceptance tests, which test a running Nextcloud +# instance from the point of view of a real user. +# +# The acceptance tests are written in Behat so, besides running the tests, this +# script installs Behat, its dependencies, and some related packages in the +# "vendor" subdirectory of the acceptance tests. The acceptance tests also use +# the Selenium server to control a web browser, so the Selenium server is also +# installed to the "selenium" subdirectory and launched before the tests start +# (it will be stopped automatically once the tests end). Finally, the tests +# expect that a Docker image with the Nextcloud installation to be tested is +# available, so the script creates it based on the Nextcloud code from the +# grandparent directory. +# +# To perform its job, the script requires the "composer", "java" and "docker" +# commands to be available. +# +# The Docker Command Line Interface (the "docker" command) requires special +# permissions to talk to the Docker daemon, and those permissions are typically +# available only to the root user. However, you should NOT run this script as +# root, but as a regular user instead. Please see the Docker documentation to +# find out how to give access to a regular user to the Docker daemon: +# https://docs.docker.com/engine/installation/linux/linux-postinstall/ +# +# Note, however, that being able to communicate with the Docker daemon is the +# same as being able to get root privileges for the system. Therefore, you must +# give access to the Docker daemon (and thus run this script as) ONLY to trusted +# and secure users: +# https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface +# +# Finally, take into account that this script will automatically remove the +# Docker containers named "nextcloud-local-test-acceptance" and +# "nextcloud-local-test-acceptance-[0-9a-f.]*" and the Docker image tagged as +# "nextcloud-local-test-acceptance:latest", even if the script did not create +# them (probably you will not have containers nor images with those names, but +# just in case). + +# Installs Behat and its dependencies. +# +# Behat and its dependencies will be installed in the "vendor" subdirectory of +# the directory of the script. +function prepareBehat() { + echo "Installing Behat and dependencies" + composer install +} + +# Launches the Selenium server, installing it if needed. +# +# The acceptance tests use Firefox by default but, unfortunately, Firefox >= 48 +# does not provide yet the same level of support as earlier versions for certain +# features related to automated testing. Therefore, if an incompatible version +# is found the script will be exited immediately with an error state. +# +# The Selenium server is installed in the "selenium" subdirectory of the +# directory of the script. +# +# The Selenium server launched here will be automatically stopped when the +# script exits (see cleanUp). If the Selenium server can not be started then the +# script will be exited immediately with an error state; the most common cause +# for the Selenium server to fail to start is that another server is already +# running in the default port. +# +# The output of the Selenium server will be saved to +# "selenium/selenium-server-{DATE}.log". +function prepareSelenium() { + FIREFOX_MAJOR_VERSION=$(firefox --version | sed -e "s/Mozilla Firefox \([0-9]\+\).*/\1/") + if [ "$FIREFOX_MAJOR_VERSION" -ge 48 ]; then + echo "The acceptance tests can not be run on Mozilla Firefox >= 48 (major version found was $FIREFOX_MAJOR_VERSION)" + exit 1 + fi + + SELENIUM_SERVER_STANDALONE="selenium-server-standalone-2.53.1.jar" + SELENIUM_SERVER_STANDALONE_URL="http://selenium-release.storage.googleapis.com/2.53/$SELENIUM_SERVER_STANDALONE" + + mkdir --parents selenium + + if [ ! -f "selenium/$SELENIUM_SERVER_STANDALONE" ]; then + echo "Installing Selenium server" + wget --output-document="selenium/$SELENIUM_SERVER_STANDALONE" "$SELENIUM_SERVER_STANDALONE_URL" + fi + + SELENIUM_SERVER_STANDALONE_LOG="selenium-server-$(date +%Y%m%d-%H%M%S).log" + + echo "Starting Selenium server" + # LANG=C forces "English" output for Selenium server to be able to look for + # the startup finished message (I do not really know if Selenium server log + # messages are localized or not, but just in case). + LANG=C java -jar "selenium/$SELENIUM_SERVER_STANDALONE" &>"selenium/$SELENIUM_SERVER_STANDALONE_LOG" & + SELENIUM_SERVER_STANDALONE_PID=$! + + echo -n "Waiting for Selenium server to be ready" + TIMEOUT=10 + TIMEOUT_STEP=1 + ELAPSED_TIME=0 + while [ $ELAPSED_TIME -lt $TIMEOUT ] && ! grep "Selenium Server is up and running" "selenium/$SELENIUM_SERVER_STANDALONE_LOG" &>/dev/null; do + sleep $TIMEOUT_STEP + echo -n "." + ELAPSED_TIME=$((ELAPSED_TIME+TIMEOUT_STEP)) + done + echo + + if [ "$ELAPSED_TIME" -eq "$TIMEOUT" ]; then + echo -n "Could not start Selenium server; see" \ + "$PWD/selenium/$SELENIUM_SERVER_STANDALONE_LOG" + + if grep "Address already in use" "selenium/$SELENIUM_SERVER_STANDALONE_LOG" &>/dev/null; then + echo " (probably another" \ + "Selenium server is already running)" + else + echo + fi + + exit 1 + fi +} + +# Creates a Docker image to be used in Behat by NextcloudTestServerContext based +# on the local Nextcloud directory. +# +# NextcloudTestServerContext creates and destroys a Docker container for each +# acceptance test run, and the image that the container is created from must +# provide an installed copy of Nextcloud with certain configuration (like an +# "admin" user with an "admin" password, or local data storage). This function +# creates that Docker image based on the Nextcloud code from the grandparent +# directory, although ignoring any configuration or data that it may provide +# (for example, if that directory was used directly to deploy a Nextcloud +# instance in a web server). As the Nextcloud code is copied to the image +# instead of referenced the original code can be modified while the acceptance +# tests are running without interfering in them. +# +# Besides the Docker image to be used by the acceptance tests, which is removed +# automatically when the script exits, this function creates another image, +# that the other one will be based on, which is not removed when the script +# exits. Building this parent image could be a slow process, so it is kept built +# instead of removing it every time to speed up the launch of the acceptance +# tests. +function prepareDocker() { + NEXTCLOUD_LOCAL_IMAGE=nextcloud-local-test-acceptance + NEXTCLOUD_LOCAL_CONTAINER=nextcloud-local-test-acceptance + + # To create the Docker image to be used by the acceptance tests first a + # parent image is created. This parent image provides a system in which a + # Nextcloud server could be installed. Then, that parent image is run in a + # container in which the relevant code from the grandparent directory is + # copied; once the code is copied, the Nextcloud server is installed and + # configured as needed inside the container. Finally, the image to be used + # by the acceptance tests is generated by persisting the container to a new + # image. + # + # The image to be used by the acceptance tests could have been created just + # with a Dockerfile by adding the relevant code to the build context before + # starting the build and then using the ADD command in the Dockerfile (plus + # running the commands to install and configure the server as needed). In + # fact, standard Docker practices favor the creation of images through + # Dockerfiles to get a reproducible build. However, in this case I felt that + # it would go against that reproducible spirit of Dockerfiles, as an + # additional .tar file would have to be explicitly created each time before + # building the image, and that file would probably be different between + # different builds, thus resulting in a different image each time. Therefore + # I think that the current approach is better suited for this scenario. + + echo "Building Docker parent image" + docker build --tag $NEXTCLOUD_LOCAL_IMAGE:parent - < docker/nextcloud-local-parent/Dockerfile + + docker run --detach --name=$NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_IMAGE:parent + + # Use the $TMPDIR or, if not set, fall back to /tmp. + NEXTCLOUD_LOCAL_TAR="$(mktemp --tmpdir="${TMPDIR:-/tmp}" --suffix=.tar nextcloud-local-XXXXXXXXXX)" + + # Setting the user and group of files in the tar would be superfluous, as + # "docker cp" does not take them into account (the extracted files are set + # to root). + echo "Copying local Git working directory of Nextcloud to the container" + tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./build" --exclude="./config/config.php" --exclude="./data" --exclude="./tests" --directory=../../ . + + docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/var/www/html/ < "$NEXTCLOUD_LOCAL_TAR" + docker exec $NEXTCLOUD_LOCAL_CONTAINER chown -R www-data:www-data /var/www/html/ + + echo "Installing Nextcloud in the container" + docker exec --user www-data $NEXTCLOUD_LOCAL_CONTAINER php occ maintenance:install --admin-pass=admin + docker exec --user www-data $NEXTCLOUD_LOCAL_CONTAINER bash -c "OC_PASS=123456 php occ user:add --password-from-env user0" + + echo "Creating Docker image to be used in acceptance tests" + docker commit --message "Nextcloud installed from the local Git working directory" $NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_IMAGE + + # Once the image to be used by the acceptance tests is created the container + # is no longer needed, so it can be stopped and removed. + docker stop $NEXTCLOUD_LOCAL_CONTAINER + # Although the parent Nextcloud image does not define a volume "--volumes" + # is used anyway just in case any of its ancestor images does. + docker rm --volumes $NEXTCLOUD_LOCAL_CONTAINER +} + +# Removes/stops temporal elements created/started by this script. +function cleanUp() { + # Disable (yes, "+" disables) exiting immediately on errors to ensure that + # all the cleanup commands are executed (well, no errors should occur during + # the cleanup anyway, but just in case). + set +o errexit + + echo "Cleaning up" + + if [ -f "$NEXTCLOUD_LOCAL_TAR" ]; then + echo "Removing $NEXTCLOUD_LOCAL_TAR" + rm $NEXTCLOUD_LOCAL_TAR + fi + + # If the script run successfully the container should have already been + # removed; this is needed only when an error happened. + # The name filter must be specified as "^/XXX$" to get an exact match; using + # just "XXX" would match every name that contained "XXX". + if [ -n "$(docker ps --all --quiet --filter name="^/$NEXTCLOUD_LOCAL_CONTAINER$")" ]; then + echo "Removing Docker container $NEXTCLOUD_LOCAL_CONTAINER" + docker rm --volumes --force $NEXTCLOUD_LOCAL_CONTAINER + fi + + # In case of failure (like calling a method that does not exist on an + # object) the tests would be aborted without removing the containers created + # by NextcloudTestServerContext; if that happens those dangling containers + # are removed here. + DANGLING_CONTAINERS_CREATED_BY_ACCEPTANCE_TESTS="$(docker ps --all --quiet --filter name="^/$NEXTCLOUD_LOCAL_CONTAINER-[0-9a-f.]*$" --filter ancestor="$NEXTCLOUD_LOCAL_IMAGE:parent")" + if [ -n "$DANGLING_CONTAINERS_CREATED_BY_ACCEPTANCE_TESTS" ]; then + echo "Removing Docker containers matching $NEXTCLOUD_LOCAL_CONTAINER-[0-9a-f.]*" + docker rm --volumes --force $DANGLING_CONTAINERS_CREATED_BY_ACCEPTANCE_TESTS + fi + + if [ -n "$(docker images --quiet $NEXTCLOUD_LOCAL_IMAGE:latest)" ]; then + echo "Removing Docker image $NEXTCLOUD_LOCAL_IMAGE:latest" + docker rmi $NEXTCLOUD_LOCAL_IMAGE:latest + fi + + if [ -n "$SELENIUM_SERVER_STANDALONE_PID" ]; then + echo "Stopping Selenium server (PID $SELENIUM_SERVER_STANDALONE_PID)" + kill $SELENIUM_SERVER_STANDALONE_PID + fi +} + +# Exit immediately on errors. +set -o errexit + +# Execute cleanUp when the script exits, either normally or due to an error. +trap cleanUp EXIT + +# Ensure working directory is script directory, as some actions (like installing +# Behat through Composer or generating the Nextcloud image for Docker) expect +# that. +cd "$(dirname $0)" + +prepareBehat +prepareSelenium +prepareDocker + +echo "Running all tests" +vendor/bin/behat From 1203369ea6a6bb1a9afb03fd390f1c6342f54aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 17:20:19 +0200 Subject: [PATCH 06/31] Add acceptance tests related to login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/config/behat.yml | 9 ++ .../features/bootstrap/FeatureContext.php | 7 + .../features/bootstrap/FilesAppContext.php | 39 +++++ .../features/bootstrap/LoginPageContext.php | 137 ++++++++++++++++++ .../bootstrap/NotificationContext.php | 54 +++++++ .../bootstrap/SettingsMenuContext.php | 76 ++++++++++ .../bootstrap/UsersSettingsContext.php | 102 +++++++++++++ build/acceptance/features/login.feature | 47 ++++++ 8 files changed, 471 insertions(+) create mode 100644 build/acceptance/features/bootstrap/FilesAppContext.php create mode 100644 build/acceptance/features/bootstrap/LoginPageContext.php create mode 100644 build/acceptance/features/bootstrap/NotificationContext.php create mode 100644 build/acceptance/features/bootstrap/SettingsMenuContext.php create mode 100644 build/acceptance/features/bootstrap/UsersSettingsContext.php create mode 100644 build/acceptance/features/login.feature diff --git a/build/acceptance/config/behat.yml b/build/acceptance/config/behat.yml index 186a57c2023..6c3d9e4a7b9 100644 --- a/build/acceptance/config/behat.yml +++ b/build/acceptance/config/behat.yml @@ -10,8 +10,17 @@ default: - NextcloudTestServerContext - FeatureContext + - FilesAppContext + - LoginPageContext + - NotificationContext + - SettingsMenuContext + - UsersSettingsContext extensions: Behat\MinkExtension: sessions: default: selenium2: ~ + John: + selenium2: ~ + Jane: + selenium2: ~ diff --git a/build/acceptance/features/bootstrap/FeatureContext.php b/build/acceptance/features/bootstrap/FeatureContext.php index 0598cbae959..a125ea01ccc 100644 --- a/build/acceptance/features/bootstrap/FeatureContext.php +++ b/build/acceptance/features/bootstrap/FeatureContext.php @@ -27,4 +27,11 @@ class FeatureContext implements Context, ActorAwareInterface { use ActorAware; + /** + * @When I visit the Home page + */ + public function iVisitTheHomePage() { + $this->actor->getSession()->visit($this->actor->locatePath("/")); + } + } diff --git a/build/acceptance/features/bootstrap/FilesAppContext.php b/build/acceptance/features/bootstrap/FilesAppContext.php new file mode 100644 index 00000000000..9702e64b552 --- /dev/null +++ b/build/acceptance/features/bootstrap/FilesAppContext.php @@ -0,0 +1,39 @@ +. + * + */ + +use Behat\Behat\Context\Context; + +class FilesAppContext implements Context, ActorAwareInterface { + + use ActorAware; + + /** + * @Then I see that the current page is the Files app + */ + public function iSeeThatTheCurrentPageIsTheFilesApp() { + PHPUnit_Framework_Assert::assertStringStartsWith( + $this->actor->locatePath("/apps/files/"), + $this->actor->getSession()->getCurrentUrl()); + } + +} diff --git a/build/acceptance/features/bootstrap/LoginPageContext.php b/build/acceptance/features/bootstrap/LoginPageContext.php new file mode 100644 index 00000000000..55db8c33bb7 --- /dev/null +++ b/build/acceptance/features/bootstrap/LoginPageContext.php @@ -0,0 +1,137 @@ +. + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; + +class LoginPageContext implements Context, ActorAwareInterface { + + use ActorAware; + + /** + * @var FeatureContext + */ + private $featureContext; + + /** + * @var FilesAppContext + */ + private $filesAppContext; + + /** + * @return Locator + */ + public static function userNameField() { + return Locator::forThe()->field("user")-> + describedAs("User name field in Login page"); + } + + /** + * @return Locator + */ + public static function passwordField() { + return Locator::forThe()->field("password")-> + describedAs("Password field in Login page"); + } + + /** + * @return Locator + */ + public static function loginButton() { + return Locator::forThe()->id("submit")-> + describedAs("Login button in Login page"); + } + + /** + * @return Locator + */ + public static function wrongPasswordMessage() { + return Locator::forThe()->content("Wrong password. Reset it?")-> + describedAs("Wrong password message in Login page"); + } + + /** + * @When I log in with user :user and password :password + */ + public function iLogInWithUserAndPassword($user, $password) { + $this->actor->find(self::userNameField(), 10)->setValue($user); + $this->actor->find(self::passwordField())->setValue($password); + $this->actor->find(self::loginButton())->click(); + } + + /** + * @Then I see that the current page is the Login page + */ + public function iSeeThatTheCurrentPageIsTheLoginPage() { + PHPUnit_Framework_Assert::assertStringStartsWith( + $this->actor->locatePath("/login"), + $this->actor->getSession()->getCurrentUrl()); + } + + /** + * @Then I see that a wrong password message is shown + */ + public function iSeeThatAWrongPasswordMessageIsShown() { + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::wrongPasswordMessage(), 10)->isVisible()); + } + + /** + * @BeforeScenario + */ + public function getOtherRequiredSiblingContexts(BeforeScenarioScope $scope) { + $environment = $scope->getEnvironment(); + + $this->featureContext = $environment->getContext("FeatureContext"); + $this->filesAppContext = $environment->getContext("FilesAppContext"); + } + + /** + * @Given I am logged in + */ + public function iAmLoggedIn() { + $this->featureContext->iVisitTheHomePage(); + $this->iLogInWithUserAndPassword("user0", "123456"); + $this->filesAppContext->iSeeThatTheCurrentPageIsTheFilesApp(); + } + + /** + * @Given I am logged in as the admin + */ + public function iAmLoggedInAsTheAdmin() { + $this->featureContext->iVisitTheHomePage(); + $this->iLogInWithUserAndPassword("admin", "admin"); + $this->filesAppContext->iSeeThatTheCurrentPageIsTheFilesApp(); + } + + /** + * @Given I can not log in with user :user and password :password + */ + public function iCanNotLogInWithUserAndPassword($user, $password) { + $this->featureContext->iVisitTheHomePage(); + $this->iLogInWithUserAndPassword($user, $password); + $this->iSeeThatTheCurrentPageIsTheLoginPage(); + $this->iSeeThatAWrongPasswordMessageIsShown(); + } + +} diff --git a/build/acceptance/features/bootstrap/NotificationContext.php b/build/acceptance/features/bootstrap/NotificationContext.php new file mode 100644 index 00000000000..f8b784e2465 --- /dev/null +++ b/build/acceptance/features/bootstrap/NotificationContext.php @@ -0,0 +1,54 @@ +. + * + */ + +use Behat\Behat\Context\Context; + +class NotificationContext implements Context, ActorAwareInterface { + + use ActorAware; + + /** + * @return Locator + */ + public static function notificationMessage($message) { + return Locator::forThe()->content($message)->descendantOf(self::notificationContainer())-> + describedAs("$message notification"); + } + + /** + * @return Locator + */ + private static function notificationContainer() { + return Locator::forThe()->id("notification-container")-> + describedAs("Notification container"); + } + + /** + * @Then I see that the :message notification is shown + */ + public function iSeeThatTheNotificationIsShown($message) { + PHPUnit_Framework_Assert::assertTrue($this->actor->find( + self::notificationMessage($message), 10)->isVisible()); + } + +} diff --git a/build/acceptance/features/bootstrap/SettingsMenuContext.php b/build/acceptance/features/bootstrap/SettingsMenuContext.php new file mode 100644 index 00000000000..78a29b08a10 --- /dev/null +++ b/build/acceptance/features/bootstrap/SettingsMenuContext.php @@ -0,0 +1,76 @@ +. + * + */ + +use Behat\Behat\Context\Context; + +class SettingsMenuContext implements Context, ActorAwareInterface { + + use ActorAware; + + /** + * @return Locator + */ + public static function settingsMenuButton() { + return Locator::forThe()->xpath("//*[@id = 'header']//*[@id = 'settings']")-> + describedAs("Settings menu button"); + } + + /** + * @return Locator + */ + public static function usersMenuItem() { + return self::menuItemFor("Users"); + } + + /** + * @return Locator + */ + public static function logOutMenuItem() { + return self::menuItemFor("Log out"); + } + + /** + * @return Locator + */ + private static function menuItemFor($itemText) { + return Locator::forThe()->content($itemText)->descendantOf(self::settingsMenuButton())-> + describedAs($itemText . " item in Settings menu"); + } + + /** + * @When I open the User settings + */ + public function iOpenTheUserSettings() { + $this->actor->find(self::settingsMenuButton(), 10)->click(); + $this->actor->find(self::usersMenuItem(), 2)->click(); + } + + /** + * @When I log out + */ + public function iLogOut() { + $this->actor->find(self::settingsMenuButton(), 10)->click(); + $this->actor->find(self::logOutMenuItem(), 2)->click(); + } + +} diff --git a/build/acceptance/features/bootstrap/UsersSettingsContext.php b/build/acceptance/features/bootstrap/UsersSettingsContext.php new file mode 100644 index 00000000000..93ab7246eb6 --- /dev/null +++ b/build/acceptance/features/bootstrap/UsersSettingsContext.php @@ -0,0 +1,102 @@ +. + * + */ + +use Behat\Behat\Context\Context; + +class UsersSettingsContext implements Context, ActorAwareInterface { + + use ActorAware; + + /** + * @return Locator + */ + public static function userNameFieldForNewUser() { + return Locator::forThe()->field("newusername")-> + describedAs("User name field for new user in Users Settings"); + } + + /** + * @return Locator + */ + public static function passwordFieldForNewUser() { + return Locator::forThe()->field("newuserpassword")-> + describedAs("Password field for new user in Users Settings"); + } + + /** + * @return Locator + */ + public static function createNewUserButton() { + return Locator::forThe()->xpath("//form[@id = 'newuser']//input[@type = 'submit']")-> + describedAs("Create user button in Users Settings"); + } + + /** + * @return Locator + */ + public static function rowForUser($user) { + return Locator::forThe()->xpath("//table[@id = 'userlist']//th[normalize-space() = '$user']/..")-> + describedAs("Row for user $user in Users Settings"); + } + + /** + * @return Locator + */ + public static function passwordCellForUser($user) { + return Locator::forThe()->css(".password")->descendantOf(self::rowForUser($user))-> + describedAs("Password cell for user $user in Users Settings"); + } + + /** + * @return Locator + */ + public static function passwordInputForUser($user) { + return Locator::forThe()->css("input")->descendantOf(self::passwordCellForUser($user))-> + describedAs("Password input for user $user in Users Settings"); + } + + /** + * @When I create user :user with password :password + */ + public function iCreateUserWithPassword($user, $password) { + $this->actor->find(self::userNameFieldForNewUser(), 10)->setValue($user); + $this->actor->find(self::passwordFieldForNewUser())->setValue($password); + $this->actor->find(self::createNewUserButton())->click(); + } + + /** + * @When I set the password for :user to :password + */ + public function iSetThePasswordForUserTo($user, $password) { + $this->actor->find(self::passwordCellForUser($user), 10)->click(); + $this->actor->find(self::passwordInputForUser($user), 2)->setValue($password . "\r"); + } + + /** + * @Then I see that the list of users contains the user :user + */ + public function iSeeThatTheListOfUsersContainsTheUser($user) { + PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::rowForUser($user), 10)); + } + +} diff --git a/build/acceptance/features/login.feature b/build/acceptance/features/login.feature new file mode 100644 index 00000000000..c4cd2add8e6 --- /dev/null +++ b/build/acceptance/features/login.feature @@ -0,0 +1,47 @@ +Feature: login + + Scenario: log in with valid user and password + Given I visit the Home page + When I log in with user user0 and password 123456 + Then I see that the current page is the Files app + + Scenario: try to log in with valid user and invalid password + Given I visit the Home page + When I log in with user user0 and password 654321 + Then I see that the current page is the Login page + And I see that a wrong password message is shown + + Scenario: log in with valid user and invalid password once fixed by admin + Given I act as John + And I can not log in with user user0 and password 654231 + When I act as Jane + And I am logged in as the admin + And I open the User settings + And I set the password for user0 to 654321 + And I see that the "Password successfully changed" notification is shown + And I act as John + And I log in with user user0 and password 654321 + Then I see that the current page is the Files app + + Scenario: try to log in with invalid user + Given I visit the Home page + When I log in with user unknownUser and password 123456 + Then I see that the current page is the Login page + And I see that a wrong password message is shown + + Scenario: log in with invalid user once fixed by admin + Given I act as John + And I can not log in with user unknownUser and password 123456 + When I act as Jane + And I am logged in as the admin + And I open the User settings + And I create user unknownUser with password 123456 + And I see that the list of users contains the user unknownUser + And I act as John + And I log in with user unknownUser and password 123456 + Then I see that the current page is the Files app + + Scenario: log out + Given I am logged in + When I log out + Then I see that the current page is the Login page From c4613733eb4076f6e641e27e953365feb88e98ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 4 Apr 2017 17:24:30 +0200 Subject: [PATCH 07/31] Add acceptance tests related to access levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../acceptance/features/access-levels.feature | 21 ++++++++ .../bootstrap/SettingsMenuContext.php | 52 +++++++++++++++++-- 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 build/acceptance/features/access-levels.feature diff --git a/build/acceptance/features/access-levels.feature b/build/acceptance/features/access-levels.feature new file mode 100644 index 00000000000..57998899a57 --- /dev/null +++ b/build/acceptance/features/access-levels.feature @@ -0,0 +1,21 @@ +Feature: access-levels + + Scenario: regular users can not see admin-level items in the Settings menu + Given I am logged in + When I open the Settings menu + Then I see that the Settings menu is shown + And I see that the "Personal" item in the Settings menu is shown + And I see that the "Admin" item in the Settings menu is not shown + And I see that the "Users" item in the Settings menu is not shown + And I see that the "Help" item in the Settings menu is shown + And I see that the "Log out" item in the Settings menu is shown + + Scenario: admin users can see admin-level items in the Settings menu + Given I am logged in as the admin + When I open the Settings menu + Then I see that the Settings menu is shown + And I see that the "Personal" item in the Settings menu is shown + And I see that the "Admin" item in the Settings menu is shown + And I see that the "Users" item in the Settings menu is shown + And I see that the "Help" item in the Settings menu is shown + And I see that the "Log out" item in the Settings menu is shown diff --git a/build/acceptance/features/bootstrap/SettingsMenuContext.php b/build/acceptance/features/bootstrap/SettingsMenuContext.php index 78a29b08a10..9ce8df4caef 100644 --- a/build/acceptance/features/bootstrap/SettingsMenuContext.php +++ b/build/acceptance/features/bootstrap/SettingsMenuContext.php @@ -35,6 +35,14 @@ class SettingsMenuContext implements Context, ActorAwareInterface { describedAs("Settings menu button"); } + /** + * @return Locator + */ + public static function settingsMenu() { + return Locator::forThe()->id("expanddiv")->descendantOf(self::settingsMenuButton())-> + describedAs("Settings menu"); + } + /** * @return Locator */ @@ -53,15 +61,23 @@ class SettingsMenuContext implements Context, ActorAwareInterface { * @return Locator */ private static function menuItemFor($itemText) { - return Locator::forThe()->content($itemText)->descendantOf(self::settingsMenuButton())-> + return Locator::forThe()->content($itemText)->descendantOf(self::settingsMenu())-> describedAs($itemText . " item in Settings menu"); } + /** + * @When I open the Settings menu + */ + public function iOpenTheSettingsMenu() { + $this->actor->find(self::settingsMenuButton(), 10)->click(); + } + /** * @When I open the User settings */ public function iOpenTheUserSettings() { - $this->actor->find(self::settingsMenuButton(), 10)->click(); + $this->iOpenTheSettingsMenu(); + $this->actor->find(self::usersMenuItem(), 2)->click(); } @@ -69,8 +85,38 @@ class SettingsMenuContext implements Context, ActorAwareInterface { * @When I log out */ public function iLogOut() { - $this->actor->find(self::settingsMenuButton(), 10)->click(); + $this->iOpenTheSettingsMenu(); + $this->actor->find(self::logOutMenuItem(), 2)->click(); } + /** + * @Then I see that the Settings menu is shown + */ + public function iSeeThatTheSettingsMenuIsShown() { + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::settingsMenu(), 10)->isVisible()); + } + + /** + * @Then I see that the :itemText item in the Settings menu is shown + */ + public function iSeeThatTheItemInTheSettingsMenuIsShown($itemText) { + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::menuItemFor($itemText), 10)->isVisible()); + } + + /** + * @Then I see that the :itemText item in the Settings menu is not shown + */ + public function iSeeThatTheItemInTheSettingsMenuIsNotShown($itemText) { + $this->iSeeThatTheSettingsMenuIsShown(); + + try { + PHPUnit_Framework_Assert::assertFalse( + $this->actor->find(self::menuItemFor($itemText))->isVisible()); + } catch (NoSuchElementException $exception) { + } + } + } From 38efa97aa596ff7ba9185bdd9fbea7ab32acd236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 12:26:52 +0200 Subject: [PATCH 08/31] Rename methods to something less tied to its implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/features/core/NextcloudTestServerContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/build/acceptance/features/core/NextcloudTestServerContext.php index cdd07dab168..7b494cdd12b 100644 --- a/build/acceptance/features/core/NextcloudTestServerContext.php +++ b/build/acceptance/features/core/NextcloudTestServerContext.php @@ -87,7 +87,7 @@ class NextcloudTestServerContext implements Context { * BeforeScenario hook scope. * @throws \Exception if the Docker container can not be started. */ - public function startNextcloudTestServer(BeforeScenarioScope $scope) { + public function setUpNextcloudTestServer(BeforeScenarioScope $scope) { $this->dockerHelper->createAndStartContainer(); $serverAddress = $this->dockerHelper->getNextcloudTestServerAddress(); @@ -116,7 +116,7 @@ class NextcloudTestServerContext implements Context { * * @throws \Exception if the Docker container can not be removed. */ - public function stopNextcloudTestServer() { + public function cleanUpNextcloudTestServer() { $this->dockerHelper->stopAndRemoveContainer(); $wasContainerRemovedCallback = function() { From dead90f1cf1992842b094359fbddcd84894db414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 18:59:31 +0200 Subject: [PATCH 09/31] Move all Docker-related logic to NextcloudTestServerDockerHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerContext.php | 40 +--------- .../core/NextcloudTestServerDockerHelper.php | 79 ++++++++++++++++++- 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/build/acceptance/features/core/NextcloudTestServerContext.php index 7b494cdd12b..600c78fd784 100644 --- a/build/acceptance/features/core/NextcloudTestServerContext.php +++ b/build/acceptance/features/core/NextcloudTestServerContext.php @@ -88,20 +88,9 @@ class NextcloudTestServerContext implements Context { * @throws \Exception if the Docker container can not be started. */ public function setUpNextcloudTestServer(BeforeScenarioScope $scope) { - $this->dockerHelper->createAndStartContainer(); + $this->dockerHelper->setUp(); - $serverAddress = $this->dockerHelper->getNextcloudTestServerAddress(); - - $isServerReadyCallback = function() use ($serverAddress) { - return $this->isServerReady($serverAddress); - }; - $timeout = 10; - $timeoutStep = 0.5; - if (!Utils::waitFor($isServerReadyCallback, $timeout, $timeoutStep)) { - throw new Exception("Docker container for Nextcloud could not be started"); - } - - $this->setBaseUrlInSiblingRawMinkContexts($scope, "http://" . $serverAddress . "/index.php"); + $this->setBaseUrlInSiblingRawMinkContexts($scope, $this->dockerHelper->getBaseUrl()); } /** @@ -117,30 +106,7 @@ class NextcloudTestServerContext implements Context { * @throws \Exception if the Docker container can not be removed. */ public function cleanUpNextcloudTestServer() { - $this->dockerHelper->stopAndRemoveContainer(); - - $wasContainerRemovedCallback = function() { - return !$this->dockerHelper->isContainerRegistered(); - }; - $timeout = 10; - $timeoutStep = 0.5; - if (!Utils::waitFor($wasContainerRemovedCallback, $timeout, $timeoutStep)) { - throw new Exception("Docker container for Nextcloud (" . $this->dockerHelper->getContainerName() . ") could not be removed"); - } - } - - private function isServerReady($serverAddress) { - $curlHandle = curl_init("http://" . $serverAddress); - - // Returning the transfer as the result of curl_exec prevents the - // transfer from being written to the output. - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); - - $transfer = curl_exec($curlHandle); - - curl_close($curlHandle); - - return $transfer !== false; + $this->dockerHelper->cleanUp(); } private function setBaseUrlInSiblingRawMinkContexts(BeforeScenarioScope $scope, $baseUrl) { diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index 7ed159ead7b..b7b2bcc2544 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -39,9 +39,8 @@ * accessed through a local port in the host system mapped to the port 80 of the * Docker container; if the Nextcloud server was instead accessed directly * through its IP address it would complain that it was being accessed from an - * untrusted domain and refuse to work until the admin whitelisted it. The IP - * address and port to access the Nextcloud server can be got from - * "getNextcloudTestServerAddress". + * untrusted domain and refuse to work until the admin whitelisted it. The base + * URL to access the Nextcloud server can be got from "getBaseUrl". * * For better compatibility, Docker CLI commands used internally follow the * pre-1.13 syntax (also available in 1.13 and newer). For example, @@ -97,6 +96,32 @@ class NextcloudTestServerDockerHelper { $this->containerName = null; } + /** + * Sets up the Nextcloud test server. + * + * It starts the Docker container and waits for its Nextcloud test server to + * be started; if the server does not start after some time an exception is + * thrown (as it is just a warning for the test runner and nothing to be + * explicitly catched a plain base Exception is used). + * + * @throws \Exception if the Docker container or its Nextcloud test server + * can not be started. + */ + public function setUp() { + $this->createAndStartContainer(); + + $serverAddress = $this->getNextcloudTestServerAddress(); + + $isServerReadyCallback = function() use ($serverAddress) { + return $this->isServerReady($serverAddress); + }; + $timeout = 10; + $timeoutStep = 0.5; + if (!Utils::waitFor($isServerReadyCallback, $timeout, $timeoutStep)) { + throw new Exception("Docker container for Nextcloud (" . $this->containerName . ") or its Nextcloud test server could not be started"); + } + } + /** * Creates and starts the container. * @@ -118,6 +143,43 @@ class NextcloudTestServerDockerHelper { $this->executeDockerCommand("run --detach --user=www-data --publish 127.0.0.1:" . $this->hostPortRangeForContainer . ":80 --name=" . $this->containerName . " " . $this->imageName); } + private function isServerReady($serverAddress) { + $curlHandle = curl_init("http://" . $serverAddress); + + // Returning the transfer as the result of curl_exec prevents the + // transfer from being written to the output. + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + + $transfer = curl_exec($curlHandle); + + curl_close($curlHandle); + + return $transfer !== false; + } + + /** + * Cleans up the Nextcloud test server. + * + * It stops and removes the Docker container; if the Docker container can + * not be removed after some time an exception is thrown (as it is just a + * warning for the test runner and nothing to be explicitly catched a plain + * base Exception is used). + * + * @throws \Exception if the Docker container can not be removed. + */ + public function cleanUp() { + $this->stopAndRemoveContainer(); + + $wasContainerRemovedCallback = function() { + return !$this->isContainerRegistered(); + }; + $timeout = 10; + $timeoutStep = 0.5; + if (!Utils::waitFor($wasContainerRemovedCallback, $timeout, $timeoutStep)) { + throw new Exception("Docker container for Nextcloud (" . $this->containerName . ") could not be removed"); + } + } + /** * Stops and removes the container. * @@ -141,6 +203,17 @@ class NextcloudTestServerDockerHelper { return $this->containerName; } + /** + * Returns the base URL of the Nextcloud test server. + * + * @return string the base URL of the Nextcloud test server. + * @throws \Exception if the Docker command failed to execute or the + * container is not running. + */ + public function getBaseUrl() { + return "http://" . $this->getNextcloudTestServerAddress() . "/index.php"; + } + /** * Returns the IP address and port of the Nextcloud test server (which is * mapped to a local port in the host). From 8170b995613482f4b31e53c9f1117f969f5a1c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 19:06:58 +0200 Subject: [PATCH 10/31] Remove no longer needed methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerDockerHelper.php | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index b7b2bcc2544..bfa5f695fdb 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -191,18 +191,6 @@ class NextcloudTestServerDockerHelper { $this->executeDockerCommand("rm --volumes --force " . $this->containerName); } - /** - * Returns the container name. - * - * If the container has not been created yet the container name will be - * null. - * - * @return string the container name. - */ - public function getContainerName() { - return $this->containerName; - } - /** * Returns the base URL of the Nextcloud test server. * @@ -226,22 +214,6 @@ class NextcloudTestServerDockerHelper { return $this->executeDockerCommand("port " . $this->containerName . " 80"); } - /** - * Returns whether the container is running or not. - * - * @return boolean true if the container is running, false otherwise. - * @throws \Exception if the Docker command failed to execute. - */ - public function isContainerRunning() { - // By default, "docker ps" only shows running containers, and the - // "--quiet" option only shows the ID of the matching containers, - // without table headers. Therefore, if the container is not running the - // output will be empty (not even a new line, as the last line of output - // returned by "executeDockerCommand" does not include a trailing new - // line character). - return $this->executeDockerCommand("ps --quiet --filter 'name=" . $this->containerName . "'") !== ""; - } - /** * Returns whether the container exists (no matter its state) or not. * From 03233b1d58a3ff100196393321aa7cff9727dd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 19:08:30 +0200 Subject: [PATCH 11/31] Hide methods not needed outside the class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../features/core/NextcloudTestServerDockerHelper.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index bfa5f695fdb..71c7f9ad850 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -130,7 +130,7 @@ class NextcloudTestServerDockerHelper { * * @throws \Exception if the Docker command failed to execute. */ - public function createAndStartContainer() { + private function createAndStartContainer() { $moreEntropy = true; $this->containerName = uniqid($this->imageName . "-", $moreEntropy); @@ -185,7 +185,7 @@ class NextcloudTestServerDockerHelper { * * @throws \Exception if the Docker command failed to execute. */ - public function stopAndRemoveContainer() { + private function stopAndRemoveContainer() { // Although the Nextcloud image does not define a volume "--volumes" is // used anyway just in case any of its ancestor images does. $this->executeDockerCommand("rm --volumes --force " . $this->containerName); @@ -210,7 +210,7 @@ class NextcloudTestServerDockerHelper { * @throws \Exception if the Docker command failed to execute or the * container is not running. */ - public function getNextcloudTestServerAddress() { + private function getNextcloudTestServerAddress() { return $this->executeDockerCommand("port " . $this->containerName . " 80"); } @@ -220,7 +220,7 @@ class NextcloudTestServerDockerHelper { * @return boolean true if the container exists, false otherwise. * @throws \Exception if the Docker command failed to execute. */ - public function isContainerRegistered() { + private function isContainerRegistered() { // With the "--quiet" option "docker ps" only shows the ID of the // matching containers, without table headers. Therefore, if the // container does not exist the output will be empty (not even a new From 4d71d37fe353c8870b6f4f50e21b37dbe0ae864c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 19:10:03 +0200 Subject: [PATCH 12/31] Reorganize method position inside class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For consistency with the rest of private methods in the class, "isContainerRegistered" is moved below the only public method in which it is used ("cleanUp"). Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerDockerHelper.php | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index 71c7f9ad850..69dafc2276b 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -191,6 +191,21 @@ class NextcloudTestServerDockerHelper { $this->executeDockerCommand("rm --volumes --force " . $this->containerName); } + /** + * Returns whether the container exists (no matter its state) or not. + * + * @return boolean true if the container exists, false otherwise. + * @throws \Exception if the Docker command failed to execute. + */ + private function isContainerRegistered() { + // With the "--quiet" option "docker ps" only shows the ID of the + // matching containers, without table headers. Therefore, if the + // container does not exist the output will be empty (not even a new + // line, as the last line of output returned by "executeDockerCommand" + // does not include a trailing new line character). + return $this->executeDockerCommand("ps --all --quiet --filter 'name=" . $this->containerName . "'") !== ""; + } + /** * Returns the base URL of the Nextcloud test server. * @@ -214,21 +229,6 @@ class NextcloudTestServerDockerHelper { return $this->executeDockerCommand("port " . $this->containerName . " 80"); } - /** - * Returns whether the container exists (no matter its state) or not. - * - * @return boolean true if the container exists, false otherwise. - * @throws \Exception if the Docker command failed to execute. - */ - private function isContainerRegistered() { - // With the "--quiet" option "docker ps" only shows the ID of the - // matching containers, without table headers. Therefore, if the - // container does not exist the output will be empty (not even a new - // line, as the last line of output returned by "executeDockerCommand" - // does not include a trailing new line character). - return $this->executeDockerCommand("ps --all --quiet --filter 'name=" . $this->containerName . "'") !== ""; - } - /** * Executes the given Docker command. * From f10156f00953b4b207cac631d4c6f5c1610a0981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 19:03:40 +0200 Subject: [PATCH 13/31] Extract NextcloudTestServerHelper interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NextcloudTestServerHelper interface provides the needed methods to manage the Nextcloud server used in acceptance tests. Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerDockerHelper.php | 23 +++--- .../core/NextcloudTestServerHelper.php | 73 +++++++++++++++++++ 2 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 build/acceptance/features/core/NextcloudTestServerHelper.php diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index 69dafc2276b..5e7c0b2df76 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -24,14 +24,13 @@ /** * Helper to manage the Docker container for the Nextcloud test server. * - * The NextcloudTestServerDockerHelper abstracts the calls to the Docker Command - * Line Interface (the "docker" command) to run, get information from, and - * destroy containers. It is not a generic abstraction, but one tailored - * specifically to the Nextcloud test server; a Docker image that provides an - * installed and ready to run Nextcloud server with the configuration and data - * expected by the acceptance tests must be available in the system. The - * Nextcloud server must use a local storage so all the changes it makes are - * confined to its running container. + * The NextcloudTestServerDockerHelper provides a Nextcloud test server using a + * Docker container. The "setUp" method creates and starts the container, while + * the "cleanUp" method destroys it. A Docker image that provides an installed + * and ready to run Nextcloud server with the configuration and data expected by + * the acceptance tests must be available in the system. The Nextcloud server + * must use a local storage so all the changes it makes are confined to its + * running container. * * Also, the Nextcloud server installed in the Docker image is expected to see * "127.0.0.1" as a trusted domain (which would be the case if it was installed @@ -42,8 +41,10 @@ * untrusted domain and refuse to work until the admin whitelisted it. The base * URL to access the Nextcloud server can be got from "getBaseUrl". * - * For better compatibility, Docker CLI commands used internally follow the - * pre-1.13 syntax (also available in 1.13 and newer). For example, + * Internally, the NextcloudTestServerDockerHelper uses the Docker Command Line + * Interface (the "docker" command) to run, get information from, and destroy + * the container, For better compatibility, the used Docker CLI commands follow + * the pre-1.13 syntax (also available in 1.13 and newer). For example, * "docker start" instead of "docker container start". * * In any case, the "docker" command requires special permissions to talk to the @@ -65,7 +66,7 @@ * just a warning for the test runner and nothing to be explicitly catched a * plain base Exception is used. */ -class NextcloudTestServerDockerHelper { +class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { /** * @var string diff --git a/build/acceptance/features/core/NextcloudTestServerHelper.php b/build/acceptance/features/core/NextcloudTestServerHelper.php new file mode 100644 index 00000000000..198d78e3fcb --- /dev/null +++ b/build/acceptance/features/core/NextcloudTestServerHelper.php @@ -0,0 +1,73 @@ +. + * + */ + +/** + * Interface for classes that manage a Nextcloud server during acceptance tests. + * + * A NextcloudTestServerHelper takes care of setting up a Nextcloud server to be + * used in acceptance tests through its "setUp" method. It does not matter + * wheter the server is a fresh new server just started or an already running + * server; in any case, the state of the server must comply with the initial + * state expected by the tests (like having performed the Nextcloud installation + * or having an admin user with certain password). + * + * As the IP address and thus its the base URL of the server is not known + * beforehand, the NextcloudTestServerHelper must provide it through its + * "getBaseUrl" method. Note that this must be the base URL from the point of + * view of the Selenium server, which may be a different value than the base URL + * from the point of view of the acceptance tests themselves. + * + * Once the Nextcloud test server is no longer needed the "cleanUp" method will + * be called; depending on how the Nextcloud test server was set up it may not + * need to do anything. + * + * All the methods throw an exception if they fail to execute; as, due to the + * current use of this interface, it is just a warning for the test runner and + * nothing to be explicitly catched a plain base Exception is used. + */ +interface NextcloudTestServerHelper { + + /** + * Sets up the Nextcloud test server. + * + * @throws \Exception if the Nextcloud test server can not be set up. + */ + public function setUp(); + + /** + * Cleans up the Nextcloud test server. + * + * @throws \Exception if the Nextcloud test server can not be cleaned up. + */ + public function cleanUp(); + + /** + * Returns the base URL of the Nextcloud test server (from the point of view + * of the Selenium server). + * + * @return string the base URL of the Nextcloud test server. + * @throws \Exception if the base URL can not be determined. + */ + public function getBaseUrl(); + +} From 7de82615ff495e7298f1c1dc4da31b7ebc965da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 10 Apr 2017 20:56:47 +0200 Subject: [PATCH 14/31] Use NextcloudTestServerHelper in NextcloudTestServerContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of depending on a Nextcloud test server created through Docker, NextcloudTestServerContext now uses the NextcloudTestServerHelper interface. This makes possible to provide other implementations of the interface for those cases in which using a Docker container is not a valid approach, like in the continuous integration system of the public repository due to security concerns. Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerContext.php | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/build/acceptance/features/core/NextcloudTestServerContext.php index 600c78fd784..847038eac0c 100644 --- a/build/acceptance/features/core/NextcloudTestServerContext.php +++ b/build/acceptance/features/core/NextcloudTestServerContext.php @@ -38,13 +38,26 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; * RawMinkContexts; just add NextcloudTestServerContext to the context list of a * suite in "behat.yml". * - * The Nextcloud server is set up by running a new Docker container; the Docker - * image used by the container must provide a Nextcloud server ready to be used - * by the tests. By default, the image "nextcloud-local-test-acceptance" is - * used, although that can be customized using the "dockerImageName" parameter - * in "behat.yml". In the same way, the range of ports in which the Nextcloud - * server will be published in the local host (by default, "15000-16000") can be - * customized using the "hostPortRangeForContainer" parameter. + * The Nextcloud server is provided by an instance of NextcloudTestServerHelper; + * its class must be specified when this context is created. By default, + * "NextcloudTestServerDockerHelper" is used, although that can be customized + * using the "nextcloudTestServerHelper" parameter in "behat.yml". In the same + * way, the parameters to be passed to the helper when it is created can be + * customized using the "nextcloudTestServerHelperParameters" parameter, which + * is an array (without keys) with the value of the parameters in the same order + * as in the constructor of the helper class (by default, + * [ "nextcloud-local-test-acceptance", "15000-16000" ]). + * + * Example of custom parameters in "behat.yml": + * default: + * suites: + * default: + * contexts: + * - NextcloudTestServerContext: + * nextcloudTestServerHelper: NextcloudTestServerDockerHelper + * nextcloudTestServerHelperParameters: + * - nextcloud-local-test-acceptance-custom-image + * - 23000-42000 * * Note that using Docker containers as a regular user requires giving access to * the Docker daemon to that user. Unfortunately, that makes possible for that @@ -55,20 +68,27 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; class NextcloudTestServerContext implements Context { /** - * @var NextcloudTestServerDockerHelper + * @var NextcloudTestServerHelper */ - private $dockerHelper; + private $nextcloudTestServerHelper; /** * Creates a new NextcloudTestServerContext. * - * @param string $dockerImageName the name of the Docker image that provides - * the Nextcloud test server. - * @param string $hostPortRangeForContainer the range of local ports in the - * host in which the port 80 of the container can be published. + * @param string $nextcloudTestServerHelper the name of the + * NextcloudTestServerHelper implementing class to use. + * @param array $nextcloudTestServerHelperParameters the parameters for the + * constructor of the $nextcloudTestServerHelper class. */ - public function __construct($dockerImageName = "nextcloud-local-test-acceptance", $hostPortRangeForContainer = "15000-16000") { - $this->dockerHelper = new NextcloudTestServerDockerHelper($dockerImageName, $hostPortRangeForContainer); + public function __construct($nextcloudTestServerHelper = "NextcloudTestServerDockerHelper", + $nextcloudTestServerHelperParameters = [ "nextcloud-local-test-acceptance", "15000-16000" ]) { + $nextcloudTestServerHelperClass = new ReflectionClass($nextcloudTestServerHelper); + + if ($nextcloudTestServerHelperParameters === null) { + $nextcloudTestServerHelperParameters = array(); + } + + $this->nextcloudTestServerHelper = $nextcloudTestServerHelperClass->newInstanceArgs($nextcloudTestServerHelperParameters); } /** @@ -76,21 +96,19 @@ class NextcloudTestServerContext implements Context { * * Sets up the Nextcloud test server before each scenario. * - * It starts the Docker container and, once ready, it sets the "base_url" - * parameter of the sibling RawMinkContexts to "http://" followed by the IP - * address and port of the container; if the Docker container can not be - * started after some time an exception is thrown (as it is just a warning - * for the test runner and nothing to be explicitly catched a plain base - * Exception is used). + * Once the Nextcloud test server is set up, the "base_url" parameter of the + * sibling RawMinkContexts is set to the base URL of the Nextcloud test + * server. * * @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope the * BeforeScenario hook scope. - * @throws \Exception if the Docker container can not be started. + * @throws \Exception if the Nextcloud test server can not be set up or its + * base URL got. */ public function setUpNextcloudTestServer(BeforeScenarioScope $scope) { - $this->dockerHelper->setUp(); + $this->nextcloudTestServerHelper->setUp(); - $this->setBaseUrlInSiblingRawMinkContexts($scope, $this->dockerHelper->getBaseUrl()); + $this->setBaseUrlInSiblingRawMinkContexts($scope, $this->nextcloudTestServerHelper->getBaseUrl()); } /** @@ -98,15 +116,10 @@ class NextcloudTestServerContext implements Context { * * Cleans up the Nextcloud test server after each scenario. * - * It stops and removes the Docker container; if the Docker container can - * not be removed after some time an exception is thrown (as it is just a - * warning for the test runner and nothing to be explicitly catched a plain - * base Exception is used). - * - * @throws \Exception if the Docker container can not be removed. + * @throws \Exception if the Nextcloud test server can not be cleaned up. */ public function cleanUpNextcloudTestServer() { - $this->dockerHelper->cleanUp(); + $this->nextcloudTestServerHelper->cleanUp(); } private function setBaseUrlInSiblingRawMinkContexts(BeforeScenarioScope $scope, $baseUrl) { From 34510b73a254a0c7c87ac631cdf2aa06d57d0c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sat, 15 Apr 2017 13:28:59 +0200 Subject: [PATCH 15/31] Extract installation and configuration of the Nextcloud server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installation and configuration of the Nextcloud server as expected by the acceptance tests is extracted to its own script so it can be used from any element that launches the acceptance tests. Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/installAndConfigureServer.sh | 30 +++++++++++++++++++ build/acceptance/run.sh | 4 +-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100755 build/acceptance/installAndConfigureServer.sh diff --git a/build/acceptance/installAndConfigureServer.sh b/build/acceptance/installAndConfigureServer.sh new file mode 100755 index 00000000000..c41f03ece16 --- /dev/null +++ b/build/acceptance/installAndConfigureServer.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com) +# +# @license GNU AGPL version 3 or any later version +# +# This program 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 (at your option) 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 . + +# Helper script to install and configure the Nextcloud server as expected by the +# acceptance tests. +# +# This script is not meant to be called manually; it is called when needed by +# the acceptance tests launchers. + +set -o errexit + +php occ maintenance:install --admin-pass=admin + +OC_PASS=123456 php occ user:add --password-from-env user0 diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh index a30d9fd8f3a..96ed9c8db4f 100755 --- a/build/acceptance/run.sh +++ b/build/acceptance/run.sh @@ -190,13 +190,13 @@ function prepareDocker() { # to root). echo "Copying local Git working directory of Nextcloud to the container" tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./build" --exclude="./config/config.php" --exclude="./data" --exclude="./tests" --directory=../../ . + tar --append --file="$NEXTCLOUD_LOCAL_TAR" --directory=../../ build/acceptance/installAndConfigureServer.sh docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/var/www/html/ < "$NEXTCLOUD_LOCAL_TAR" docker exec $NEXTCLOUD_LOCAL_CONTAINER chown -R www-data:www-data /var/www/html/ echo "Installing Nextcloud in the container" - docker exec --user www-data $NEXTCLOUD_LOCAL_CONTAINER php occ maintenance:install --admin-pass=admin - docker exec --user www-data $NEXTCLOUD_LOCAL_CONTAINER bash -c "OC_PASS=123456 php occ user:add --password-from-env user0" + docker exec --user www-data $NEXTCLOUD_LOCAL_CONTAINER build/acceptance/installAndConfigureServer.sh echo "Creating Docker image to be used in acceptance tests" docker commit --message "Nextcloud installed from the local Git working directory" $NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_IMAGE From c452390d591b50f2dc603e8f077d7489d5f22cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sun, 16 Apr 2017 12:57:48 +0200 Subject: [PATCH 16/31] Extract waiting for the server to start to the Utils class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerDockerHelper.php | 22 +------------ build/acceptance/features/core/Utils.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index 5e7c0b2df76..120e13fe70c 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -111,14 +111,8 @@ class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { public function setUp() { $this->createAndStartContainer(); - $serverAddress = $this->getNextcloudTestServerAddress(); - - $isServerReadyCallback = function() use ($serverAddress) { - return $this->isServerReady($serverAddress); - }; $timeout = 10; - $timeoutStep = 0.5; - if (!Utils::waitFor($isServerReadyCallback, $timeout, $timeoutStep)) { + if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) { throw new Exception("Docker container for Nextcloud (" . $this->containerName . ") or its Nextcloud test server could not be started"); } } @@ -144,20 +138,6 @@ class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { $this->executeDockerCommand("run --detach --user=www-data --publish 127.0.0.1:" . $this->hostPortRangeForContainer . ":80 --name=" . $this->containerName . " " . $this->imageName); } - private function isServerReady($serverAddress) { - $curlHandle = curl_init("http://" . $serverAddress); - - // Returning the transfer as the result of curl_exec prevents the - // transfer from being written to the output. - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); - - $transfer = curl_exec($curlHandle); - - curl_close($curlHandle); - - return $transfer !== false; - } - /** * Cleans up the Nextcloud test server. * diff --git a/build/acceptance/features/core/Utils.php b/build/acceptance/features/core/Utils.php index 5dc52cd7377..86b7515e4c6 100644 --- a/build/acceptance/features/core/Utils.php +++ b/build/acceptance/features/core/Utils.php @@ -56,4 +56,35 @@ class Utils { return $conditionMet; } + /** + * Waits at most $timeout seconds for the server at the given URL to be up, + * checking it again every $timeoutStep seconds. + * + * Note that it does not verify whether the URL returns a valid HTTP status + * or not; it simply checks that the server at the given URL is accessible. + * + * @param string $url the URL for the server to check. + * @param float $timeout the number of seconds (decimals allowed) to wait at + * most for the server. + * @param float $timeoutStep the number of seconds (decimals allowed) to + * wait before checking the server again; by default, 0.5 seconds. + * @return boolean true if the server was found, false otherwise. + */ + public static function waitForServer($url, $timeout, $timeoutStep = 0.5) { + $isServerUpCallback = function() use ($url) { + $curlHandle = curl_init($url); + + // Returning the transfer as the result of curl_exec prevents the + // transfer from being written to the output. + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + + $transfer = curl_exec($curlHandle); + + curl_close($curlHandle); + + return $transfer !== false; + }; + return self::waitFor($isServerUpCallback, $timeout, $timeoutStep); + } + } From ff7d1bf1e7d8fa7acddd34863b306eb6206f832e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sat, 15 Apr 2017 14:30:12 +0200 Subject: [PATCH 17/31] Add NextcloudTestServerHelper for Nextcloud servers in Drone services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due to security concerns, the public Nextcloud server repository is not set as "trusted" in Drone (otherwise a malicious pull request could be used to take over the server), so it is not possible to create Docker containers from the containers started by Drone. Therefore, the Nextcloud server must be started as a service by Drone itself. The NextcloudTestServerDroneHelper is added to manage from the acceptance tests a Nextcloud test server running in a Drone service; to be able to control the remote Nextcloud server the Drone service must provide the Nextcloud server control server. Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerDroneHelper.php | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 build/acceptance/features/core/NextcloudTestServerDroneHelper.php diff --git a/build/acceptance/features/core/NextcloudTestServerDroneHelper.php b/build/acceptance/features/core/NextcloudTestServerDroneHelper.php new file mode 100644 index 00000000000..c6b1df809f4 --- /dev/null +++ b/build/acceptance/features/core/NextcloudTestServerDroneHelper.php @@ -0,0 +1,272 @@ +. + * + */ + +namespace NextcloudServerControl { + +class SocketException extends \Exception { + public function __construct($message) { + parent::__construct($message); + } +} + +/** + * Common class for communication between client and server. + * + * Clients and server communicate through messages: a client sends a request and + * the server answers with a response. Requests and responses all have the same + * common structure composed by a mandatory header and optional data. The header + * contains a code that identifies the type of request or response followed by + * the length of the data (which can be 0). The data is a free form string that + * depends on each request and response type. + * + * The Messenger abstracts all that and provides two public methods: readMessage + * and writeMessage. For each connection a client first writes the request + * message and then reads the response message, while the server first reads the + * request message and then writes the response message. If the client needs to + * send another request it must connect again to the server. + * + * The Messenger class in the server must be kept in sync with the Messenger + * class in the client. Due to the size of the code and its current use it was + * more practical, at least for the time being, to keep two copies of the code + * than creating a library that had to be downloaded and included in the client + * and in the server. + */ +class Messenger { + + /** + * Reset the Nextcloud server. + * + * -Request data: empty + * -OK response data: empty. + * -Failed response data: error information. + */ + const CODE_REQUEST_RESET = 0; + + const CODE_RESPONSE_OK = 0; + const CODE_RESPONSE_FAILED = 1; + + const HEADER_LENGTH = 5; + + /** + * Reads a message from the given socket. + * + * The message is returned as an indexed array with keys "code" and "data". + * + * @param resource $socket the socket to read the message from. + * @return array the message read. + * @throws SocketException if an error occurs while reading the socket. + */ + public static function readMessage($socket) { + $header = self::readSocket($socket, self::HEADER_LENGTH); + $header = unpack("Ccode/VdataLength", $header); + + $data = self::readSocket($socket, $header["dataLength"]); + + return [ "code" => $header["code"], "data" => $data ]; + } + + /** + * Reads content from the given socket. + * + * It blocks until the specified number of bytes were read. + * + * @param resource $socket the socket to read the message from. + * @param int $length the number of bytes to read. + * @return string the content read. + * @throws SocketException if an error occurs while reading the socket. + */ + private static function readSocket($socket, $length) { + if ($socket == null) { + throw new SocketException("Null socket can not be read from"); + } + + $pendingLength = $length; + $content = ""; + + while ($pendingLength > 0) { + $readContent = socket_read($socket, $pendingLength); + if ($readContent === "") { + throw new SocketException("Socket could not be read: $pendingLength bytes are pending, but there is no more data to read"); + } else if ($readContent == false) { + throw new SocketException("Socket could not be read: " . socket_strerror(socket_last_error())); + } + + $pendingLength -= strlen($readContent); + $content = $content . $readContent; + } + + return $content; + } + + /** + * Writes a message to the given socket. + * + * @param resource $socket the socket to write the message to. + * @param int $code the message code. + * @param string $data the message data, if any. + * @throws SocketException if an error occurs while reading the socket. + */ + public static function writeMessage($socket, $code, $data = "") { + if ($socket == null) { + throw new SocketException("Null socket can not be written to"); + } + + $header = pack("CV", $code, strlen($data)); + + $message = $header . $data; + $pendingLength = strlen($message); + + while ($pendingLength > 0) { + $sent = socket_write($socket, $message, $pendingLength); + if ($sent !== 0 && $sent == false) { + throw new SocketException("Message ($message) could not be written: " . socket_strerror(socket_last_error())); + } + + $pendingLength -= $sent; + $message = substr($message, $sent); + } + } +} + +} + +namespace { + +use NextcloudServerControl\Messenger; +use NextcloudServerControl\SocketException; + +/** + * Helper to manage the Nextcloud test server running in a Drone service. + * + * The NextcloudTestServerDroneHelper controls a Nextcloud test server running + * in a Drone service. The "setUp" method resets the Nextcloud server to its + * initial state; nothing needs to be done in the "cleanUp" method. To be able + * to control the remote Nextcloud server the Drone service must provide the + * Nextcloud server control server; the port in which the server listens on can + * be set with the $nextcloudTestServerControlPort parameter of the constructor. + * + * Drone services are available at "127.0.0.1", so the Nextcloud server is + * expected to see "127.0.0.1" as a trusted domain (which would be the case if + * it was installed by running "occ maintenance:install"). Note, however, that + * the Nextcloud server does not listen on port "80" but on port "8000" due to + * internal issues of the Nextcloud server control. In any case, the base URL to + * access the Nextcloud server can be got from "getBaseUrl". + */ +class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { + + /** + * @var int + */ + private $nextcloudTestServerControlPort; + + /** + * Creates a new NextcloudTestServerDroneHelper. + * + * @param int $nextcloudTestServerControlPort the port in which the + * Nextcloud server control is listening. + */ + public function __construct($nextcloudTestServerControlPort) { + $this->nextcloudTestServerControlPort = $nextcloudTestServerControlPort; + } + + /** + * Sets up the Nextcloud test server. + * + * It resets the Nextcloud test server through the control system provided + * by its Drone service and waits for the Nextcloud test server to be + * started again; if the server can not be reset or if it does not start + * again after some time an exception is thrown (as it is just a warning for + * the test runner and nothing to be explicitly catched a plain base + * Exception is used). + * + * @throws \Exception if the Nextcloud test server in the Drone service can + * not be reset or started again. + */ + public function setUp() { + $resetNextcloudServerCallback = function($socket) { + Messenger::writeMessage($socket, Messenger::CODE_REQUEST_RESET); + + $response = Messenger::readMessage($socket); + + if ($response["code"] == Messenger::CODE_RESPONSE_FAILED) { + throw new Exception("Request to reset Nextcloud server failed: " . $response["data"]); + } + }; + $this->sendRequestAndHandleResponse($resetNextcloudServerCallback); + + $timeout = 60; + if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) { + throw new Exception("Nextcloud test server could not be started"); + } + } + + /** + * Cleans up the Nextcloud test server. + * + * Nothing needs to be done when using the Drone service. + */ + public function cleanUp() { + } + + /** + * Returns the base URL of the Nextcloud test server. + * + * @return string the base URL of the Nextcloud test server. + */ + public function getBaseUrl() { + return "http://127.0.0.1:8000/index.php"; + } + + /** + * Executes the given callback to communicate with the Nextcloud test server + * control. + * + * A socket is created with the Nextcloud test server control and passed to + * the callback to send the request and handle its response. + * + * @param \Closure $nextcloudServerControlCallback the callback to call with + * the communication socket. + * @throws \Exception if any socket-related operation fails. + */ + private function sendRequestAndHandleResponse($nextcloudServerControlCallback) { + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($socket === false) { + throw new Exception("Request socket to reset Nextcloud server could not be created: " . socket_strerror(socket_last_error())); + } + + try { + if (socket_connect($socket, "127.0.0.1", $this->nextcloudTestServerControlPort) === false) { + throw new Exception("Request socket to reset Nextcloud server could not be connected: " . socket_strerror(socket_last_error())); + } + + $nextcloudServerControlCallback($socket); + } catch (SocketException $exception) { + throw new Exception("Request socket to reset Nextcloud server failed: " . $exception->getMessage()); + } finally { + socket_close($socket); + } + } + +} + +} From a7e1833cf3f110845da92180f0e8fbb9fbf430ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sat, 15 Apr 2017 14:53:28 +0200 Subject: [PATCH 18/31] Add the timeout in NoSuchElementException messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/features/core/Actor.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build/acceptance/features/core/Actor.php b/build/acceptance/features/core/Actor.php index 2445a09bffc..ac08822fffe 100644 --- a/build/acceptance/features/core/Actor.php +++ b/build/acceptance/features/core/Actor.php @@ -139,7 +139,11 @@ class Actor { return $element !== null; }; if (!Utils::waitFor($findCallback, $timeout, $timeoutStep)) { - throw new NoSuchElementException($elementLocator->getDescription() . " could not be found"); + $message = $elementLocator->getDescription() . " could not be found"; + if ($timeout > 0) { + $message = $message . " after $timeout seconds"; + } + throw new NoSuchElementException($message); } return $element; @@ -177,6 +181,9 @@ class Actor { // exception in the chain. $message = $exception->getMessage() . "\n" . $elementLocator->getDescription() . " could not be found"; + if ($timeout > 0) { + $message = $message . " after $timeout seconds"; + } throw new NoSuchElementException($message, $exception); } } From be96be09b599235c6b712d39f88760f10020508b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sat, 15 Apr 2017 18:56:24 +0200 Subject: [PATCH 19/31] Add general multiplier for find timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Although the timeouts specified in the acceptance tests are enough in most cases they may not be when running them in a slow system or environment. For those situations a general multiplier for find timeouts is added. It can be set in the "behat.yml" configuration file to increase the timeout used in every find call (except those that used a timeout of 0, as in those cases the element had to be already present when finding it and whether the system is slow or not does not change that). Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/features/core/Actor.php | 28 ++++++++++++++++++- .../acceptance/features/core/ActorContext.php | 21 ++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/build/acceptance/features/core/Actor.php b/build/acceptance/features/core/Actor.php index ac08822fffe..a27e8e6a015 100644 --- a/build/acceptance/features/core/Actor.php +++ b/build/acceptance/features/core/Actor.php @@ -41,6 +41,13 @@ * several features: the element can be looked for based on a Locator object, an * exception is thrown if the element is not found, and, optionally, it is * possible to try again to find the element several times before giving up. + * + * The amount of time to wait before giving up is specified in each call to + * find(). However, a general multiplier to be applied to every timeout can be + * set using setFindTimeoutMultiplier(); this makes possible to retry longer + * before giving up without modifying the tests themselves. Note that the + * multiplier affects the timeout, but not the timeout step; the rate at which + * find() will try again to find the element does not change. */ class Actor { @@ -54,6 +61,11 @@ class Actor { */ private $baseUrl; + /** + * @var float + */ + private $findTimeoutMultiplier; + /** * Creates a new Actor. * @@ -64,6 +76,7 @@ class Actor { public function __construct(\Behat\Mink\Session $session, $baseUrl) { $this->session = $session; $this->baseUrl = $baseUrl; + $this->findTimeoutMultiplier = 1; } /** @@ -75,6 +88,16 @@ class Actor { $this->baseUrl = $baseUrl; } + /** + * Sets the multiplier for find timeouts. + * + * @param float $findTimeoutMultiplier the multiplier to apply to find + * timeouts. + */ + public function setFindTimeoutMultiplier($findTimeoutMultiplier) { + $this->findTimeoutMultiplier = $findTimeoutMultiplier; + } + /** * Returns the Mink Session used to control its web browser. * @@ -113,7 +136,8 @@ class Actor { * and, then, up to 10 seconds to find the ancestor and, then, up to 10 * seconds to find the element. By default the timeout is 0, so the element * and its ancestor will be looked for just once; the default time to wait - * before retrying is half a second. + * before retrying is half a second. If the timeout is not 0 it will be + * affected by the multiplier set using setFindTimeoutMultiplier(), if any. * * In any case, if the element, or its ancestors, can not be found a * NoSuchElementException is thrown. @@ -128,6 +152,8 @@ class Actor { * be found. */ public function find($elementLocator, $timeout = 0, $timeoutStep = 0.5) { + $timeout = $timeout * $this->findTimeoutMultiplier; + $element = null; $selector = $elementLocator->getSelector(); $locator = $elementLocator->getLocator(); diff --git a/build/acceptance/features/core/ActorContext.php b/build/acceptance/features/core/ActorContext.php index e655911af67..9667ef2f01c 100644 --- a/build/acceptance/features/core/ActorContext.php +++ b/build/acceptance/features/core/ActorContext.php @@ -38,6 +38,10 @@ use Behat\MinkExtension\Context\RawMinkContext; * Besides updating the current actor in sibling contexts the ActorContext also * propagates its inherited "base_url" Mink parameter to the Actors as needed. * + * By default no multiplier for the find timeout is set in the Actors. However, + * it can be customized using the "actorFindTimeoutMultiplier" parameter of the + * ActorContext in "behat.yml". + * * Every actor used in the scenarios must have a corresponding Mink session * declared in "behat.yml" with the same name as the actor. All used sessions * are stopped after each scenario is run. @@ -54,6 +58,21 @@ class ActorContext extends RawMinkContext { */ private $currentActor; + /** + * @var float + */ + private $actorFindTimeoutMultiplier; + + /** + * Creates a new ActorContext. + * + * @param float $actorFindTimeoutMultiplier the find timeout multiplier to + * set in the Actors. + */ + public function __construct($actorFindTimeoutMultiplier = 1) { + $this->actorFindTimeoutMultiplier = $actorFindTimeoutMultiplier; + } + /** * Sets a Mink parameter. * @@ -85,6 +104,7 @@ class ActorContext extends RawMinkContext { $this->actors = array(); $this->actors["default"] = new Actor($this->getSession(), $this->getMinkParameter("base_url")); + $this->actors["default"]->setFindTimeoutMultiplier($this->actorFindTimeoutMultiplier); $this->currentActor = $this->actors["default"]; } @@ -108,6 +128,7 @@ class ActorContext extends RawMinkContext { public function iActAs($actorName) { if (!array_key_exists($actorName, $this->actors)) { $this->actors[$actorName] = new Actor($this->getSession($actorName), $this->getMinkParameter("base_url")); + $this->actors[$actorName]->setFindTimeoutMultiplier($this->actorFindTimeoutMultiplier); } $this->currentActor = $this->actors[$actorName]; From ed7d63d16a3e954841c40253a8a54223f6ae729e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sun, 16 Apr 2017 13:39:59 +0200 Subject: [PATCH 20/31] Add acceptance test steps to Drone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each acceptance test feature is run in its own Drone step. The container of the step runs the acceptance tests themselves, but they require two additional Drone services. One service provides the Selenium server that performs the web browser actions specified by the tests, and the other service provides the Nextcloud server that the tests will be run against (due to security concerns the acceptance tests themselves can not create Docker containers for the Nextcloud server as done when running them in a local system, as if Drone containers had access to Docker a malicious pull request could be used to take over the Drone server). Signed-off-by: Daniel Calviño Sánchez --- .drone.yml | 40 +++++++++++++++++++ build/acceptance/run-drone.sh | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100755 build/acceptance/run-drone.sh diff --git a/.drone.yml b/.drone.yml index 2d4134957ad..16f4be6a39a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -478,6 +478,33 @@ pipeline: when: matrix: TESTS: integration-trashbin + # As it needs access to the cloned Git repository it must be defined in the + # pipeline as "detached" instead of in the services. + service-acceptance-nextcloud-server: + image: nextcloudci/acceptance-nextcloud-server-php7.1-apache + detach: true + commands: + # "nextcloud-server-control-setup.sh" can not be set as the entry point in + # the image because Drone overrides it. + - /usr/local/bin/nextcloud-server-control-setup.sh + - su --shell "/bin/sh" --command "php /usr/local/bin/nextcloud-server-control.php 12345" - www-data + when: + matrix: + TESTS: acceptance + acceptance-access-levels: + image: nextcloudci/php7.0:php7.0-7 + commands: + - build/acceptance/run-drone.sh features/access-levels.feature + when: + matrix: + TESTS-ACCEPTANCE: access-levels + acceptance-login: + image: nextcloudci/php7.0:php7.0-7 + commands: + - build/acceptance/run-drone.sh features/login.feature + when: + matrix: + TESTS-ACCEPTANCE: login nodb-codecov: image: nextcloudci/php7.0:php7.0-7 commands: @@ -551,6 +578,10 @@ matrix: - TESTS: integration-transfer-ownership-features - TESTS: integration-ldap-features - TESTS: integration-trashbin + - TESTS: acceptance + TESTS-ACCEPTANCE: access-levels + - TESTS: acceptance + TESTS-ACCEPTANCE: login - TESTS: jsunit - TESTS: check-autoloader - TESTS: check-mergejs @@ -626,5 +657,14 @@ services: when: matrix: OBJECT_STORE: s3 + selenium: + image: selenium/standalone-firefox:2.53.1-beryllium + environment: + # Reduce default log level for Selenium server (INFO) as it is too + # verbose. + - JAVA_OPTS=-Dselenium.LOGGER.level=WARNING + when: + matrix: + TESTS: acceptance branches: [ master, stable* ] diff --git a/build/acceptance/run-drone.sh b/build/acceptance/run-drone.sh new file mode 100755 index 00000000000..93e91c474c6 --- /dev/null +++ b/build/acceptance/run-drone.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com) +# +# @license GNU AGPL version 3 or any later version +# +# This program 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 (at your option) 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 . + +# Helper script to run the acceptance tests, which test a running Nextcloud +# instance from the point of view of a real user, in a Drone step. +# +# The acceptance tests are written in Behat so, besides running the tests, this +# script installs Behat, its dependencies, and some related packages in the +# "vendor" subdirectory of the acceptance tests. The acceptance tests also use +# the Selenium server to control a web browser, and they require a Nextcloud +# server to be available, so this script waits for the Selenium server and the +# Nextcloud server (both provided in their own Drone service) to be ready before +# running the tests. + +# Exit immediately on errors. +set -o errexit + +# Ensure working directory is script directory, as some actions (like installing +# Behat through Composer or running Behat) expect that. +cd "$(dirname $0)" + +SCENARIO_TO_RUN=$1 + +composer install + +# Although Behat documentation states that using the BEHAT_PARAMS environment +# variable "You can set any value for any option that is available in a +# behat.yml file" this is currently not true for the constructor parameters of +# contexts (see https://github.com/Behat/Behat/issues/983). Thus, the default +# "behat.yml" configuration file has to be adjusted to provide the appropriate +# parameters for NextcloudTestServerContext. +ORIGINAL="\ + - NextcloudTestServerContext" +REPLACEMENT="\ + - NextcloudTestServerContext:\n\ + nextcloudTestServerHelper: NextcloudTestServerDroneHelper\n\ + nextcloudTestServerHelperParameters:\n\ + - $NEXTCLOUD_SERVER_CONTROL_PORT" +sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-drone.yml + +# Both the Selenium server and the Nextcloud server control should be ready by +# now, as Composer typically takes way longer to execute than their startup +# (which is done in parallel in Drone services), but just in case. + +echo "Waiting for Selenium" +timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done" + +# This just checks if it can connect to the port in which the Nextcloud server +# control should be listening on. +NEXTCLOUD_SERVER_CONTROL_PORT="12345" +PHP_CHECK_NEXTCLOUD_SERVER="\ +if ((\\\$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) { exit(1); } \ +if (socket_connect(\\\$socket, \\\"127.0.0.1\\\", \\\"$NEXTCLOUD_SERVER_CONTROL_PORT\\\") === false) { exit(1); } \ +socket_close(\\\$socket);" + +echo "Waiting for Nextcloud server control" +timeout 60s bash -c "while ! php -r \"$PHP_CHECK_NEXTCLOUD_SERVER\" >/dev/null 2>&1; do sleep 1; done" + +vendor/bin/behat --config=config/behat-drone.yml $SCENARIO_TO_RUN From 50dfca8d8ae786c5c8802baa1f020e6e7513d7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sun, 16 Apr 2017 18:12:12 +0200 Subject: [PATCH 21/31] Make possible to specify a subset of the acceptance tests to run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/run.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh index 96ed9c8db4f..99087d93af4 100755 --- a/build/acceptance/run.sh +++ b/build/acceptance/run.sh @@ -264,9 +264,12 @@ trap cleanUp EXIT # that. cd "$(dirname $0)" +# If no parameter is provided to this script all the acceptance tests are run. +SCENARIO_TO_RUN=$1 + prepareBehat prepareSelenium prepareDocker -echo "Running all tests" -vendor/bin/behat +echo "Running tests" +vendor/bin/behat $SCENARIO_TO_RUN From 593118204a5a8d5c54423e9424d8469689ceed19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sun, 16 Apr 2017 20:31:52 +0200 Subject: [PATCH 22/31] Replace downloaded Selenium server with Docker container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of downloading the Selenium server and requiring a specific Firefox version to be installed in the system now the Selenium server is run using one of the official Selenium Docker images, which provides both the Selenium server and the appropriate version of Firefox. Moreover, as it is run inside the Docker container, the web browser is now run in headless mode; however, if needed, it can still be viewed through VNC. Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerContext.php | 6 +- .../core/NextcloudTestServerDockerHelper.php | 52 ++++----- build/acceptance/run.sh | 105 +++++++----------- 3 files changed, 68 insertions(+), 95 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/build/acceptance/features/core/NextcloudTestServerContext.php index 847038eac0c..4e0ba05bde9 100644 --- a/build/acceptance/features/core/NextcloudTestServerContext.php +++ b/build/acceptance/features/core/NextcloudTestServerContext.php @@ -46,7 +46,7 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; * customized using the "nextcloudTestServerHelperParameters" parameter, which * is an array (without keys) with the value of the parameters in the same order * as in the constructor of the helper class (by default, - * [ "nextcloud-local-test-acceptance", "15000-16000" ]). + * [ "nextcloud-local-test-acceptance", "selenium-nextcloud-local-test-acceptance" ]). * * Example of custom parameters in "behat.yml": * default: @@ -57,7 +57,7 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; * nextcloudTestServerHelper: NextcloudTestServerDockerHelper * nextcloudTestServerHelperParameters: * - nextcloud-local-test-acceptance-custom-image - * - 23000-42000 + * - selenium-nextcloud-local-test-acceptance-custom-image * * Note that using Docker containers as a regular user requires giving access to * the Docker daemon to that user. Unfortunately, that makes possible for that @@ -81,7 +81,7 @@ class NextcloudTestServerContext implements Context { * constructor of the $nextcloudTestServerHelper class. */ public function __construct($nextcloudTestServerHelper = "NextcloudTestServerDockerHelper", - $nextcloudTestServerHelperParameters = [ "nextcloud-local-test-acceptance", "15000-16000" ]) { + $nextcloudTestServerHelperParameters = [ "nextcloud-local-test-acceptance", "selenium-nextcloud-local-test-acceptance" ]) { $nextcloudTestServerHelperClass = new ReflectionClass($nextcloudTestServerHelper); if ($nextcloudTestServerHelperParameters === null) { diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php index 120e13fe70c..d13efdb6b0a 100644 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php @@ -34,12 +34,14 @@ * * Also, the Nextcloud server installed in the Docker image is expected to see * "127.0.0.1" as a trusted domain (which would be the case if it was installed - * by running "occ maintenance:install"). Therefore, the Nextcloud server is - * accessed through a local port in the host system mapped to the port 80 of the - * Docker container; if the Nextcloud server was instead accessed directly - * through its IP address it would complain that it was being accessed from an - * untrusted domain and refuse to work until the admin whitelisted it. The base - * URL to access the Nextcloud server can be got from "getBaseUrl". + * by running "occ maintenance:install"). Therefore, the Nextcloud server + * container is connected to the network of the Selenium server container (which + * can be customized in the constructor) so the Selenium server can access the + * Nextcloud server using the "127.0.0.1" IP address. The Selenium server + * container is also expected to map its 80 port to the 80 port of the host so + * the acceptance tests can also access the Nextcloud server using the + * "127.0.0.1" IP address. In any case, the base URL to access the Nextcloud + * server can be got from "getBaseUrl". * * Internally, the NextcloudTestServerDockerHelper uses the Docker Command Line * Interface (the "docker" command) to run, get information from, and destroy @@ -76,7 +78,7 @@ class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { /** * @var string */ - private $hostPortRangeForContainer; + private $seleniumContainerName; /** * @var string @@ -88,12 +90,12 @@ class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { * * @param string $imageName the name of the Docker image that provides the * Nextcloud test server. - * @param string $hostPortRangeForContainer the range of local ports in the - * host in which the port 80 of the container can be published. + * @param string $seleniumContainerName the name of the Selenium server + * container. */ - public function __construct($imageName = "nextcloud-local-test-acceptance", $hostPortRangeForContainer = "15000-16000") { + public function __construct($imageName = "nextcloud-local-test-acceptance", $seleniumContainerName = "selenium-nextcloud-local-test-acceptance") { $this->imageName = $imageName; - $this->hostPortRangeForContainer = $hostPortRangeForContainer; + $this->seleniumContainerName = $seleniumContainerName; $this->containerName = null; } @@ -131,11 +133,15 @@ class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { // There is no need to start the web server as root, so it is started // directly as www-data instead. - // The port 80 of the container is mapped to a free port from a range in - // the host system; due to this it can be accessed from the host using - // the "127.0.0.1" IP address, which prevents Nextcloud from complaining - // that it is being accessed from an untrusted domain. - $this->executeDockerCommand("run --detach --user=www-data --publish 127.0.0.1:" . $this->hostPortRangeForContainer . ":80 --name=" . $this->containerName . " " . $this->imageName); + // The container is connected to the network of the Selenium server + // container; due to this, the Selenium server can access the Nextcloud + // server using the "127.0.0.1" IP address, which prevents Nextcloud + // from complaining that it is being accessed from an untrusted domain. + // Moreover, as the Selenium server container is expected to map its + // 80 port to the 80 port of the host the acceptance tests can also + // access the Nextcloud server using the "127.0.0.1" IP address to check + // whether the server is ready or not. + $this->executeDockerCommand("run --detach --user=www-data --network container:" . $this->seleniumContainerName . " --name=" . $this->containerName . " " . $this->imageName); } /** @@ -195,19 +201,7 @@ class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { * container is not running. */ public function getBaseUrl() { - return "http://" . $this->getNextcloudTestServerAddress() . "/index.php"; - } - - /** - * Returns the IP address and port of the Nextcloud test server (which is - * mapped to a local port in the host). - * - * @return string the IP address and port as "$ipAddress:$port". - * @throws \Exception if the Docker command failed to execute or the - * container is not running. - */ - private function getNextcloudTestServerAddress() { - return $this->executeDockerCommand("port " . $this->containerName . " 80"); + return "http://127.0.0.1/index.php"; } /** diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh index 99087d93af4..eb8ce479769 100755 --- a/build/acceptance/run.sh +++ b/build/acceptance/run.sh @@ -24,14 +24,13 @@ # script installs Behat, its dependencies, and some related packages in the # "vendor" subdirectory of the acceptance tests. The acceptance tests also use # the Selenium server to control a web browser, so the Selenium server is also -# installed to the "selenium" subdirectory and launched before the tests start -# (it will be stopped automatically once the tests end). Finally, the tests -# expect that a Docker image with the Nextcloud installation to be tested is -# available, so the script creates it based on the Nextcloud code from the -# grandparent directory. +# launched before the tests start in its own Docker container (it will be +# stopped automatically once the tests end). Finally, the tests expect that a +# Docker image with the Nextcloud installation to be tested is available, so the +# script creates it based on the Nextcloud code from the grandparent directory. # -# To perform its job, the script requires the "composer", "java" and "docker" -# commands to be available. +# To perform its job, the script requires the "composer" and "docker" commands +# to be available. # # The Docker Command Line Interface (the "docker" command) requires special # permissions to talk to the Docker daemon, and those permissions are typically @@ -47,8 +46,9 @@ # https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface # # Finally, take into account that this script will automatically remove the -# Docker containers named "nextcloud-local-test-acceptance" and -# "nextcloud-local-test-acceptance-[0-9a-f.]*" and the Docker image tagged as +# Docker containers named "selenium-nextcloud-local-test-acceptance", +# "nextcloud-local-test-acceptance" and +# "nextcloud-local-test-acceptance-[0-9a-f.]*", and the Docker image tagged as # "nextcloud-local-test-acceptance:latest", even if the script did not create # them (probably you will not have containers nor images with those names, but # just in case). @@ -62,71 +62,50 @@ function prepareBehat() { composer install } -# Launches the Selenium server, installing it if needed. +# Launches the Selenium server in a Docker container. # # The acceptance tests use Firefox by default but, unfortunately, Firefox >= 48 # does not provide yet the same level of support as earlier versions for certain -# features related to automated testing. Therefore, if an incompatible version -# is found the script will be exited immediately with an error state. +# features related to automated testing. Therefore, the Docker image used is not +# the latest one, but an older version known to work. # -# The Selenium server is installed in the "selenium" subdirectory of the -# directory of the script. +# The acceptance tests expect the Selenium server to be accessible at +# "127.0.0.1:4444", so the 4444 port of the container is mapped to the 4444 port +# of the host. # -# The Selenium server launched here will be automatically stopped when the +# The Nextcloud server has to be accessed at "127.0.0.1" by the Selenium server +# (as that is the only trusted domain by default), so the Nextcloud server +# containers have to be connected to the network of the Selenium server +# container (another option would be to connect the Selenium server to the host +# network, but messing with the host network is better avoided if possible). The +# acceptance tests themselves also need access to the Nextcloud server to ensure +# that it is ready before starting each scenario, so the 80 port of the Selenium +# server is mapped to the 80 port of the host (it is not possible to map the +# port in the container that connects to the network of another container). +# +# Besides the Selenium server, the Docker image also provides a VNC server, so +# the 5900 port of the container is also mapped to the 5900 port of the host. +# +# The Docker container started here will be automatically stopped when the # script exits (see cleanUp). If the Selenium server can not be started then the # script will be exited immediately with an error state; the most common cause # for the Selenium server to fail to start is that another server is already # running in the default port. # -# The output of the Selenium server will be saved to -# "selenium/selenium-server-{DATE}.log". +# As the web browser is run inside the Docker container it is not visible by +# default. However, it can be viewed using VNC (for example, +# "vncviewer 127.0.0.1:5900"); when asked for the password use "secret". function prepareSelenium() { - FIREFOX_MAJOR_VERSION=$(firefox --version | sed -e "s/Mozilla Firefox \([0-9]\+\).*/\1/") - if [ "$FIREFOX_MAJOR_VERSION" -ge 48 ]; then - echo "The acceptance tests can not be run on Mozilla Firefox >= 48 (major version found was $FIREFOX_MAJOR_VERSION)" - exit 1 - fi - - SELENIUM_SERVER_STANDALONE="selenium-server-standalone-2.53.1.jar" - SELENIUM_SERVER_STANDALONE_URL="http://selenium-release.storage.googleapis.com/2.53/$SELENIUM_SERVER_STANDALONE" - - mkdir --parents selenium - - if [ ! -f "selenium/$SELENIUM_SERVER_STANDALONE" ]; then - echo "Installing Selenium server" - wget --output-document="selenium/$SELENIUM_SERVER_STANDALONE" "$SELENIUM_SERVER_STANDALONE_URL" - fi - - SELENIUM_SERVER_STANDALONE_LOG="selenium-server-$(date +%Y%m%d-%H%M%S).log" + SELENIUM_CONTAINER=selenium-nextcloud-local-test-acceptance echo "Starting Selenium server" - # LANG=C forces "English" output for Selenium server to be able to look for - # the startup finished message (I do not really know if Selenium server log - # messages are localized or not, but just in case). - LANG=C java -jar "selenium/$SELENIUM_SERVER_STANDALONE" &>"selenium/$SELENIUM_SERVER_STANDALONE_LOG" & - SELENIUM_SERVER_STANDALONE_PID=$! + docker run --detach --name=$SELENIUM_CONTAINER --publish 80:80 --publish 4444:4444 --publish 5900:5900 selenium/standalone-firefox-debug:2.53.1-beryllium - echo -n "Waiting for Selenium server to be ready" - TIMEOUT=10 - TIMEOUT_STEP=1 - ELAPSED_TIME=0 - while [ $ELAPSED_TIME -lt $TIMEOUT ] && ! grep "Selenium Server is up and running" "selenium/$SELENIUM_SERVER_STANDALONE_LOG" &>/dev/null; do - sleep $TIMEOUT_STEP - echo -n "." - ELAPSED_TIME=$((ELAPSED_TIME+TIMEOUT_STEP)) - done - echo - - if [ "$ELAPSED_TIME" -eq "$TIMEOUT" ]; then - echo -n "Could not start Selenium server; see" \ - "$PWD/selenium/$SELENIUM_SERVER_STANDALONE_LOG" - - if grep "Address already in use" "selenium/$SELENIUM_SERVER_STANDALONE_LOG" &>/dev/null; then - echo " (probably another" \ - "Selenium server is already running)" - else - echo - fi + echo "Waiting for Selenium server to be ready" + if ! timeout 10s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done"; then + echo "Could not start Selenium server; running" \ + "\"docker run --rm --publish 80:80 --publish 4444:4444 --publish 5900:5900 selenium/standalone-firefox-debug:2.53.1-beryllium\"" \ + "could give you a hint of the problem" exit 1 fi @@ -247,9 +226,9 @@ function cleanUp() { docker rmi $NEXTCLOUD_LOCAL_IMAGE:latest fi - if [ -n "$SELENIUM_SERVER_STANDALONE_PID" ]; then - echo "Stopping Selenium server (PID $SELENIUM_SERVER_STANDALONE_PID)" - kill $SELENIUM_SERVER_STANDALONE_PID + if [ -n "$(docker ps --all --quiet --filter name="^/$SELENIUM_CONTAINER$")" ]; then + echo "Removing Docker container $SELENIUM_CONTAINER" + docker rm --volumes --force $SELENIUM_CONTAINER fi } From 72310cdac1d9a4b9dd9b78e0734100a058fb9714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 18 Apr 2017 20:24:46 +0200 Subject: [PATCH 23/31] Use PHP built-in web server instead of Apache in Drone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of running an additional Drone service with the Nextcloud server now the Nextcloud server is run in the same Drone step as the acceptance tests themselves using the PHP built-in web server. Thanks to this, the Nextcloud server control is no longer needed, as the acceptance tests can now directly reset, start and stop the Nextcloud server. Also, the "nextcloudci/php7.0:php7.0-7" image provides everything needed to run and manage the Nextcloud server (including the Git command used to restore the directory to a saved state), so the custom image is no longer needed either. Signed-off-by: Daniel Calviño Sánchez --- .drone.yml | 13 - .../core/NextcloudTestServerDroneHelper.php | 256 ++++-------------- build/acceptance/run-drone.sh | 42 +-- 3 files changed, 80 insertions(+), 231 deletions(-) diff --git a/.drone.yml b/.drone.yml index 16f4be6a39a..9996b5df5cf 100644 --- a/.drone.yml +++ b/.drone.yml @@ -478,19 +478,6 @@ pipeline: when: matrix: TESTS: integration-trashbin - # As it needs access to the cloned Git repository it must be defined in the - # pipeline as "detached" instead of in the services. - service-acceptance-nextcloud-server: - image: nextcloudci/acceptance-nextcloud-server-php7.1-apache - detach: true - commands: - # "nextcloud-server-control-setup.sh" can not be set as the entry point in - # the image because Drone overrides it. - - /usr/local/bin/nextcloud-server-control-setup.sh - - su --shell "/bin/sh" --command "php /usr/local/bin/nextcloud-server-control.php 12345" - www-data - when: - matrix: - TESTS: acceptance acceptance-access-levels: image: nextcloudci/php7.0:php7.0-7 commands: diff --git a/build/acceptance/features/core/NextcloudTestServerDroneHelper.php b/build/acceptance/features/core/NextcloudTestServerDroneHelper.php index c6b1df809f4..23f6db2df97 100644 --- a/build/acceptance/features/core/NextcloudTestServerDroneHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerDroneHelper.php @@ -21,198 +21,63 @@ * */ -namespace NextcloudServerControl { - -class SocketException extends \Exception { - public function __construct($message) { - parent::__construct($message); - } -} - /** - * Common class for communication between client and server. + * Helper to manage a Nextcloud test server when acceptance tests are run in a + * Drone step. * - * Clients and server communicate through messages: a client sends a request and - * the server answers with a response. Requests and responses all have the same - * common structure composed by a mandatory header and optional data. The header - * contains a code that identifies the type of request or response followed by - * the length of the data (which can be 0). The data is a free form string that - * depends on each request and response type. + * The Nextcloud test server is executed using the PHP built-in web server + * directly from the grandparent directory of the acceptance tests directory + * (that is, the root directory of the Nextcloud server); note that the + * acceptance tests must be run from the acceptance tests directory. The "setUp" + * method resets the Nextcloud server to its initial state and starts it, while + * the "cleanUp" method stops it. To be able to reset the Nextcloud server to + * its initial state a Git repository must be provided in the root directory of + * the Nextcloud server; the last commit in that Git repository must provide the + * initial state for the Nextcloud server expected by the acceptance tests. * - * The Messenger abstracts all that and provides two public methods: readMessage - * and writeMessage. For each connection a client first writes the request - * message and then reads the response message, while the server first reads the - * request message and then writes the response message. If the client needs to - * send another request it must connect again to the server. - * - * The Messenger class in the server must be kept in sync with the Messenger - * class in the client. Due to the size of the code and its current use it was - * more practical, at least for the time being, to keep two copies of the code - * than creating a library that had to be downloaded and included in the client - * and in the server. - */ -class Messenger { - - /** - * Reset the Nextcloud server. - * - * -Request data: empty - * -OK response data: empty. - * -Failed response data: error information. - */ - const CODE_REQUEST_RESET = 0; - - const CODE_RESPONSE_OK = 0; - const CODE_RESPONSE_FAILED = 1; - - const HEADER_LENGTH = 5; - - /** - * Reads a message from the given socket. - * - * The message is returned as an indexed array with keys "code" and "data". - * - * @param resource $socket the socket to read the message from. - * @return array the message read. - * @throws SocketException if an error occurs while reading the socket. - */ - public static function readMessage($socket) { - $header = self::readSocket($socket, self::HEADER_LENGTH); - $header = unpack("Ccode/VdataLength", $header); - - $data = self::readSocket($socket, $header["dataLength"]); - - return [ "code" => $header["code"], "data" => $data ]; - } - - /** - * Reads content from the given socket. - * - * It blocks until the specified number of bytes were read. - * - * @param resource $socket the socket to read the message from. - * @param int $length the number of bytes to read. - * @return string the content read. - * @throws SocketException if an error occurs while reading the socket. - */ - private static function readSocket($socket, $length) { - if ($socket == null) { - throw new SocketException("Null socket can not be read from"); - } - - $pendingLength = $length; - $content = ""; - - while ($pendingLength > 0) { - $readContent = socket_read($socket, $pendingLength); - if ($readContent === "") { - throw new SocketException("Socket could not be read: $pendingLength bytes are pending, but there is no more data to read"); - } else if ($readContent == false) { - throw new SocketException("Socket could not be read: " . socket_strerror(socket_last_error())); - } - - $pendingLength -= strlen($readContent); - $content = $content . $readContent; - } - - return $content; - } - - /** - * Writes a message to the given socket. - * - * @param resource $socket the socket to write the message to. - * @param int $code the message code. - * @param string $data the message data, if any. - * @throws SocketException if an error occurs while reading the socket. - */ - public static function writeMessage($socket, $code, $data = "") { - if ($socket == null) { - throw new SocketException("Null socket can not be written to"); - } - - $header = pack("CV", $code, strlen($data)); - - $message = $header . $data; - $pendingLength = strlen($message); - - while ($pendingLength > 0) { - $sent = socket_write($socket, $message, $pendingLength); - if ($sent !== 0 && $sent == false) { - throw new SocketException("Message ($message) could not be written: " . socket_strerror(socket_last_error())); - } - - $pendingLength -= $sent; - $message = substr($message, $sent); - } - } -} - -} - -namespace { - -use NextcloudServerControl\Messenger; -use NextcloudServerControl\SocketException; - -/** - * Helper to manage the Nextcloud test server running in a Drone service. - * - * The NextcloudTestServerDroneHelper controls a Nextcloud test server running - * in a Drone service. The "setUp" method resets the Nextcloud server to its - * initial state; nothing needs to be done in the "cleanUp" method. To be able - * to control the remote Nextcloud server the Drone service must provide the - * Nextcloud server control server; the port in which the server listens on can - * be set with the $nextcloudTestServerControlPort parameter of the constructor. - * - * Drone services are available at "127.0.0.1", so the Nextcloud server is - * expected to see "127.0.0.1" as a trusted domain (which would be the case if - * it was installed by running "occ maintenance:install"). Note, however, that - * the Nextcloud server does not listen on port "80" but on port "8000" due to - * internal issues of the Nextcloud server control. In any case, the base URL to - * access the Nextcloud server can be got from "getBaseUrl". + * The Nextcloud server is available at "127.0.0.1", so it is expected to see + * "127.0.0.1" as a trusted domain (which would be the case if it was installed + * by running "occ maintenance:install"). The base URL to access the Nextcloud + * server can be got from "getBaseUrl". */ class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { /** - * @var int + * @var string */ - private $nextcloudTestServerControlPort; + private $phpServerPid; /** * Creates a new NextcloudTestServerDroneHelper. - * - * @param int $nextcloudTestServerControlPort the port in which the - * Nextcloud server control is listening. */ - public function __construct($nextcloudTestServerControlPort) { - $this->nextcloudTestServerControlPort = $nextcloudTestServerControlPort; + public function __construct() { + $this->phpServerPid = ""; } /** * Sets up the Nextcloud test server. * - * It resets the Nextcloud test server through the control system provided - * by its Drone service and waits for the Nextcloud test server to be - * started again; if the server can not be reset or if it does not start - * again after some time an exception is thrown (as it is just a warning for - * the test runner and nothing to be explicitly catched a plain base - * Exception is used). + * It resets the Nextcloud test server restoring its last saved Git state + * and then waits for the Nextcloud test server to start again; if the + * server can not be reset or if it does not start again after some time an + * exception is thrown (as it is just a warning for the test runner and + * nothing to be explicitly catched a plain base Exception is used). * - * @throws \Exception if the Nextcloud test server in the Drone service can - * not be reset or started again. + * @throws \Exception if the Nextcloud test server can not be reset or + * started again. */ public function setUp() { - $resetNextcloudServerCallback = function($socket) { - Messenger::writeMessage($socket, Messenger::CODE_REQUEST_RESET); + // Ensure that previous PHP server is not running (as cleanUp may not + // have been called). + $this->killPhpServer(); - $response = Messenger::readMessage($socket); + $this->execOrException("cd ../../ && git reset --hard HEAD"); + $this->execOrException("cd ../../ && git clean -d --force"); - if ($response["code"] == Messenger::CODE_RESPONSE_FAILED) { - throw new Exception("Request to reset Nextcloud server failed: " . $response["data"]); - } - }; - $this->sendRequestAndHandleResponse($resetNextcloudServerCallback); + // execOrException is not used because the server is started in the + // background, so the command will always succeed even if the server + // itself fails. + $this->phpServerPid = exec("php -S 127.0.0.1:80 -t ../../ >/dev/null 2>&1 & echo $!"); $timeout = 60; if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) { @@ -223,9 +88,10 @@ class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { /** * Cleans up the Nextcloud test server. * - * Nothing needs to be done when using the Drone service. + * It kills the running Nextcloud test server, if any. */ public function cleanUp() { + $this->killPhpServer(); } /** @@ -234,39 +100,35 @@ class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { * @return string the base URL of the Nextcloud test server. */ public function getBaseUrl() { - return "http://127.0.0.1:8000/index.php"; + return "http://127.0.0.1/index.php"; } /** - * Executes the given callback to communicate with the Nextcloud test server - * control. + * Executes the given command, throwing an Exception if it fails. * - * A socket is created with the Nextcloud test server control and passed to - * the callback to send the request and handle its response. - * - * @param \Closure $nextcloudServerControlCallback the callback to call with - * the communication socket. - * @throws \Exception if any socket-related operation fails. + * @param string $command the command to execute. + * @throws \Exception if the command fails to execute. */ - private function sendRequestAndHandleResponse($nextcloudServerControlCallback) { - $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - if ($socket === false) { - throw new Exception("Request socket to reset Nextcloud server could not be created: " . socket_strerror(socket_last_error())); - } - - try { - if (socket_connect($socket, "127.0.0.1", $this->nextcloudTestServerControlPort) === false) { - throw new Exception("Request socket to reset Nextcloud server could not be connected: " . socket_strerror(socket_last_error())); - } - - $nextcloudServerControlCallback($socket); - } catch (SocketException $exception) { - throw new Exception("Request socket to reset Nextcloud server failed: " . $exception->getMessage()); - } finally { - socket_close($socket); + private function execOrException($command) { + exec($command . " 2>&1", $output, $returnValue); + if ($returnValue != 0) { + throw new Exception("'$command' could not be executed: " . implode("\n", $output)); } } -} + /** + * Kills the PHP built-in web server started in setUp, if any. + */ + private function killPhpServer() { + if ($this->phpServerPid == "") { + return; + } + + // execOrException is not used because the PID may no longer exist when + // trying to kill it. + exec("kill " . $this->phpServerPid); + + $this->phpServerPid = ""; + } } diff --git a/build/acceptance/run-drone.sh b/build/acceptance/run-drone.sh index 93e91c474c6..c90a79c95e0 100755 --- a/build/acceptance/run-drone.sh +++ b/build/acceptance/run-drone.sh @@ -22,11 +22,13 @@ # # The acceptance tests are written in Behat so, besides running the tests, this # script installs Behat, its dependencies, and some related packages in the -# "vendor" subdirectory of the acceptance tests. The acceptance tests also use -# the Selenium server to control a web browser, and they require a Nextcloud -# server to be available, so this script waits for the Selenium server and the -# Nextcloud server (both provided in their own Drone service) to be ready before -# running the tests. +# "vendor" subdirectory of the acceptance tests. The acceptance tests expect +# that the last commit in the Git repository provides the default state of the +# Nextcloud server, so the script installs the Nextcloud server and saves a +# snapshot of the whole grandparent directory (no .gitignore file is used) in +# the Git repository. Finally, the acceptance tests also use the Selenium server +# to control a web browser, so this script waits for the Selenium server +# (provided in its own Drone service) to be ready before running the tests. # Exit immediately on errors. set -o errexit @@ -50,26 +52,24 @@ ORIGINAL="\ REPLACEMENT="\ - NextcloudTestServerContext:\n\ nextcloudTestServerHelper: NextcloudTestServerDroneHelper\n\ - nextcloudTestServerHelperParameters:\n\ - - $NEXTCLOUD_SERVER_CONTROL_PORT" + nextcloudTestServerHelperParameters:" sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-drone.yml -# Both the Selenium server and the Nextcloud server control should be ready by -# now, as Composer typically takes way longer to execute than their startup -# (which is done in parallel in Drone services), but just in case. +cd ../../ +echo "Installing and configuring Nextcloud server" +build/acceptance/installAndConfigureServer.sh + +echo "Saving the default state so acceptance tests can reset to it" +find . -name ".gitignore" -exec rm --force {} \; +git add --all && echo 'Default state' | git -c user.name='John Doe' -c user.email='john@doe.org' commit --quiet --file=- + +cd build/acceptance + +# The Selenium server should be ready by now, as Composer typically takes way +# longer to execute than its startup (which is done in parallel in a Drone +# service), but just in case. echo "Waiting for Selenium" timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done" -# This just checks if it can connect to the port in which the Nextcloud server -# control should be listening on. -NEXTCLOUD_SERVER_CONTROL_PORT="12345" -PHP_CHECK_NEXTCLOUD_SERVER="\ -if ((\\\$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) { exit(1); } \ -if (socket_connect(\\\$socket, \\\"127.0.0.1\\\", \\\"$NEXTCLOUD_SERVER_CONTROL_PORT\\\") === false) { exit(1); } \ -socket_close(\\\$socket);" - -echo "Waiting for Nextcloud server control" -timeout 60s bash -c "while ! php -r \"$PHP_CHECK_NEXTCLOUD_SERVER\" >/dev/null 2>&1; do sleep 1; done" - vendor/bin/behat --config=config/behat-drone.yml $SCENARIO_TO_RUN From bbe479bcd94ea9bdcebe973b17487c5ca015934b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 19 Apr 2017 07:58:18 +0200 Subject: [PATCH 24/31] Generalize names and descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .drone.yml | 4 ++-- ...er.php => NextcloudTestServerLocalHelper.php} | 8 ++++---- build/acceptance/{run-drone.sh => run-local.sh} | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) rename build/acceptance/features/core/{NextcloudTestServerDroneHelper.php => NextcloudTestServerLocalHelper.php} (94%) rename build/acceptance/{run-drone.sh => run-local.sh} (86%) diff --git a/.drone.yml b/.drone.yml index 9996b5df5cf..b768fd3f1ea 100644 --- a/.drone.yml +++ b/.drone.yml @@ -481,14 +481,14 @@ pipeline: acceptance-access-levels: image: nextcloudci/php7.0:php7.0-7 commands: - - build/acceptance/run-drone.sh features/access-levels.feature + - build/acceptance/run-local.sh features/access-levels.feature when: matrix: TESTS-ACCEPTANCE: access-levels acceptance-login: image: nextcloudci/php7.0:php7.0-7 commands: - - build/acceptance/run-drone.sh features/login.feature + - build/acceptance/run-local.sh features/login.feature when: matrix: TESTS-ACCEPTANCE: login diff --git a/build/acceptance/features/core/NextcloudTestServerDroneHelper.php b/build/acceptance/features/core/NextcloudTestServerLocalHelper.php similarity index 94% rename from build/acceptance/features/core/NextcloudTestServerDroneHelper.php rename to build/acceptance/features/core/NextcloudTestServerLocalHelper.php index 23f6db2df97..32b5330c61a 100644 --- a/build/acceptance/features/core/NextcloudTestServerDroneHelper.php +++ b/build/acceptance/features/core/NextcloudTestServerLocalHelper.php @@ -22,8 +22,8 @@ */ /** - * Helper to manage a Nextcloud test server when acceptance tests are run in a - * Drone step. + * Helper to manage a Nextcloud test server started directly by the acceptance + * tests themselves using the PHP built-in web server. * * The Nextcloud test server is executed using the PHP built-in web server * directly from the grandparent directory of the acceptance tests directory @@ -40,7 +40,7 @@ * by running "occ maintenance:install"). The base URL to access the Nextcloud * server can be got from "getBaseUrl". */ -class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { +class NextcloudTestServerLocalHelper implements NextcloudTestServerHelper { /** * @var string @@ -48,7 +48,7 @@ class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper { private $phpServerPid; /** - * Creates a new NextcloudTestServerDroneHelper. + * Creates a new NextcloudTestServerLocalHelper. */ public function __construct() { $this->phpServerPid = ""; diff --git a/build/acceptance/run-drone.sh b/build/acceptance/run-local.sh similarity index 86% rename from build/acceptance/run-drone.sh rename to build/acceptance/run-local.sh index c90a79c95e0..900b0ba30a4 100755 --- a/build/acceptance/run-drone.sh +++ b/build/acceptance/run-local.sh @@ -18,7 +18,8 @@ # along with this program. If not, see . # Helper script to run the acceptance tests, which test a running Nextcloud -# instance from the point of view of a real user, in a Drone step. +# instance from the point of view of a real user, configured to start the +# Nextcloud server themselves and from their grandparent directory. # # The acceptance tests are written in Behat so, besides running the tests, this # script installs Behat, its dependencies, and some related packages in the @@ -28,7 +29,8 @@ # snapshot of the whole grandparent directory (no .gitignore file is used) in # the Git repository. Finally, the acceptance tests also use the Selenium server # to control a web browser, so this script waits for the Selenium server -# (provided in its own Drone service) to be ready before running the tests. +# (which should have been started before executing this script) to be ready +# before running the tests. # Exit immediately on errors. set -o errexit @@ -51,9 +53,9 @@ ORIGINAL="\ - NextcloudTestServerContext" REPLACEMENT="\ - NextcloudTestServerContext:\n\ - nextcloudTestServerHelper: NextcloudTestServerDroneHelper\n\ + nextcloudTestServerHelper: NextcloudTestServerLocalHelper\n\ nextcloudTestServerHelperParameters:" -sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-drone.yml +sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-local.yml cd ../../ @@ -66,10 +68,8 @@ git add --all && echo 'Default state' | git -c user.name='John Doe' -c user.emai cd build/acceptance -# The Selenium server should be ready by now, as Composer typically takes way -# longer to execute than its startup (which is done in parallel in a Drone -# service), but just in case. +# Ensure that the Selenium server is ready before running the tests. echo "Waiting for Selenium" timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done" -vendor/bin/behat --config=config/behat-drone.yml $SCENARIO_TO_RUN +vendor/bin/behat --config=config/behat-local.yml $SCENARIO_TO_RUN From 59004dc75e6ec8503d5162eb7a162019a13f6200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 19 Apr 2017 07:59:25 +0200 Subject: [PATCH 25/31] Run acceptance tests using the local helper instead of the Docker one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When run through "run.sh" the acceptance tests were executed in the same system in which the script was called and they started and stopped the Nextcloud server using Docker containers that provided real web servers. For consistency now they use the same approach used when run through Drone: the acceptance tests are run in a Docker container and they start and stop the Nextcloud server directly using the PHP built-in web server. Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/run.sh | 175 +++++++++++----------------------------- 1 file changed, 49 insertions(+), 126 deletions(-) diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh index eb8ce479769..7a394883d81 100755 --- a/build/acceptance/run.sh +++ b/build/acceptance/run.sh @@ -20,23 +20,20 @@ # Helper script to run the acceptance tests, which test a running Nextcloud # instance from the point of view of a real user. # -# The acceptance tests are written in Behat so, besides running the tests, this -# script installs Behat, its dependencies, and some related packages in the -# "vendor" subdirectory of the acceptance tests. The acceptance tests also use -# the Selenium server to control a web browser, so the Selenium server is also -# launched before the tests start in its own Docker container (it will be -# stopped automatically once the tests end). Finally, the tests expect that a -# Docker image with the Nextcloud installation to be tested is available, so the -# script creates it based on the Nextcloud code from the grandparent directory. +# The acceptance tests are run in its own Docker container; the grandparent +# directory of the acceptance tests directory (that is, the root directory of +# the Nextcloud server) is copied to the container and the acceptance tests are +# run inside it. Once the tests end the container is stopped. The acceptance +# tests also use the Selenium server to control a web browser, so the Selenium +# server is also launched before the tests start in its own Docker container (it +# will be stopped automatically too once the tests end). # -# To perform its job, the script requires the "composer" and "docker" commands -# to be available. +# To perform its job, the script requires the "docker" command to be available. # # The Docker Command Line Interface (the "docker" command) requires special # permissions to talk to the Docker daemon, and those permissions are typically -# available only to the root user. However, you should NOT run this script as -# root, but as a regular user instead. Please see the Docker documentation to -# find out how to give access to a regular user to the Docker daemon: +# available only to the root user. Please see the Docker documentation to find +# out how to give access to a regular user to the Docker daemon: # https://docs.docker.com/engine/installation/linux/linux-postinstall/ # # Note, however, that being able to communicate with the Docker daemon is the @@ -46,21 +43,10 @@ # https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface # # Finally, take into account that this script will automatically remove the -# Docker containers named "selenium-nextcloud-local-test-acceptance", -# "nextcloud-local-test-acceptance" and -# "nextcloud-local-test-acceptance-[0-9a-f.]*", and the Docker image tagged as -# "nextcloud-local-test-acceptance:latest", even if the script did not create -# them (probably you will not have containers nor images with those names, but -# just in case). - -# Installs Behat and its dependencies. -# -# Behat and its dependencies will be installed in the "vendor" subdirectory of -# the directory of the script. -function prepareBehat() { - echo "Installing Behat and dependencies" - composer install -} +# Docker containers named "selenium-nextcloud-local-test-acceptance" and +# "nextcloud-local-test-acceptance", even if the script did not create them +# (probably you will not have containers nor images with those names, but just +# in case). # Launches the Selenium server in a Docker container. # @@ -70,18 +56,12 @@ function prepareBehat() { # the latest one, but an older version known to work. # # The acceptance tests expect the Selenium server to be accessible at -# "127.0.0.1:4444", so the 4444 port of the container is mapped to the 4444 port -# of the host. -# -# The Nextcloud server has to be accessed at "127.0.0.1" by the Selenium server -# (as that is the only trusted domain by default), so the Nextcloud server -# containers have to be connected to the network of the Selenium server -# container (another option would be to connect the Selenium server to the host -# network, but messing with the host network is better avoided if possible). The -# acceptance tests themselves also need access to the Nextcloud server to ensure -# that it is ready before starting each scenario, so the 80 port of the Selenium -# server is mapped to the 80 port of the host (it is not possible to map the -# port in the container that connects to the network of another container). +# "127.0.0.1:4444"; as the Selenium server container and the container in which +# the acceptance tests are run share the same network nothing else needs to be +# done for the acceptance tests to access the Selenium server and for the +# Selenium server to access the Nextcloud server. However, in order to ensure +# from this script that the Selenium server was started the 4444 port of its +# container is mapped to the 4444 port of the host. # # Besides the Selenium server, the Docker image also provides a VNC server, so # the 5900 port of the container is also mapped to the 5900 port of the host. @@ -90,7 +70,7 @@ function prepareBehat() { # script exits (see cleanUp). If the Selenium server can not be started then the # script will be exited immediately with an error state; the most common cause # for the Selenium server to fail to start is that another server is already -# running in the default port. +# using the mapped ports in the host. # # As the web browser is run inside the Docker container it is not visible by # default. However, it can be viewed using VNC (for example, @@ -99,67 +79,37 @@ function prepareSelenium() { SELENIUM_CONTAINER=selenium-nextcloud-local-test-acceptance echo "Starting Selenium server" - docker run --detach --name=$SELENIUM_CONTAINER --publish 80:80 --publish 4444:4444 --publish 5900:5900 selenium/standalone-firefox-debug:2.53.1-beryllium + docker run --detach --name=$SELENIUM_CONTAINER --publish 4444:4444 --publish 5900:5900 selenium/standalone-firefox-debug:2.53.1-beryllium echo "Waiting for Selenium server to be ready" if ! timeout 10s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done"; then echo "Could not start Selenium server; running" \ - "\"docker run --rm --publish 80:80 --publish 4444:4444 --publish 5900:5900 selenium/standalone-firefox-debug:2.53.1-beryllium\"" \ + "\"docker run --rm --publish 4444:4444 --publish 5900:5900 selenium/standalone-firefox-debug:2.53.1-beryllium\"" \ "could give you a hint of the problem" exit 1 fi } -# Creates a Docker image to be used in Behat by NextcloudTestServerContext based -# on the local Nextcloud directory. +# Creates a Docker container to run both the acceptance tests and the Nextcloud +# server used by them. # -# NextcloudTestServerContext creates and destroys a Docker container for each -# acceptance test run, and the image that the container is created from must -# provide an installed copy of Nextcloud with certain configuration (like an -# "admin" user with an "admin" password, or local data storage). This function -# creates that Docker image based on the Nextcloud code from the grandparent -# directory, although ignoring any configuration or data that it may provide -# (for example, if that directory was used directly to deploy a Nextcloud -# instance in a web server). As the Nextcloud code is copied to the image -# instead of referenced the original code can be modified while the acceptance -# tests are running without interfering in them. -# -# Besides the Docker image to be used by the acceptance tests, which is removed -# automatically when the script exits, this function creates another image, -# that the other one will be based on, which is not removed when the script -# exits. Building this parent image could be a slow process, so it is kept built -# instead of removing it every time to speed up the launch of the acceptance -# tests. +# This function starts a Docker container with a copy the Nextcloud code from +# the grandparent directory, although ignoring any configuration or data that it +# may provide (for example, if that directory was used directly to deploy a +# Nextcloud instance in a web server). As the Nextcloud code is copied to the +# container instead of referenced the original code can be modified while the +# acceptance tests are running without interfering in them. function prepareDocker() { - NEXTCLOUD_LOCAL_IMAGE=nextcloud-local-test-acceptance NEXTCLOUD_LOCAL_CONTAINER=nextcloud-local-test-acceptance - # To create the Docker image to be used by the acceptance tests first a - # parent image is created. This parent image provides a system in which a - # Nextcloud server could be installed. Then, that parent image is run in a - # container in which the relevant code from the grandparent directory is - # copied; once the code is copied, the Nextcloud server is installed and - # configured as needed inside the container. Finally, the image to be used - # by the acceptance tests is generated by persisting the container to a new - # image. - # - # The image to be used by the acceptance tests could have been created just - # with a Dockerfile by adding the relevant code to the build context before - # starting the build and then using the ADD command in the Dockerfile (plus - # running the commands to install and configure the server as needed). In - # fact, standard Docker practices favor the creation of images through - # Dockerfiles to get a reproducible build. However, in this case I felt that - # it would go against that reproducible spirit of Dockerfiles, as an - # additional .tar file would have to be explicitly created each time before - # building the image, and that file would probably be different between - # different builds, thus resulting in a different image each time. Therefore - # I think that the current approach is better suited for this scenario. - - echo "Building Docker parent image" - docker build --tag $NEXTCLOUD_LOCAL_IMAGE:parent - < docker/nextcloud-local-parent/Dockerfile - - docker run --detach --name=$NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_IMAGE:parent + echo "Starting the Nextcloud container" + # As the Nextcloud server container uses the network of the Selenium server + # container the Nextcloud server can be accessed at "127.0.0.1" from the + # Selenium server. + # The container exits immediately if no command is given, so a Bash session + # is created to prevent that. + docker run --detach --name=$NEXTCLOUD_LOCAL_CONTAINER --network=container:$SELENIUM_CONTAINER --interactive --tty nextcloudci/php7.0:php7.0-7 bash # Use the $TMPDIR or, if not set, fall back to /tmp. NEXTCLOUD_LOCAL_TAR="$(mktemp --tmpdir="${TMPDIR:-/tmp}" --suffix=.tar nextcloud-local-XXXXXXXXXX)" @@ -168,24 +118,16 @@ function prepareDocker() { # "docker cp" does not take them into account (the extracted files are set # to root). echo "Copying local Git working directory of Nextcloud to the container" - tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./build" --exclude="./config/config.php" --exclude="./data" --exclude="./tests" --directory=../../ . - tar --append --file="$NEXTCLOUD_LOCAL_TAR" --directory=../../ build/acceptance/installAndConfigureServer.sh + tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./config/config.php" --exclude="./data" --exclude="./tests" --directory=../../ . - docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/var/www/html/ < "$NEXTCLOUD_LOCAL_TAR" - docker exec $NEXTCLOUD_LOCAL_CONTAINER chown -R www-data:www-data /var/www/html/ + docker exec $NEXTCLOUD_LOCAL_CONTAINER mkdir /nextcloud + docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/nextcloud/ < "$NEXTCLOUD_LOCAL_TAR" - echo "Installing Nextcloud in the container" - docker exec --user www-data $NEXTCLOUD_LOCAL_CONTAINER build/acceptance/installAndConfigureServer.sh - - echo "Creating Docker image to be used in acceptance tests" - docker commit --message "Nextcloud installed from the local Git working directory" $NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_IMAGE - - # Once the image to be used by the acceptance tests is created the container - # is no longer needed, so it can be stopped and removed. - docker stop $NEXTCLOUD_LOCAL_CONTAINER - # Although the parent Nextcloud image does not define a volume "--volumes" - # is used anyway just in case any of its ancestor images does. - docker rm --volumes $NEXTCLOUD_LOCAL_CONTAINER + # run-local.sh expects a Git repository to be available in the root of the + # Nextcloud server, but it was excluded when the Git working directory was + # copied to the container to avoid copying the large and unneeded history of + # the repository. + docker exec $NEXTCLOUD_LOCAL_CONTAINER bash -c "cd nextcloud && git init" } # Removes/stops temporal elements created/started by this script. @@ -202,8 +144,6 @@ function cleanUp() { rm $NEXTCLOUD_LOCAL_TAR fi - # If the script run successfully the container should have already been - # removed; this is needed only when an error happened. # The name filter must be specified as "^/XXX$" to get an exact match; using # just "XXX" would match every name that contained "XXX". if [ -n "$(docker ps --all --quiet --filter name="^/$NEXTCLOUD_LOCAL_CONTAINER$")" ]; then @@ -211,21 +151,6 @@ function cleanUp() { docker rm --volumes --force $NEXTCLOUD_LOCAL_CONTAINER fi - # In case of failure (like calling a method that does not exist on an - # object) the tests would be aborted without removing the containers created - # by NextcloudTestServerContext; if that happens those dangling containers - # are removed here. - DANGLING_CONTAINERS_CREATED_BY_ACCEPTANCE_TESTS="$(docker ps --all --quiet --filter name="^/$NEXTCLOUD_LOCAL_CONTAINER-[0-9a-f.]*$" --filter ancestor="$NEXTCLOUD_LOCAL_IMAGE:parent")" - if [ -n "$DANGLING_CONTAINERS_CREATED_BY_ACCEPTANCE_TESTS" ]; then - echo "Removing Docker containers matching $NEXTCLOUD_LOCAL_CONTAINER-[0-9a-f.]*" - docker rm --volumes --force $DANGLING_CONTAINERS_CREATED_BY_ACCEPTANCE_TESTS - fi - - if [ -n "$(docker images --quiet $NEXTCLOUD_LOCAL_IMAGE:latest)" ]; then - echo "Removing Docker image $NEXTCLOUD_LOCAL_IMAGE:latest" - docker rmi $NEXTCLOUD_LOCAL_IMAGE:latest - fi - if [ -n "$(docker ps --all --quiet --filter name="^/$SELENIUM_CONTAINER$")" ]; then echo "Removing Docker container $SELENIUM_CONTAINER" docker rm --volumes --force $SELENIUM_CONTAINER @@ -238,17 +163,15 @@ set -o errexit # Execute cleanUp when the script exits, either normally or due to an error. trap cleanUp EXIT -# Ensure working directory is script directory, as some actions (like installing -# Behat through Composer or generating the Nextcloud image for Docker) expect -# that. +# Ensure working directory is script directory, as some actions (like copying +# the Git working directory to the container) expect that. cd "$(dirname $0)" # If no parameter is provided to this script all the acceptance tests are run. SCENARIO_TO_RUN=$1 -prepareBehat prepareSelenium prepareDocker echo "Running tests" -vendor/bin/behat $SCENARIO_TO_RUN +docker exec $NEXTCLOUD_LOCAL_CONTAINER bash -c "cd nextcloud && build/acceptance/run-local.sh $SCENARIO_TO_RUN" From eea015c1ba714db783c7990edc935ca3b4cf62b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 19 Apr 2017 08:05:40 +0200 Subject: [PATCH 26/31] Change default configuration to use local helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../core/NextcloudTestServerContext.php | 21 +++++++------------ build/acceptance/run-local.sh | 16 +------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/build/acceptance/features/core/NextcloudTestServerContext.php index 4e0ba05bde9..f8d13a656b9 100644 --- a/build/acceptance/features/core/NextcloudTestServerContext.php +++ b/build/acceptance/features/core/NextcloudTestServerContext.php @@ -40,13 +40,12 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; * * The Nextcloud server is provided by an instance of NextcloudTestServerHelper; * its class must be specified when this context is created. By default, - * "NextcloudTestServerDockerHelper" is used, although that can be customized + * "NextcloudTestServerLocalHelper" is used, although that can be customized * using the "nextcloudTestServerHelper" parameter in "behat.yml". In the same * way, the parameters to be passed to the helper when it is created can be * customized using the "nextcloudTestServerHelperParameters" parameter, which * is an array (without keys) with the value of the parameters in the same order - * as in the constructor of the helper class (by default, - * [ "nextcloud-local-test-acceptance", "selenium-nextcloud-local-test-acceptance" ]). + * as in the constructor of the helper class (by default, [ ]). * * Example of custom parameters in "behat.yml": * default: @@ -54,16 +53,10 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; * default: * contexts: * - NextcloudTestServerContext: - * nextcloudTestServerHelper: NextcloudTestServerDockerHelper + * nextcloudTestServerHelper: NextcloudTestServerCustomHelper * nextcloudTestServerHelperParameters: - * - nextcloud-local-test-acceptance-custom-image - * - selenium-nextcloud-local-test-acceptance-custom-image - * - * Note that using Docker containers as a regular user requires giving access to - * the Docker daemon to that user. Unfortunately, that makes possible for that - * user to get root privileges for the system. Please see the - * NextcloudTestServerDockerHelper documentation for further information on this - * issue. + * - first-parameter-value + * - second-parameter-value */ class NextcloudTestServerContext implements Context { @@ -80,8 +73,8 @@ class NextcloudTestServerContext implements Context { * @param array $nextcloudTestServerHelperParameters the parameters for the * constructor of the $nextcloudTestServerHelper class. */ - public function __construct($nextcloudTestServerHelper = "NextcloudTestServerDockerHelper", - $nextcloudTestServerHelperParameters = [ "nextcloud-local-test-acceptance", "selenium-nextcloud-local-test-acceptance" ]) { + public function __construct($nextcloudTestServerHelper = "NextcloudTestServerLocalHelper", + $nextcloudTestServerHelperParameters = [ ]) { $nextcloudTestServerHelperClass = new ReflectionClass($nextcloudTestServerHelper); if ($nextcloudTestServerHelperParameters === null) { diff --git a/build/acceptance/run-local.sh b/build/acceptance/run-local.sh index 900b0ba30a4..a235871624e 100755 --- a/build/acceptance/run-local.sh +++ b/build/acceptance/run-local.sh @@ -43,20 +43,6 @@ SCENARIO_TO_RUN=$1 composer install -# Although Behat documentation states that using the BEHAT_PARAMS environment -# variable "You can set any value for any option that is available in a -# behat.yml file" this is currently not true for the constructor parameters of -# contexts (see https://github.com/Behat/Behat/issues/983). Thus, the default -# "behat.yml" configuration file has to be adjusted to provide the appropriate -# parameters for NextcloudTestServerContext. -ORIGINAL="\ - - NextcloudTestServerContext" -REPLACEMENT="\ - - NextcloudTestServerContext:\n\ - nextcloudTestServerHelper: NextcloudTestServerLocalHelper\n\ - nextcloudTestServerHelperParameters:" -sed "s/$ORIGINAL/$REPLACEMENT/" config/behat.yml > config/behat-local.yml - cd ../../ echo "Installing and configuring Nextcloud server" @@ -72,4 +58,4 @@ cd build/acceptance echo "Waiting for Selenium" timeout 60s bash -c "while ! curl 127.0.0.1:4444 >/dev/null 2>&1; do sleep 1; done" -vendor/bin/behat --config=config/behat-local.yml $SCENARIO_TO_RUN +vendor/bin/behat $SCENARIO_TO_RUN From 42fbf809fe25d8812a39d41f8d1352c34344c76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 19 Apr 2017 08:06:32 +0200 Subject: [PATCH 27/31] Remove no longer needed Docker helper and its related Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../docker/nextcloud-local-parent/Dockerfile | 52 ---- .../core/NextcloudTestServerDockerHelper.php | 226 ------------------ 2 files changed, 278 deletions(-) delete mode 100644 build/acceptance/docker/nextcloud-local-parent/Dockerfile delete mode 100644 build/acceptance/features/core/NextcloudTestServerDockerHelper.php diff --git a/build/acceptance/docker/nextcloud-local-parent/Dockerfile b/build/acceptance/docker/nextcloud-local-parent/Dockerfile deleted file mode 100644 index 5e2fd5277bd..00000000000 --- a/build/acceptance/docker/nextcloud-local-parent/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# This Dockerfile builds an image of a system in which a Nextcloud server could -# be installed. It is based on the Dockerfile for Nextcloud 11 on Apache -# (https://github.com/nextcloud/docker/blob/843d309ee62b9d2704e6141d2103f9ded97e35b6/11.0/apache/Dockerfile), -# although without the download and copy of a specific Nextcloud version; there -# is no volume either at "/var/www/html" to make possible to create a new image -# from a container based on this image that includes the installed Nextcloud -# server of the container (as the command to generate a new image from a -# container, "docker commit", does not include in the new image any data stored -# in volumes mounted inside the container). - -FROM php:7.1-apache - -RUN apt-get update && apt-get install -y \ - bzip2 \ - libcurl4-openssl-dev \ - libfreetype6-dev \ - libicu-dev \ - libjpeg-dev \ - libldap2-dev \ - libmcrypt-dev \ - libmemcached-dev \ - libpng12-dev \ - libpq-dev \ - libxml2-dev \ - && rm -rf /var/lib/apt/lists/* - -# https://docs.nextcloud.com/server/9/admin_manual/installation/source_installation.html -RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \ - && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \ - && docker-php-ext-install gd exif intl mbstring mcrypt ldap mysqli opcache pdo_mysql pdo_pgsql pgsql zip - -# set recommended PHP.ini settings -# see https://secure.php.net/manual/en/opcache.installation.php -RUN { \ - echo 'opcache.memory_consumption=128'; \ - echo 'opcache.interned_strings_buffer=8'; \ - echo 'opcache.max_accelerated_files=4000'; \ - echo 'opcache.revalidate_freq=60'; \ - echo 'opcache.fast_shutdown=1'; \ - echo 'opcache.enable_cli=1'; \ - } > /usr/local/etc/php/conf.d/opcache-recommended.ini -RUN a2enmod rewrite - -# PECL extensions -RUN set -ex \ - && pecl install APCu-5.1.8 \ - && pecl install memcached-3.0.2 \ - && pecl install redis-3.1.1 \ - && docker-php-ext-enable apcu redis memcached -RUN a2enmod rewrite - -CMD ["apache2-foreground"] diff --git a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php b/build/acceptance/features/core/NextcloudTestServerDockerHelper.php deleted file mode 100644 index d13efdb6b0a..00000000000 --- a/build/acceptance/features/core/NextcloudTestServerDockerHelper.php +++ /dev/null @@ -1,226 +0,0 @@ -. - * - */ - -/** - * Helper to manage the Docker container for the Nextcloud test server. - * - * The NextcloudTestServerDockerHelper provides a Nextcloud test server using a - * Docker container. The "setUp" method creates and starts the container, while - * the "cleanUp" method destroys it. A Docker image that provides an installed - * and ready to run Nextcloud server with the configuration and data expected by - * the acceptance tests must be available in the system. The Nextcloud server - * must use a local storage so all the changes it makes are confined to its - * running container. - * - * Also, the Nextcloud server installed in the Docker image is expected to see - * "127.0.0.1" as a trusted domain (which would be the case if it was installed - * by running "occ maintenance:install"). Therefore, the Nextcloud server - * container is connected to the network of the Selenium server container (which - * can be customized in the constructor) so the Selenium server can access the - * Nextcloud server using the "127.0.0.1" IP address. The Selenium server - * container is also expected to map its 80 port to the 80 port of the host so - * the acceptance tests can also access the Nextcloud server using the - * "127.0.0.1" IP address. In any case, the base URL to access the Nextcloud - * server can be got from "getBaseUrl". - * - * Internally, the NextcloudTestServerDockerHelper uses the Docker Command Line - * Interface (the "docker" command) to run, get information from, and destroy - * the container, For better compatibility, the used Docker CLI commands follow - * the pre-1.13 syntax (also available in 1.13 and newer). For example, - * "docker start" instead of "docker container start". - * - * In any case, the "docker" command requires special permissions to talk to the - * Docker daemon, and those permissions are typically available only to the root - * user. However, you should NOT run the acceptance tests as root, but as a - * regular user instead. Please see the Docker documentation to find out how to - * give access to a regular user to the Docker daemon: - * https://docs.docker.com/engine/installation/linux/linux-postinstall/ - * - * Note, however, that being able to communicate with the Docker daemon is the - * same as being able to get root privileges for the system. Therefore, you must - * give access to the Docker daemon (and thus run the acceptance tests as) ONLY - * to trusted and secure users: - * https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface - * - * All the public methods that use the 'docker' command throw an exception if - * the command can not be executed or if it does not have enough permissions to - * connect to the Docker daemon; as, due to the current use of this class, it is - * just a warning for the test runner and nothing to be explicitly catched a - * plain base Exception is used. - */ -class NextcloudTestServerDockerHelper implements NextcloudTestServerHelper { - - /** - * @var string - */ - private $imageName; - - /** - * @var string - */ - private $seleniumContainerName; - - /** - * @var string - */ - private $containerName; - - /** - * Creates a new NextcloudTestServerDockerHelper. - * - * @param string $imageName the name of the Docker image that provides the - * Nextcloud test server. - * @param string $seleniumContainerName the name of the Selenium server - * container. - */ - public function __construct($imageName = "nextcloud-local-test-acceptance", $seleniumContainerName = "selenium-nextcloud-local-test-acceptance") { - $this->imageName = $imageName; - $this->seleniumContainerName = $seleniumContainerName; - $this->containerName = null; - } - - /** - * Sets up the Nextcloud test server. - * - * It starts the Docker container and waits for its Nextcloud test server to - * be started; if the server does not start after some time an exception is - * thrown (as it is just a warning for the test runner and nothing to be - * explicitly catched a plain base Exception is used). - * - * @throws \Exception if the Docker container or its Nextcloud test server - * can not be started. - */ - public function setUp() { - $this->createAndStartContainer(); - - $timeout = 10; - if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) { - throw new Exception("Docker container for Nextcloud (" . $this->containerName . ") or its Nextcloud test server could not be started"); - } - } - - /** - * Creates and starts the container. - * - * Note that, even if the container has started, the server it contains may - * not have started yet when this method returns. - * - * @throws \Exception if the Docker command failed to execute. - */ - private function createAndStartContainer() { - $moreEntropy = true; - $this->containerName = uniqid($this->imageName . "-", $moreEntropy); - - // There is no need to start the web server as root, so it is started - // directly as www-data instead. - // The container is connected to the network of the Selenium server - // container; due to this, the Selenium server can access the Nextcloud - // server using the "127.0.0.1" IP address, which prevents Nextcloud - // from complaining that it is being accessed from an untrusted domain. - // Moreover, as the Selenium server container is expected to map its - // 80 port to the 80 port of the host the acceptance tests can also - // access the Nextcloud server using the "127.0.0.1" IP address to check - // whether the server is ready or not. - $this->executeDockerCommand("run --detach --user=www-data --network container:" . $this->seleniumContainerName . " --name=" . $this->containerName . " " . $this->imageName); - } - - /** - * Cleans up the Nextcloud test server. - * - * It stops and removes the Docker container; if the Docker container can - * not be removed after some time an exception is thrown (as it is just a - * warning for the test runner and nothing to be explicitly catched a plain - * base Exception is used). - * - * @throws \Exception if the Docker container can not be removed. - */ - public function cleanUp() { - $this->stopAndRemoveContainer(); - - $wasContainerRemovedCallback = function() { - return !$this->isContainerRegistered(); - }; - $timeout = 10; - $timeoutStep = 0.5; - if (!Utils::waitFor($wasContainerRemovedCallback, $timeout, $timeoutStep)) { - throw new Exception("Docker container for Nextcloud (" . $this->containerName . ") could not be removed"); - } - } - - /** - * Stops and removes the container. - * - * @throws \Exception if the Docker command failed to execute. - */ - private function stopAndRemoveContainer() { - // Although the Nextcloud image does not define a volume "--volumes" is - // used anyway just in case any of its ancestor images does. - $this->executeDockerCommand("rm --volumes --force " . $this->containerName); - } - - /** - * Returns whether the container exists (no matter its state) or not. - * - * @return boolean true if the container exists, false otherwise. - * @throws \Exception if the Docker command failed to execute. - */ - private function isContainerRegistered() { - // With the "--quiet" option "docker ps" only shows the ID of the - // matching containers, without table headers. Therefore, if the - // container does not exist the output will be empty (not even a new - // line, as the last line of output returned by "executeDockerCommand" - // does not include a trailing new line character). - return $this->executeDockerCommand("ps --all --quiet --filter 'name=" . $this->containerName . "'") !== ""; - } - - /** - * Returns the base URL of the Nextcloud test server. - * - * @return string the base URL of the Nextcloud test server. - * @throws \Exception if the Docker command failed to execute or the - * container is not running. - */ - public function getBaseUrl() { - return "http://127.0.0.1/index.php"; - } - - /** - * Executes the given Docker command. - * - * @return string the last line of output, without trailing new line - * character. - * @throws \Exception if the Docker command failed to execute. - */ - private function executeDockerCommand($dockerCommand) { - $output = array(); - $returnValue = 0; - $lastLine = exec("docker " . $dockerCommand . " 2>&1", $output, $returnValue); - - if ($returnValue !== 0) { - throw new Exception("Failed to execute 'docker " . $dockerCommand . "': " . implode("\n", $output)); - } - - return $lastLine; - } - -} From cccbd028a6974ed8d26f23d266a5ee5f545bf0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 19 Apr 2017 08:15:51 +0200 Subject: [PATCH 28/31] Add safety parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the script modifies the Git repository a safety parameter was added to prevent running it by mistake and messing with the local copy of the repository. Signed-off-by: Daniel Calviño Sánchez --- .drone.yml | 4 ++-- build/acceptance/run-local.sh | 10 +++++++++- build/acceptance/run.sh | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index b768fd3f1ea..60194f482c4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -481,14 +481,14 @@ pipeline: acceptance-access-levels: image: nextcloudci/php7.0:php7.0-7 commands: - - build/acceptance/run-local.sh features/access-levels.feature + - build/acceptance/run-local.sh allow-git-repository-modifications features/access-levels.feature when: matrix: TESTS-ACCEPTANCE: access-levels acceptance-login: image: nextcloudci/php7.0:php7.0-7 commands: - - build/acceptance/run-local.sh features/login.feature + - build/acceptance/run-local.sh allow-git-repository-modifications features/login.feature when: matrix: TESTS-ACCEPTANCE: login diff --git a/build/acceptance/run-local.sh b/build/acceptance/run-local.sh index a235871624e..bd5d6b09be1 100755 --- a/build/acceptance/run-local.sh +++ b/build/acceptance/run-local.sh @@ -39,7 +39,15 @@ set -o errexit # Behat through Composer or running Behat) expect that. cd "$(dirname $0)" -SCENARIO_TO_RUN=$1 +# Safety parameter to prevent executing this script by mistake and messing with +# the Git repository. +if [ "$1" != "allow-git-repository-modifications" ]; then + echo "To run the acceptance tests use \"run.sh\" instead" + + exit 1 +fi + +SCENARIO_TO_RUN=$2 composer install diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh index 7a394883d81..2a6efde74f3 100755 --- a/build/acceptance/run.sh +++ b/build/acceptance/run.sh @@ -174,4 +174,4 @@ prepareSelenium prepareDocker echo "Running tests" -docker exec $NEXTCLOUD_LOCAL_CONTAINER bash -c "cd nextcloud && build/acceptance/run-local.sh $SCENARIO_TO_RUN" +docker exec $NEXTCLOUD_LOCAL_CONTAINER bash -c "cd nextcloud && build/acceptance/run-local.sh allow-git-repository-modifications $SCENARIO_TO_RUN" From f89c16f83e33a38bf9a906b990c661b69eddd1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 21 Apr 2017 14:29:07 +0200 Subject: [PATCH 29/31] Exclude data-autotest from the files copied to the container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- build/acceptance/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/acceptance/run.sh b/build/acceptance/run.sh index 2a6efde74f3..8cb2bee621d 100755 --- a/build/acceptance/run.sh +++ b/build/acceptance/run.sh @@ -118,7 +118,7 @@ function prepareDocker() { # "docker cp" does not take them into account (the extracted files are set # to root). echo "Copying local Git working directory of Nextcloud to the container" - tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./config/config.php" --exclude="./data" --exclude="./tests" --directory=../../ . + tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./config/config.php" --exclude="./data" --exclude="./data-autotest" --exclude="./tests" --directory=../../ . docker exec $NEXTCLOUD_LOCAL_CONTAINER mkdir /nextcloud docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/nextcloud/ < "$NEXTCLOUD_LOCAL_TAR" From 2f80025ec25dd3ce19c0664f07073399e9bf99e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 21 Apr 2017 14:35:19 +0200 Subject: [PATCH 30/31] Move acceptance tests from build/acceptance to tests/acceptance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .drone.yml | 4 ++-- {build => tests}/acceptance/composer.json | 0 {build => tests}/acceptance/config/behat.yml | 0 {build => tests}/acceptance/features/access-levels.feature | 0 .../acceptance/features/bootstrap/FeatureContext.php | 0 .../acceptance/features/bootstrap/FilesAppContext.php | 0 .../acceptance/features/bootstrap/LoginPageContext.php | 0 .../acceptance/features/bootstrap/NotificationContext.php | 0 .../acceptance/features/bootstrap/SettingsMenuContext.php | 0 .../acceptance/features/bootstrap/UsersSettingsContext.php | 0 {build => tests}/acceptance/features/core/Actor.php | 0 {build => tests}/acceptance/features/core/ActorAware.php | 0 .../acceptance/features/core/ActorAwareInterface.php | 0 {build => tests}/acceptance/features/core/ActorContext.php | 0 {build => tests}/acceptance/features/core/Locator.php | 0 .../acceptance/features/core/NextcloudTestServerContext.php | 0 .../acceptance/features/core/NextcloudTestServerHelper.php | 0 .../features/core/NextcloudTestServerLocalHelper.php | 0 .../acceptance/features/core/NoSuchElementException.php | 0 {build => tests}/acceptance/features/core/Utils.php | 0 {build => tests}/acceptance/features/login.feature | 0 {build => tests}/acceptance/installAndConfigureServer.sh | 0 {build => tests}/acceptance/run-local.sh | 4 ++-- {build => tests}/acceptance/run.sh | 5 +++-- 24 files changed, 7 insertions(+), 6 deletions(-) rename {build => tests}/acceptance/composer.json (100%) rename {build => tests}/acceptance/config/behat.yml (100%) rename {build => tests}/acceptance/features/access-levels.feature (100%) rename {build => tests}/acceptance/features/bootstrap/FeatureContext.php (100%) rename {build => tests}/acceptance/features/bootstrap/FilesAppContext.php (100%) rename {build => tests}/acceptance/features/bootstrap/LoginPageContext.php (100%) rename {build => tests}/acceptance/features/bootstrap/NotificationContext.php (100%) rename {build => tests}/acceptance/features/bootstrap/SettingsMenuContext.php (100%) rename {build => tests}/acceptance/features/bootstrap/UsersSettingsContext.php (100%) rename {build => tests}/acceptance/features/core/Actor.php (100%) rename {build => tests}/acceptance/features/core/ActorAware.php (100%) rename {build => tests}/acceptance/features/core/ActorAwareInterface.php (100%) rename {build => tests}/acceptance/features/core/ActorContext.php (100%) rename {build => tests}/acceptance/features/core/Locator.php (100%) rename {build => tests}/acceptance/features/core/NextcloudTestServerContext.php (100%) rename {build => tests}/acceptance/features/core/NextcloudTestServerHelper.php (100%) rename {build => tests}/acceptance/features/core/NextcloudTestServerLocalHelper.php (100%) rename {build => tests}/acceptance/features/core/NoSuchElementException.php (100%) rename {build => tests}/acceptance/features/core/Utils.php (100%) rename {build => tests}/acceptance/features/login.feature (100%) rename {build => tests}/acceptance/installAndConfigureServer.sh (100%) rename {build => tests}/acceptance/run-local.sh (97%) rename {build => tests}/acceptance/run.sh (96%) diff --git a/.drone.yml b/.drone.yml index 60194f482c4..9b6a01bd4f0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -481,14 +481,14 @@ pipeline: acceptance-access-levels: image: nextcloudci/php7.0:php7.0-7 commands: - - build/acceptance/run-local.sh allow-git-repository-modifications features/access-levels.feature + - tests/acceptance/run-local.sh allow-git-repository-modifications features/access-levels.feature when: matrix: TESTS-ACCEPTANCE: access-levels acceptance-login: image: nextcloudci/php7.0:php7.0-7 commands: - - build/acceptance/run-local.sh allow-git-repository-modifications features/login.feature + - tests/acceptance/run-local.sh allow-git-repository-modifications features/login.feature when: matrix: TESTS-ACCEPTANCE: login diff --git a/build/acceptance/composer.json b/tests/acceptance/composer.json similarity index 100% rename from build/acceptance/composer.json rename to tests/acceptance/composer.json diff --git a/build/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml similarity index 100% rename from build/acceptance/config/behat.yml rename to tests/acceptance/config/behat.yml diff --git a/build/acceptance/features/access-levels.feature b/tests/acceptance/features/access-levels.feature similarity index 100% rename from build/acceptance/features/access-levels.feature rename to tests/acceptance/features/access-levels.feature diff --git a/build/acceptance/features/bootstrap/FeatureContext.php b/tests/acceptance/features/bootstrap/FeatureContext.php similarity index 100% rename from build/acceptance/features/bootstrap/FeatureContext.php rename to tests/acceptance/features/bootstrap/FeatureContext.php diff --git a/build/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php similarity index 100% rename from build/acceptance/features/bootstrap/FilesAppContext.php rename to tests/acceptance/features/bootstrap/FilesAppContext.php diff --git a/build/acceptance/features/bootstrap/LoginPageContext.php b/tests/acceptance/features/bootstrap/LoginPageContext.php similarity index 100% rename from build/acceptance/features/bootstrap/LoginPageContext.php rename to tests/acceptance/features/bootstrap/LoginPageContext.php diff --git a/build/acceptance/features/bootstrap/NotificationContext.php b/tests/acceptance/features/bootstrap/NotificationContext.php similarity index 100% rename from build/acceptance/features/bootstrap/NotificationContext.php rename to tests/acceptance/features/bootstrap/NotificationContext.php diff --git a/build/acceptance/features/bootstrap/SettingsMenuContext.php b/tests/acceptance/features/bootstrap/SettingsMenuContext.php similarity index 100% rename from build/acceptance/features/bootstrap/SettingsMenuContext.php rename to tests/acceptance/features/bootstrap/SettingsMenuContext.php diff --git a/build/acceptance/features/bootstrap/UsersSettingsContext.php b/tests/acceptance/features/bootstrap/UsersSettingsContext.php similarity index 100% rename from build/acceptance/features/bootstrap/UsersSettingsContext.php rename to tests/acceptance/features/bootstrap/UsersSettingsContext.php diff --git a/build/acceptance/features/core/Actor.php b/tests/acceptance/features/core/Actor.php similarity index 100% rename from build/acceptance/features/core/Actor.php rename to tests/acceptance/features/core/Actor.php diff --git a/build/acceptance/features/core/ActorAware.php b/tests/acceptance/features/core/ActorAware.php similarity index 100% rename from build/acceptance/features/core/ActorAware.php rename to tests/acceptance/features/core/ActorAware.php diff --git a/build/acceptance/features/core/ActorAwareInterface.php b/tests/acceptance/features/core/ActorAwareInterface.php similarity index 100% rename from build/acceptance/features/core/ActorAwareInterface.php rename to tests/acceptance/features/core/ActorAwareInterface.php diff --git a/build/acceptance/features/core/ActorContext.php b/tests/acceptance/features/core/ActorContext.php similarity index 100% rename from build/acceptance/features/core/ActorContext.php rename to tests/acceptance/features/core/ActorContext.php diff --git a/build/acceptance/features/core/Locator.php b/tests/acceptance/features/core/Locator.php similarity index 100% rename from build/acceptance/features/core/Locator.php rename to tests/acceptance/features/core/Locator.php diff --git a/build/acceptance/features/core/NextcloudTestServerContext.php b/tests/acceptance/features/core/NextcloudTestServerContext.php similarity index 100% rename from build/acceptance/features/core/NextcloudTestServerContext.php rename to tests/acceptance/features/core/NextcloudTestServerContext.php diff --git a/build/acceptance/features/core/NextcloudTestServerHelper.php b/tests/acceptance/features/core/NextcloudTestServerHelper.php similarity index 100% rename from build/acceptance/features/core/NextcloudTestServerHelper.php rename to tests/acceptance/features/core/NextcloudTestServerHelper.php diff --git a/build/acceptance/features/core/NextcloudTestServerLocalHelper.php b/tests/acceptance/features/core/NextcloudTestServerLocalHelper.php similarity index 100% rename from build/acceptance/features/core/NextcloudTestServerLocalHelper.php rename to tests/acceptance/features/core/NextcloudTestServerLocalHelper.php diff --git a/build/acceptance/features/core/NoSuchElementException.php b/tests/acceptance/features/core/NoSuchElementException.php similarity index 100% rename from build/acceptance/features/core/NoSuchElementException.php rename to tests/acceptance/features/core/NoSuchElementException.php diff --git a/build/acceptance/features/core/Utils.php b/tests/acceptance/features/core/Utils.php similarity index 100% rename from build/acceptance/features/core/Utils.php rename to tests/acceptance/features/core/Utils.php diff --git a/build/acceptance/features/login.feature b/tests/acceptance/features/login.feature similarity index 100% rename from build/acceptance/features/login.feature rename to tests/acceptance/features/login.feature diff --git a/build/acceptance/installAndConfigureServer.sh b/tests/acceptance/installAndConfigureServer.sh similarity index 100% rename from build/acceptance/installAndConfigureServer.sh rename to tests/acceptance/installAndConfigureServer.sh diff --git a/build/acceptance/run-local.sh b/tests/acceptance/run-local.sh similarity index 97% rename from build/acceptance/run-local.sh rename to tests/acceptance/run-local.sh index bd5d6b09be1..ee7a4e6455c 100755 --- a/build/acceptance/run-local.sh +++ b/tests/acceptance/run-local.sh @@ -54,13 +54,13 @@ composer install cd ../../ echo "Installing and configuring Nextcloud server" -build/acceptance/installAndConfigureServer.sh +tests/acceptance/installAndConfigureServer.sh echo "Saving the default state so acceptance tests can reset to it" find . -name ".gitignore" -exec rm --force {} \; git add --all && echo 'Default state' | git -c user.name='John Doe' -c user.email='john@doe.org' commit --quiet --file=- -cd build/acceptance +cd tests/acceptance # Ensure that the Selenium server is ready before running the tests. echo "Waiting for Selenium" diff --git a/build/acceptance/run.sh b/tests/acceptance/run.sh similarity index 96% rename from build/acceptance/run.sh rename to tests/acceptance/run.sh index 8cb2bee621d..f9711cbb404 100755 --- a/build/acceptance/run.sh +++ b/tests/acceptance/run.sh @@ -118,7 +118,8 @@ function prepareDocker() { # "docker cp" does not take them into account (the extracted files are set # to root). echo "Copying local Git working directory of Nextcloud to the container" - tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./config/config.php" --exclude="./data" --exclude="./data-autotest" --exclude="./tests" --directory=../../ . + tar --create --file="$NEXTCLOUD_LOCAL_TAR" --exclude=".git" --exclude="./build" --exclude="./config/config.php" --exclude="./data" --exclude="./data-autotest" --exclude="./tests" --directory=../../ . + tar --append --file="$NEXTCLOUD_LOCAL_TAR" --directory=../../ tests/acceptance/ docker exec $NEXTCLOUD_LOCAL_CONTAINER mkdir /nextcloud docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/nextcloud/ < "$NEXTCLOUD_LOCAL_TAR" @@ -174,4 +175,4 @@ prepareSelenium prepareDocker echo "Running tests" -docker exec $NEXTCLOUD_LOCAL_CONTAINER bash -c "cd nextcloud && build/acceptance/run-local.sh allow-git-repository-modifications $SCENARIO_TO_RUN" +docker exec $NEXTCLOUD_LOCAL_CONTAINER bash -c "cd nextcloud && tests/acceptance/run-local.sh allow-git-repository-modifications $SCENARIO_TO_RUN" From e970b5261fd0ae126db788d514ab4c7770688356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 21 Apr 2017 14:47:44 +0200 Subject: [PATCH 31/31] Make test passwords valid for the password_policy app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As requested by Morris Jobke, the passwords in the acceptance tests were modified to make them valid both for a clean Nextcloud server and one with the password_policy app enabled. Signed-off-by: Daniel Calviño Sánchez --- .../acceptance/features/bootstrap/LoginPageContext.php | 2 +- tests/acceptance/features/login.feature | 10 +++++----- tests/acceptance/installAndConfigureServer.sh | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/features/bootstrap/LoginPageContext.php b/tests/acceptance/features/bootstrap/LoginPageContext.php index 55db8c33bb7..4b0672f652c 100644 --- a/tests/acceptance/features/bootstrap/LoginPageContext.php +++ b/tests/acceptance/features/bootstrap/LoginPageContext.php @@ -111,7 +111,7 @@ class LoginPageContext implements Context, ActorAwareInterface { */ public function iAmLoggedIn() { $this->featureContext->iVisitTheHomePage(); - $this->iLogInWithUserAndPassword("user0", "123456"); + $this->iLogInWithUserAndPassword("user0", "123456acb"); $this->filesAppContext->iSeeThatTheCurrentPageIsTheFilesApp(); } diff --git a/tests/acceptance/features/login.feature b/tests/acceptance/features/login.feature index c4cd2add8e6..e414209206e 100644 --- a/tests/acceptance/features/login.feature +++ b/tests/acceptance/features/login.feature @@ -2,7 +2,7 @@ Feature: login Scenario: log in with valid user and password Given I visit the Home page - When I log in with user user0 and password 123456 + When I log in with user user0 and password 123456acb Then I see that the current page is the Files app Scenario: try to log in with valid user and invalid password @@ -25,20 +25,20 @@ Feature: login Scenario: try to log in with invalid user Given I visit the Home page - When I log in with user unknownUser and password 123456 + When I log in with user unknownUser and password 123456acb Then I see that the current page is the Login page And I see that a wrong password message is shown Scenario: log in with invalid user once fixed by admin Given I act as John - And I can not log in with user unknownUser and password 123456 + And I can not log in with user unknownUser and password 123456acb When I act as Jane And I am logged in as the admin And I open the User settings - And I create user unknownUser with password 123456 + And I create user unknownUser with password 123456acb And I see that the list of users contains the user unknownUser And I act as John - And I log in with user unknownUser and password 123456 + And I log in with user unknownUser and password 123456acb Then I see that the current page is the Files app Scenario: log out diff --git a/tests/acceptance/installAndConfigureServer.sh b/tests/acceptance/installAndConfigureServer.sh index c41f03ece16..2fbdf821f77 100755 --- a/tests/acceptance/installAndConfigureServer.sh +++ b/tests/acceptance/installAndConfigureServer.sh @@ -27,4 +27,4 @@ set -o errexit php occ maintenance:install --admin-pass=admin -OC_PASS=123456 php occ user:add --password-from-env user0 +OC_PASS=123456acb php occ user:add --password-from-env user0