diff --git a/core/composer.json b/core/composer.json index e54ea55..019b29e 100644 --- a/core/composer.json +++ b/core/composer.json @@ -27,7 +27,9 @@ "zendframework/zend-feed": "2.3.*", "mikey179/vfsStream": "1.*", "stack/builder": "1.0.*", - "egulias/email-validator": "1.2.*" + "egulias/email-validator": "1.2.*", + "behat/mink": "~1.6", + "behat/mink-goutte-driver": "~1.1" }, "autoload": { "psr-4": { diff --git a/core/composer.lock b/core/composer.lock index 9c24eb3..aca1009 100644 --- a/core/composer.lock +++ b/core/composer.lock @@ -4,9 +4,171 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "c977649e8e1a8b93301fa83283672a06", + "hash": "00ac3034749aec468b8abbf5811ac9a3", "packages": [ { + "name": "behat/mink", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "090900a0049c441f1e072bbd837db4079b2250c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/090900a0049c441f1e072bbd837db4079b2250c5", + "reference": "090900a0049c441f1e072bbd837db4079b2250c5", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "~2.0" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Mink": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Web acceptance testing framework for PHP 5.3", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "time": "2014-09-26 09:25:05" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "aed8f4a596b79014a75254c3e337511c33e38cbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/aed8f4a596b79014a75254c3e337511c33e38cbd", + "reference": "aed8f4a596b79014a75254c3e337511c33e38cbd", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "php": ">=5.3.1", + "symfony/browser-kit": "~2.0", + "symfony/dom-crawler": "~2.0" + }, + "require-dev": { + "silex/silex": "~1.2" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Mink\\Driver": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "time": "2014-09-26 11:35:19" + }, + { + "name": "behat/mink-goutte-driver", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "2bf327b4166694ecaa8ae7f956cb6ae252ecf03e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/2bf327b4166694ecaa8ae7f956cb6ae252ecf03e", + "reference": "2bf327b4166694ecaa8ae7f956cb6ae252ecf03e", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0", + "php": ">=5.3.1" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Mink\\Driver": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "time": "2014-10-09 09:21:12" + }, + { "name": "doctrine/annotations", "version": "v1.2.1", "source": { @@ -571,6 +733,260 @@ "time": "2014-11-06 08:59:44" }, { + "name": "fabpot/goutte", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "794b196e76bdd37b5155cdecbad311f0a3b07625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/794b196e76bdd37b5155cdecbad311f0a3b07625", + "reference": "794b196e76bdd37b5155cdecbad311f0a3b07625", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "guzzle/http": "~3.1", + "php": ">=5.3.0", + "symfony/browser-kit": "~2.1", + "symfony/css-selector": "~2.1", + "symfony/dom-crawler": "~2.1", + "symfony/finder": "~2.1", + "symfony/process": "~2.1" + }, + "require-dev": { + "guzzle/plugin-history": "~3.1", + "guzzle/plugin-mock": "~3.1" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "Goutte": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/fabpot/Goutte", + "keywords": [ + "scraper" + ], + "time": "2014-10-09 15:52:51" + }, + { + "name": "guzzle/common", + "version": "v3.9.2", + "target-dir": "Guzzle/Common", + "source": { + "type": "git", + "url": "https://github.com/guzzle/common.git", + "reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/common/zipball/2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", + "reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/event-dispatcher": ">=2.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Common": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Common libraries used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "collection", + "common", + "event", + "exception" + ], + "time": "2014-08-11 04:32:36" + }, + { + "name": "guzzle/http", + "version": "v3.9.2", + "target-dir": "Guzzle/Http", + "source": { + "type": "git", + "url": "https://github.com/guzzle/http.git", + "reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/http/zipball/1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", + "reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", + "shasum": "" + }, + "require": { + "guzzle/common": "self.version", + "guzzle/parser": "self.version", + "guzzle/stream": "self.version", + "php": ">=5.3.2" + }, + "suggest": { + "ext-curl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Http": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "HTTP libraries used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "client", + "curl", + "http", + "http client" + ], + "time": "2014-08-11 04:32:36" + }, + { + "name": "guzzle/parser", + "version": "v3.9.2", + "target-dir": "Guzzle/Parser", + "source": { + "type": "git", + "url": "https://github.com/guzzle/parser.git", + "reference": "6874d171318a8e93eb6d224cf85e4678490b625c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/parser/zipball/6874d171318a8e93eb6d224cf85e4678490b625c", + "reference": "6874d171318a8e93eb6d224cf85e4678490b625c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Parser": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Interchangeable parsers used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "URI Template", + "cookie", + "http", + "message", + "url" + ], + "time": "2014-02-05 18:29:46" + }, + { + "name": "guzzle/stream", + "version": "v3.9.2", + "target-dir": "Guzzle/Stream", + "source": { + "type": "git", + "url": "https://github.com/guzzle/stream.git", + "reference": "60c7fed02e98d2c518dae8f97874c8f4622100f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/stream/zipball/60c7fed02e98d2c518dae8f97874c8f4622100f0", + "reference": "60c7fed02e98d2c518dae8f97874c8f4622100f0", + "shasum": "" + }, + "require": { + "guzzle/common": "self.version", + "php": ">=5.3.2" + }, + "suggest": { + "guzzle/http": "To convert Guzzle request objects to PHP streams" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Stream": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle stream wrapper component", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "component", + "stream" + ], + "time": "2014-05-01 21:36:02" + }, + { "name": "guzzlehttp/guzzle", "version": "5.0.3", "source": { @@ -1694,6 +2110,61 @@ "time": "2014-10-20 20:55:17" }, { + "name": "symfony/browser-kit", + "version": "v2.6.4", + "target-dir": "Symfony/Component/BrowserKit", + "source": { + "type": "git", + "url": "https://github.com/symfony/BrowserKit.git", + "reference": "2ecec44ed5047020c65dd6e4a4b2f3cf13ae3c04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/BrowserKit/zipball/2ecec44ed5047020c65dd6e4a4b2f3cf13ae3c04", + "reference": "2ecec44ed5047020c65dd6e4a4b2f3cf13ae3c04", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/dom-crawler": "~2.0,>=2.0.5" + }, + "require-dev": { + "symfony/css-selector": "~2.0,>=2.0.5", + "symfony/process": "~2.0,>=2.0.5" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\BrowserKit\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "http://symfony.com", + "time": "2015-01-03 08:01:59" + }, + { "name": "symfony/class-loader", "version": "v2.6.4", "target-dir": "Symfony/Component/ClassLoader", @@ -1915,6 +2386,59 @@ "time": "2015-01-25 04:39:26" }, { + "name": "symfony/dom-crawler", + "version": "v2.6.4", + "target-dir": "Symfony/Component/DomCrawler", + "source": { + "type": "git", + "url": "https://github.com/symfony/DomCrawler.git", + "reference": "26a9eb302decd828990e1015afaa11b78b016073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/DomCrawler/zipball/26a9eb302decd828990e1015afaa11b78b016073", + "reference": "26a9eb302decd828990e1015afaa11b78b016073", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/css-selector": "~2.3" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\DomCrawler\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "http://symfony.com", + "time": "2015-01-03 08:01:59" + }, + { "name": "symfony/event-dispatcher", "version": "v2.6.4", "target-dir": "Symfony/Component/EventDispatcher", @@ -1973,6 +2497,53 @@ "time": "2015-02-01 16:10:57" }, { + "name": "symfony/finder", + "version": "v2.6.4", + "target-dir": "Symfony/Component/Finder", + "source": { + "type": "git", + "url": "https://github.com/symfony/Finder.git", + "reference": "16513333bca64186c01609961a2bb1b95b5e1355" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Finder/zipball/16513333bca64186c01609961a2bb1b95b5e1355", + "reference": "16513333bca64186c01609961a2bb1b95b5e1355", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Finder\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Finder Component", + "homepage": "http://symfony.com", + "time": "2015-01-03 08:01:59" + }, + { "name": "symfony/http-foundation", "version": "v2.6.4", "target-dir": "Symfony/Component/HttpFoundation", diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index fee191a..4804713 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -106,7 +106,9 @@ * @see http://php.net/manual/reserved.variables.server.php * @see http://php.net/manual/function.time.php */ -define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']); +if (!defined('REQUEST_TIME')) { + define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']); +} /** * Regular expression to match PHP function names. @@ -134,7 +136,9 @@ * * This strips two levels of directories off the current directory. */ -define('DRUPAL_ROOT', dirname(dirname(__DIR__))); +if (!defined('DRUPAL_ROOT')) { + define('DRUPAL_ROOT', dirname(dirname(__DIR__))); +} /** * Returns the appropriate configuration directory. @@ -841,7 +845,9 @@ function drupal_valid_test_ua($new_prefix = NULL) { // Perform a basic check on the User-Agent HTTP request header first. Any // inbound request that uses the simpletest UA header needs to be validated. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) { + $http_user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : NULL; + $user_agent = isset($_COOKIE['SIMPLETEST_USER_AGENT']) ? $_COOKIE['SIMPLETEST_USER_AGENT'] : $http_user_agent; + if (isset($user_agent) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $user_agent, $matches)) { list(, $prefix, $time, $salt, $hmac) = $matches; $check_string = $prefix . ';' . $time . ';' . $salt; // Read the hash salt prepared by drupal_generate_test_ua(). diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index f98c64d..7901d78 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -301,7 +301,8 @@ function install_begin_request($class_loader, &$install_state) { // The user agent header is used to pass a database prefix in the request when // running tests. However, for security reasons, it is imperative that no // installation be permitted using such a prefix. - if ($install_state['interactive'] && strpos($request->server->get('HTTP_USER_AGENT'), 'simpletest') !== FALSE && !drupal_valid_test_ua()) { + $user_agent = $request->cookies->get('SIMPLETEST_USER_AGENT') ?: $request->server->get('HTTP_USER_AGENT'); + if ($install_state['interactive'] && strpos($user_agent, 'simpletest') !== FALSE && !drupal_valid_test_ua()) { header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden'); exit; } diff --git a/core/modules/simpletest/src/BrowserTestBase.php b/core/modules/simpletest/src/BrowserTestBase.php new file mode 100644 index 0000000..8b7192b --- /dev/null +++ b/core/modules/simpletest/src/BrowserTestBase.php @@ -0,0 +1,1342 @@ +mink = new Mink(); + $this->mink->registerSession('goutte', $session); + $this->mink->setDefaultSessionName('goutte'); + $this->registerSessions(); + return $session; + } + + /** + * Registers additional mink sessions. + * + * Tests wishing to use a different driver or change the default driver should + * override this method. + * + * @code + * // Register a new session that uses the MinkPonyDriver. + * $pony = new MinkPonyDriver(); + * $session = new Session($pony); + * $this->mink->registerSession('pony', $session); + * @endcode + */ + protected function registerSessions() {} + + /** + * {@inheritdoc} + */ + public function setUp() { + global $base_url; + parent::setUp(); + + // Get and set the domain of the environment we are running our test + // coverage against. + $base_url = getenv('SIMPLETEST_BASE_URL'); + if (!$base_url) { + throw new \InvalidArgumentException('You must provide a SIMPLETEST_BASE_URL environment variable to run PHPUnit based functional tests.'); + } + + // Setup $_SERVER variable. + $parsed_url = parse_url($base_url); + $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); + $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : ''; + $port = (isset($parsed_url['port']) ? $parsed_url['port'] : 80); + if ($path == '/') { + $path = ''; + } + // If the passed URL schema is 'https' then setup the $_SERVER variables + // properly so that testing will run under HTTPS. + if ($parsed_url['scheme'] === 'https') { + $_SERVER['HTTPS'] = 'on'; + } + $_SERVER['HTTP_HOST'] = $host; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_PORT'] = $port; + $_SERVER['SERVER_SOFTWARE'] = NULL; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['REQUEST_URI'] = $path .'/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['SCRIPT_NAME'] = $path .'/index.php'; + $_SERVER['SCRIPT_FILENAME'] = $path .'/index.php'; + $_SERVER['PHP_SELF'] = $path .'/index.php'; + $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; + + // Install drupal test site. + $this->prepareEnvironment(); + $this->installDrupal(); + + // Setup Mink. + $session = $this->initMink(); + + // In order to debug web tests you need to either set a cookie, have the + // Xdebug session in the URL or set an environment variable in case of CLI + // requests. If the developer listens to connection when running tests, by + // default the cookie is not forwarded to the client side, so you cannot + // debug the code running on the test site. In order to make debuggers work + // this bit of information is forwarded. Make sure that the debugger listens + // to at least three external connections. + $request = \Drupal::request(); + $cookie_params = $request->cookies; + if ($cookie_params->has('XDEBUG_SESSION')) { + $session->setCookie('XDEBUG_SESSION', $cookie_params->get('XDEBUG_SESSION')); + } + // For CLI requests, the information is stored in $_SERVER. + $server = $request->server; + if ($server->has('XDEBUG_CONFIG')) { + // $_SERVER['XDEBUG_CONFIG'] has the form "key1=value1 key2=value2 ...". + $pairs = explode(' ', $server->get('XDEBUG_CONFIG')); + foreach ($pairs as $pair) { + list($key, $value) = explode('=', $pair); + // Account for key-value pairs being separated by multiple spaces. + if (trim($key, ' ') == 'idekey') { + $session->setCookie('XDEBUG_SESSION', trim($value, ' ')); + } + } + } + } + + /** + * Ensures test files are deletable within file_unmanaged_delete_recursive(). + * + * Some tests chmod generated files to be read only. During + * BrowserTestBase::cleanupEnvironment() and other cleanup operations, + * these files need to get deleted too. + * + * @param string $path + * The file path. + */ + public static function filePreDeleteCallback($path) { + chmod($path, 0700); + } + + /** + * Clean up the simpletest environment. + */ + protected function cleanupEnvironment() { + // Remove all prefixed tables. + $original_connection_info = Database::getConnectionInfo('simpletest_original_default'); + $original_prefix = $original_connection_info['default']['prefix']['default']; + $test_connection_info = Database::getConnectionInfo('default'); + $test_prefix = $test_connection_info['default']['prefix']['default']; + if ($original_prefix != $test_prefix) { + $tables = Database::getConnection()->schema()->findTables($test_prefix . '%'); + $prefix_length = strlen($test_prefix); + foreach ($tables as $table) { + if (Database::getConnection()->schema()->dropTable(substr($table, $prefix_length))) { + unset($tables[$table]); + } + } + } + + // Delete test site directory. + file_unmanaged_delete_recursive($this->siteDirectory, array($this, 'filePreDeleteCallback')); + } + + /** + * {@inheritdoc} + */ + public function tearDown() { + parent::tearDown(); + + // Destroy the testing kernel. + if (isset($this->kernel)) { + $this->cleanupEnvironment(); + $this->kernel->shutdown(); + } + + // Ensure that internal logged in variable is reset. + $this->loggedInUser = FALSE; + + if ($this->mink) { + $this->mink->stopSessions(); + } + } + + /** + * Returns Mink session. + * + * @param string|null $name + * (optional) Name of the session OR active session will be used. + * + * @return \Behat\Mink\Session + * The active mink session object. + */ + public function getSession($name = NULL) { + return $this->mink->getSession($name); + } + + /** + * Returns Mink assert session. + * + * @param string|null $name + * (optional) Name of the session. Defaults to the active session. + * + * @return \Drupal\simpletest\WebAssert + * A new web-assert option for asserting the presence of elements with. + */ + public function assertSession($name = NULL) { + return new WebAssert($this->getSession($name)); + } + + /** + * Prepare for a request to testing site. + * + * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that + * is checked by drupal_valid_test_ua(). + * + * @see drupal_valid_test_ua() + */ + protected function prepareRequest() { + $session = $this->getSession(); + $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix)); + } + + /** + * Retrieves a Drupal path or an absolute path. + * + * @param string $path + * Drupal path or URL to load into internal browser + * @param array $options + * (optional) Options to be forwarded to the url generator. + * + * @return string + * The retrieved HTML string, also available as $this->getRawContent() + */ + protected function drupalGet($path, array $options = array()) { + $options['absolute'] = TRUE; + + // The URL generator service is not necessarily available yet; e.g., in + // interactive installer tests. + if ($this->container->has('url_generator')) { + $url = $this->container->get('url_generator')->generateFromPath($path, $options); + } + else { + $url = $this->getAbsoluteUrl($path); + } + $session = $this->getSession(); + + $this->prepareRequest(); + $session->visit($url); + $out = $session->getPage()->getContent(); + + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + + return $out; + } + + /** + * Takes a path and returns an absolute path. + * + * @param string $path + * A path from the internal browser content. + * + * @return string + * The $path with $base_url prepended, if necessary. + */ + protected function getAbsoluteUrl($path) { + global $base_url, $base_path; + + $parts = parse_url($path); + if (empty($parts['host'])) { + // Ensure that we have a string (and no xpath object). + $path = (string) $path; + // Strip $base_path, if existent. + $length = strlen($base_path); + if (substr($path, 0, $length) === $base_path) { + $path = substr($path, $length); + } + // Ensure that we have an absolute path. + if ($path[0] !== '/') { + $path = '/' . $path; + } + // Finally, prepend the $base_url. + $path = $base_url . $path; + } + return $path; + } + + /** + * Creates a user with a given set of permissions. + * + * @param array $permissions + * (optional) Array of permission names to assign to user. Note that the + * user always has the default permissions derived from the + * "authenticated users" role. + * @param string $name + * (optional) The user name. + * + * @return \Drupal\user\Entity\User|false + * A fully loaded user object with passRaw property, or FALSE if account + * creation fails. + */ + protected function drupalCreateUser(array $permissions = array(), $name = NULL) { + // Create a role with the given permission set, if any. + $rid = FALSE; + if ($permissions) { + $rid = $this->drupalCreateRole($permissions); + if (!$rid) { + return FALSE; + } + } + + // Create a user assigned to that role. + $edit = array(); + $edit['name'] = !empty($name) ? $name : $this->randomMachineName(); + $edit['mail'] = $edit['name'] . '@example.com'; + $edit['pass'] = user_password(); + $edit['status'] = 1; + if ($rid) { + $edit['roles'] = array($rid); + } + + $account = entity_create('user', $edit); + $account->save(); + + $this->assertNotNull($account->id(), String::format('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass']))); + if (!$account->id()) { + return FALSE; + } + + // Add the raw password so that we can log in as this user. + $account->passRaw = $edit['pass']; + return $account; + } + + /** + * Creates a role with specified permissions. + * + * @param array $permissions + * Array of permission names to assign to role. + * @param string $rid + * (optional) The role ID (machine name). Defaults to a random name. + * @param string $name + * (optional) The label for the role. Defaults to a random string. + * @param int $weight + * (optional) The weight for the role. Defaults NULL so that entity_create() + * sets the weight to maximum + 1. + * + * @return string + * Role ID of newly created role, or FALSE if role creation failed. + */ + protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) { + // Generate a random, lowercase machine name if none was passed. + if (!isset($rid)) { + $rid = strtolower($this->randomMachineName(8)); + } + // Generate a random label. + if (!isset($name)) { + // In the role UI role names are trimmed and random string can start or + // end with a space. + $name = trim($this->randomString(8)); + } + + // Check the all the permissions strings are valid. + if (!$this->checkPermissions($permissions)) { + return FALSE; + } + + // Create new role. + /* @var \Drupal\user\RoleInterface $role */ + $role = entity_create('user_role', array( + 'id' => $rid, + 'label' => $name, + )); + if (!is_null($weight)) { + $role->set('weight', $weight); + } + $result = $role->save(); + + $this->assertSame($result, SAVED_NEW, String::format('Created role ID @rid with name @name.', array( + '@name' => var_export($role->label(), TRUE), + '@rid' => var_export($role->id(), TRUE), + ))); + + if ($result === SAVED_NEW) { + // Grant the specified permissions to the role, if any. + if (!empty($permissions)) { + user_role_grant_permissions($role->id(), $permissions); + $assigned_permissions = entity_load('user_role', $role->id())->getPermissions(); + $missing_permissions = array_diff($permissions, $assigned_permissions); + if ($missing_permissions) { + $this->fail(String::format('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions)))); + } + } + return $role->id(); + } + else { + return FALSE; + } + } + + /** + * Generates a unique random string containing letters and numbers. + * + * Do not use this method when testing unvalidated user input. Instead, use + * \Drupal\simpletest\TestBase::randomString(). + * + * @param int $length + * (optional) Length of random string to generate. + * + * @return string + * Randomly generated unique string. + * + * @see \Drupal\Component\Utility\Random::name() + */ + public function randomMachineName($length = 8) { + return $this->getRandomGenerator()->name($length, TRUE); + } + + /** + * Generates a pseudo-random string of ASCII characters of codes 32 to 126. + * + * Do not use this method when special characters are not possible (e.g., in + * machine or file names that have already been validated); instead, use + * \Drupal\simpletest\TestBase::randomMachineName(). If $length is greater + * than 2 the random string will include at least one ampersand ('&') + * character to ensure coverage for special characters and avoid the + * introduction of random test failures. + * + * @param int $length + * (optional) Length of random string to generate. + * + * @return string + * Pseudo-randomly generated unique string including special characters. + * + * @see \Drupal\Component\Utility\Random::string() + */ + public function randomString($length = 8) { + if ($length < 3) { + return $this->getRandomGenerator()->string($length, TRUE, array($this, 'randomStringValidate')); + } + + // To prevent the introduction of random test failures, ensure that the + // returned string contains a character that needs to be escaped in HTML by + // injecting an ampersand into it. + $replacement_pos = floor($length / 2); + // Remove 1 from the length to account for the ampersand character. + $string = $this->getRandomGenerator()->string($length - 1, TRUE, array($this, 'randomStringValidate')); + return substr_replace($string, '&', $replacement_pos, 0); + } + + /** + * Checks whether a given list of permission names is valid. + * + * @param array $permissions + * The permission names to check. + * + * @return bool + * TRUE if the permissions are valid, FALSE otherwise. + */ + protected function checkPermissions(array $permissions) { + $available = array_keys(\Drupal::service('user.permissions')->getPermissions()); + $valid = TRUE; + foreach ($permissions as $permission) { + if (!in_array($permission, $available)) { + $this->fail(String::format('Invalid permission %permission.', array('%permission' => $permission))); + $valid = FALSE; + } + } + return $valid; + } + + /** + * Logs in a user using the mink browser. + * + * If a user is already logged in, then the current user is logged out before + * logging in the specified user. + * + * Please note that neither the current user nor the passed-in user object is + * populated with data of the logged in user. If you need full access to the + * user object after logging in, it must be updated manually. If you also need + * access to the plain-text password of the user (set by drupalCreateUser()), + * e.g. to log in the same user again, then it must be re-assigned manually. + * For example: + * @code + * // Create a user. + * $account = $this->drupalCreateUser(array()); + * $this->drupalLogin($account); + * // Load real user object. + * $pass_raw = $account->passRaw; + * $account = user_load($account->id()); + * $account->passRaw = $pass_raw; + * @endcode + * + * @param \Drupal\Core\Session\AccountInterface $account + * User object representing the user to log in. + * + * @see drupalCreateUser() + */ + protected function drupalLogin(AccountInterface $account) { + if ($this->loggedInUser) { + $this->drupalLogout(); + } + + $this->drupalGet('user'); + $this->assertSession()->statusCodeEquals(200); + $this->submitForm(array( + 'name' => $account->getUsername(), + 'pass' => $account->passRaw, + ), t('Log in')); + + // @see BrowserTestBase::drupalUserIsLoggedIn() + $account->sessionId = $this->getSession()->getCookie(session_name()); + $this->assertTrue($this->drupalUserIsLoggedIn($account), String::format('User %name successfully logged in.', array('name' => $account->getUsername()))); + + $this->loggedInUser = $account; + $this->container->get('current_user')->setAccount($account); + } + + /** + * Logs a user out of the internal browser and confirms. + * + * Confirms logout by checking the login page. + */ + protected function drupalLogout() { + // Make a request to the logout page, and redirect to the user page, the + // idea being if you were properly logged out you should be seeing a login + // screen. + $assert_session = $this->assertSession(); + $this->drupalGet('user/logout', array('query' => array('destination' => 'user'))); + $assert_session->statusCodeEquals(200); + $assert_session->fieldExists('name'); + $assert_session->fieldExists('pass'); + + // @see BrowserTestBase::drupalUserIsLoggedIn() + unset($this->loggedInUser->sessionId); + $this->loggedInUser = FALSE; + $this->container->get('current_user')->setAccount(new AnonymousUserSession()); + } + + /** + * Fills and submit a form. + * + * @param array $edit + * Field data in an associative array. Changes the current input fields + * (where possible) to the values indicated. + * + * A checkbox can be set to TRUE to be checked and should be set to FALSE to + * be unchecked. + * @param string $submit + * Value of the submit button whose click is to be emulated. For example, + * t('Save'). The processing of the request depends on this value. For + * example, a form may have one button with the value t('Save') and another + * button with the value t('Delete'), and execute different code depending + * on which one is clicked. + * @param string $form_html_id + * (optional) HTML ID of the form to be submitted. On some pages + * there are many identical forms, so just using the value of the submit + * button is not enough. For example: 'trigger-node-presave-assign-form'. + * Note that this is not the Drupal $form_id, but rather the HTML ID of the + * form, which is typically the same thing but with hyphens replacing the + * underscores. + */ + protected function submitForm($edit, $submit, $form_html_id = NULL) { + $assert_session = $this->assertSession(); + + // Get the form. + if (isset($form_html_id)) { + $form = $assert_session->elementExists('xpath', "//form[@id='" . $form_html_id . "']"); + $submit_button = $assert_session->buttonExists($submit, $form); + } + else { + $submit_button = $assert_session->buttonExists($submit); + $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button); + } + + // Edit the form values. + foreach ($edit as $name => $value) { + $field = $assert_session->fieldExists($name, $form); + $field->setValue($value); + } + + // Submit form. + $this->prepareRequest(); + $submit_button->press(); + + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + } + + /** + * Helper function to get the options of select field. + * + * @param NodeElement|string $select + * Name, ID, or Label of select field to assert. + * @param Element $container + * (optional) Container element to check against. Defaults to current page. + * + * @return array + * Associative array of option keys and values. + */ + protected function getOptions($select, Element $container = NULL) { + if (is_string($select)) { + $select = $this->assertSession()->selectExists($select, $container); + } + $options = []; + /* @var \Behat\Mink\Element\NodeElement $option */ + foreach ($select->findAll('xpath', '//option') as $option) { + $label = $option->getText(); + $value = $option->getAttribute('value') ?: $label; + $options[$value] = $label; + } + return $options; + } + + /** + * {@inheritdoc} + */ + public function run(\PHPUnit_Framework_TestResult $result = NULL) { + if ($result === NULL) { + $result = $this->createResult(); + } + + parent::run($result); + return $result; + } + + /** + * Override to use Mink exceptions. + * + * @return mixed + * Either a test result or NULL. + * @throws \PHPUnit_Framework_AssertionFailedError + * When exception was thrown inside the test. + */ + protected function runTest() { + try { + return parent::runTest(); + } + catch (Exception $e) { + throw new \PHPUnit_Framework_AssertionFailedError($e->getMessage()); + } + } + + /** + * Generates a unique random string containing letters and numbers. + * + * Do not use this method when testing unvalidated user input. Instead, use + * \Drupal\simpletest\TestBase::randomString(). + * + * @param int $length + * (optional) Length of random string to generate. + * + * @return string + * Randomly generated unique string. + * + * @see \Drupal\Component\Utility\Random::name() + */ + public function randomName($length = 8) { + return $this->getRandomGenerator()->name($length, TRUE); + } + + /** + * Gets the random generator for the utility methods. + * + * @return \Drupal\Component\Utility\Random + * The random generator + */ + protected function getRandomGenerator() { + if (!is_object($this->randomGenerator)) { + $this->randomGenerator = new Random(); + } + return $this->randomGenerator; + } + + /** + * Installs drupal into the simpletest site. + */ + public function installDrupal() { + // Define information about the user 1 account. + $this->rootUser = new UserSession(array( + 'uid' => 1, + 'name' => 'admin', + 'mail' => 'admin@example.com', + 'passRaw' => $this->randomName(), + )); + + // Some tests (SessionTest and SessionHttpsTest) need to examine whether the + // proper session cookies were set on a response. Because the child site + // uses the same session name as the test runner, it is necessary to make + // that available to test-methods. + $this->sessionName = session_name(); + + // Get parameters for install_drupal() before removing global variables. + $parameters = $this->installParameters(); + + // Prepare installer settings that are not install_drupal() parameters. + // Copy and prepare an actual settings.php, so as to resemble a regular + // installation. + // Not using File API; a potential error must trigger a PHP warning. + $directory = DRUPAL_ROOT . '/' . $this->siteDirectory; + copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php'); + copy(DRUPAL_ROOT . '/sites/default/default.services.yml', $directory . '/services.yml'); + + // All file system paths are created by System module during installation. + // @see system_requirements() + // @see TestBase::prepareEnvironment() + $settings['settings']['file_public_path'] = (object) array( + 'value' => $this->publicFilesDirectory, + 'required' => TRUE, + ); + $this->writeSettings($settings); + // Allow for test-specific overrides. + $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/settings.testing.php'; + if (file_exists($settings_testing_file)) { + // Copy the testing-specific settings.php overrides in place. + copy($settings_testing_file, $directory . '/settings.testing.php'); + // Add the name of the testing class to settings.php and include the + // testing specific overrides. + file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) . "';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' . "\n", FILE_APPEND); + } + $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/testing.services.yml'; + if (file_exists($settings_services_file)) { + // Copy the testing-specific service overrides in place. + copy($settings_services_file, $directory . '/services.yml'); + } + + // Since Drupal is bootstrapped already, install_begin_request() will not + // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to + // reload the newly written custom settings.php manually. + Settings::initialize(DRUPAL_ROOT, $directory, $this->classLoader); + + // Execute the non-interactive installer. + require_once DRUPAL_ROOT . '/core/includes/install.core.inc'; + install_drupal($parameters); + + // Import new settings.php written by the installer. + Settings::initialize(DRUPAL_ROOT, $directory, $this->classLoader); + foreach ($GLOBALS['config_directories'] as $type => $path) { + $this->configDirectories[$type] = $path; + } + + // After writing settings.php, the installer removes write permissions + // from the site directory. To allow drupal_generate_test_ua() to write + // a file containing the private key for drupal_valid_test_ua(), the site + // directory has to be writable. + // TestBase::restoreEnvironment() will delete the entire site directory. + // Not using File API; a potential error must trigger a PHP warning. + chmod($directory, 0777); + + $request = \Drupal::request(); + $this->kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod', TRUE); + $this->kernel->prepareLegacyRequest($request); + // Force the container to be built from scratch instead of loaded from the + // disk. This forces us to not accidently load the parent site. + $container = $this->kernel->rebuildContainer(); + + $config = $container->get('config.factory'); + + // Manually create and configure private and temporary files directories. + // While these could be preset/enforced in settings.php like the public + // files directory above, some tests expect them to be configurable in the + // UI. If declared in settings.php, they would no longer be configurable. + file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY); + $config->getEditable('system.file') + ->set('path.private', $this->privateFilesDirectory) + ->set('path.temporary', $this->tempFilesDirectory) + ->save(); + + // Manually configure the test mail collector implementation to prevent + // tests from sending out emails and collect them in state instead. + // While this should be enforced via settings.php prior to installation, + // some tests expect to be able to test mail system implementations. + $config->getEditable('system.mail') + ->set('interface.default', 'test_mail_collector') + ->save(); + + // By default, verbosely display all errors and disable all production + // environment optimizations for all tests to avoid needless overhead and + // ensure a sane default experience for test authors. + // @see https://drupal.org/node/2259167 + $config->getEditable('system.logging') + ->set('error_level', 'verbose') + ->save(); + $config->getEditable('system.performance') + ->set('css.preprocess', FALSE) + ->set('js.preprocess', FALSE) + ->save(); + + // Collect modules to install. + $class = get_class($this); + $modules = array(); + while ($class) { + if (property_exists($class, 'modules')) { + $modules = array_merge($modules, $class::$modules); + } + $class = get_parent_class($class); + } + if ($modules) { + $modules = array_unique($modules); + $success = $container->get('module_installer')->install($modules, TRUE); + $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules)))); + $this->rebuildContainer(); + } + + // Reset/rebuild all data structures after enabling the modules, primarily + // to synchronize all data structures and caches between the test runner and + // the child site. + // Affects e.g. file_get_stream_wrappers(). + // @see \Drupal\Core\DrupalKernel::bootCode() + // @todo Test-specific setUp() methods may set up further fixtures; find a + // way to execute this after setUp() is done, or to eliminate it entirely. + $this->resetAll(); + $this->kernel->prepareLegacyRequest($request); + } + + /** + * Returns the parameters that will be used when Simpletest installs Drupal. + * + * @see install_drupal() + * @see install_state_defaults() + */ + protected function installParameters() { + $connection_info = Database::getConnectionInfo(); + $driver = $connection_info['default']['driver']; + $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default']; + unset($connection_info['default']['driver']); + unset($connection_info['default']['namespace']); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + $parameters = array( + 'interactive' => FALSE, + 'parameters' => array( + 'profile' => $this->profile, + 'langcode' => 'en', + ), + 'forms' => array( + 'install_settings_form' => array( + 'driver' => $driver, + $driver => $connection_info['default'], + ), + 'install_configure_form' => array( + 'site_name' => 'Drupal', + 'site_mail' => 'simpletest@example.com', + 'account' => array( + 'name' => $this->rootUser->name, + 'mail' => $this->rootUser->getEmail(), + 'pass' => array( + 'pass1' => $this->rootUser->passRaw, + 'pass2' => $this->rootUser->passRaw, + ), + ), + // form_type_checkboxes_value() requires NULL instead of FALSE values + // for programmatic form submissions to disable a checkbox. + 'update_status_module' => array( + 1 => NULL, + 2 => NULL, + ), + ), + ), + ); + return $parameters; + } + + /** + * Generates a database prefix for running tests. + * + * The database prefix is used by prepareEnvironment() to setup a public files + * directory for the test to be run, which also contains the PHP error log, + * which is written to in case of a fatal error. Since that directory is based + * on the database prefix, all tests (even unit tests) need to have one, in + * order to access and read the error log. + * + * The generated database table prefix is used for the Drupal installation + * being performed for the test. It is also used by the cookie value of + * SIMPLETEST_USER_AGENT by the mink browser. During early Drupal bootstrap, + * the cookie is parsed, and if it matches, all database queries use + * the database table prefix that has been generated here. + * + * @see drupal_valid_test_ua() + * @see BrowserTestBase::prepareEnvironment() + */ + private function prepareDatabasePrefix() { + // Ensure that the generated test site directory does not exist already, + // which may happen with a large amount of concurrent threads and + // long-running tests. + do { + $suffix = mt_rand(100000, 999999); + $this->siteDirectory = 'sites/simpletest/' . $suffix; + $this->databasePrefix = 'simpletest' . $suffix; + } while (is_dir(DRUPAL_ROOT . '/' . $this->siteDirectory)); + } + + /** + * Changes the database connection to the prefixed one. + * + * @see BrowserTestBase::prepareEnvironment() + */ + private function changeDatabasePrefix() { + if (empty($this->databasePrefix)) { + $this->prepareDatabasePrefix(); + } + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + // Replace the full table prefix definition to ensure that no table + // prefixes of the test runner leak into the test. + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); + } + + /** + * Prepares the current environment for running the test. + * + * Also sets up new resources for the testing environment, such as the public + * filesystem and configuration directories. + * + * This method is private as it must only be called once by + * BrowserTestBase::setUp() (multiple invocations for the same test would have + * unpredictable consequences) and it must not be callable or overridable by + * test classes. + */ + protected function prepareEnvironment() { + // Bootstrap Drupal so we can use Drupal's built in functions. + $this->classLoader = require __DIR__ . '/../../../vendor/autoload.php'; + $request = Request::createFromGlobals(); + $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader); + // TestRunnerKernel expects the working directory to be DRUPAL_ROOT. + chdir(DRUPAL_ROOT); + $kernel->prepareLegacyRequest($request); + $this->prepareDatabasePrefix(); + + $this->originalSiteDirectory = $kernel->findSitePath($request); + + // Create test directory ahead of installation so fatal errors and debug + // information can be logged during installation process. + file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + + // Prepare filesystem directory paths. + $this->publicFilesDirectory = $this->siteDirectory . '/files'; + $this->privateFilesDirectory = $this->siteDirectory . '/private'; + $this->tempFilesDirectory = $this->siteDirectory . '/temp'; + $this->translationFilesDirectory = $this->siteDirectory . '/translations'; + + // Ensure the configImporter is refreshed for each test. + $this->configImporter = NULL; + + // Unregister all custom stream wrappers of the parent site. + $wrappers = \Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::ALL); + foreach ($wrappers as $scheme => $info) { + stream_wrapper_unregister($scheme); + } + + // Reset statics. + drupal_static_reset(); + + // Ensure there is no service container. + $this->container = NULL; + \Drupal::setContainer(NULL); + + // Unset globals. + unset($GLOBALS['config_directories']); + unset($GLOBALS['config']); + unset($GLOBALS['conf']); + + // Log fatal errors. + ini_set('log_errors', 1); + ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); + + // Change the database prefix. + $this->changeDatabasePrefix(); + + // After preparing the environment and changing the database prefix, we are + // in a valid test environment. + drupal_valid_test_ua($this->databasePrefix); + + // Reset settings. + new Settings(array( + // For performance, simply use the database prefix as hash salt. + 'hash_salt' => $this->databasePrefix, + )); + + drupal_set_time_limit($this->timeLimit); + } + + /** + * Returns the database connection to the site running Simpletest. + * + * @return \Drupal\Core\Database\Connection + * The database connection to use for inserting assertions. + */ + public static function getDatabaseConnection() { + // Check whether there is a test runner connection. + // @see run-tests.sh + try { + $connection = Database::getConnection('default', 'test-runner'); + } + catch (ConnectionNotDefinedException $e) { + // Check whether there is a backup of the original default connection. + // @see BrowserTestBase::prepareEnvironment() + try { + $connection = Database::getConnection('default', 'simpletest_original_default'); + } + catch (ConnectionNotDefinedException $e) { + // If BrowserTestBase::prepareEnvironment() or + // BrowserTestBase::restoreEnvironment() failed, the test-specific + // database connection does not exist yet/anymore, so fall back to the + // default of the (UI) test runner. + $connection = Database::getConnection('default', 'default'); + } + } + return $connection; + } + + /** + * Rewrites the settings.php file of the test site. + * + * @param array $settings + * An array of settings to write out, in the format expected by + * drupal_rewrite_settings(). + * + * @see drupal_rewrite_settings() + */ + protected function writeSettings(array $settings) { + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + $filename = $this->siteDirectory . '/settings.php'; + + error_log($filename); + + // system_requirements() removes write permissions from settings.php + // whenever it is invoked. + // Not using File API; a potential error must trigger a PHP warning. + chmod($filename, 0666); + drupal_rewrite_settings($settings, $filename); + } + + /** + * Rebuilds \Drupal::getContainer(). + * + * Use this to build a new kernel and service container. For example, when the + * list of enabled modules is changed via the internal browser, in which case + * the test process still contains an old kernel and service container with an + * old module list. + * + * @see BrowserTestBase::prepareEnvironment() + * @see BrowserTestBase::restoreEnvironment() + * + * @todo Fix https://www.drupal.org/node/2021959 so that module enable/disable + * changes are immediately reflected in \Drupal::getContainer(). Until then, + * tests can invoke this workaround when requiring services from newly + * enabled modules to be immediately available in the same request. + */ + protected function rebuildContainer() { + // Maintain the current global request object. + $request = \Drupal::request(); + // Rebuild the kernel and bring it back to a fully bootstrapped state. + $this->container = $this->kernel->rebuildContainer(); + + // Make sure the url generator has a request object, otherwise calls to + // $this->drupalGet() will fail. + $this->prepareRequestForGenerator(); + } + + /** + * Creates a mock request and sets it on the generator. + * + * This is used to manipulate how the generator generates paths during tests. + * It also ensures that calls to $this->drupalGet() will work when running + * from run-tests.sh because the url generator no longer looks at the global + * variables that are set there but relies on getting this information from a + * request object. + * + * @param bool $clean_urls + * Whether to mock the request using clean urls. + * @param array $override_server_vars + * An array of server variables to override. + * + * @return Request + * The mocked request object. + */ + protected function prepareRequestForGenerator($clean_urls = TRUE, $override_server_vars = array()) { + $request = Request::createFromGlobals(); + $server = $request->server->all(); + if (basename($server['SCRIPT_FILENAME']) != basename($server['SCRIPT_NAME'])) { + // We need this for when the test is executed by run-tests.sh. + // @todo Remove this once run-tests.sh has been converted to use a Request + // object. + $cwd = getcwd(); + $server['SCRIPT_FILENAME'] = $cwd . '/' . basename($server['SCRIPT_NAME']); + $base_path = rtrim($server['REQUEST_URI'], '/'); + } + else { + $base_path = $request->getBasePath(); + } + if ($clean_urls) { + $request_path = $base_path ? $base_path . '/user' : 'user'; + } + else { + $request_path = $base_path ? $base_path . '/index.php/user' : '/index.php/user'; + } + $server = array_merge($server, $override_server_vars); + + $request = Request::create($request_path, 'GET', array(), array(), array(), $server); + $this->container->get('request_stack')->push($request); + + // The request context is normally set by the router_listener from within + // its KernelEvents::REQUEST listener. In the simpletest parent site this + // event is not fired, therefore it is necessary to updated the request + // context manually here. + $this->container->get('router.request_context')->fromRequest($request); + + return $request; + } + + /** + * Resets all data structures after having enabled new modules. + * + * This method is called by \Drupal\simpletest\WebTestBase::setUp() after + * enabling the requested modules. It must be called again when additional + * modules are enabled later. + */ + protected function resetAll() { + // Clear all database and static caches and rebuild data structures. + drupal_flush_all_caches(); + $this->container = \Drupal::getContainer(); + + // Reset static variables and reload permissions. + $this->refreshVariables(); + } + + /** + * Refreshes in-memory configuration and state information. + * + * Useful after a page request is made that changes configuration or state in + * a different thread. + * + * In other words calling a settings page with $this->drupalPostForm() with a + * changed value would update configuration to reflect that change, but in the + * thread that made the call (thread running the test) the changed values + * would not be picked up. + * + * This method clears the cache and loads a fresh copy. + */ + protected function refreshVariables() { + // Clear the tag cache. + // @todo Replace drupal_static() usage within classes and provide a + // proper interface for invoking reset() on a cache backend: + // https://www.drupal.org/node/2311945. + drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache'); + drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::deletedTags'); + drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::invalidatedTags'); + foreach (Cache::getBins() as $backend) { + if (is_callable(array($backend, 'reset'))) { + $backend->reset(); + } + } + + $this->container->get('config.factory')->reset(); + $this->container->get('state')->resetCache(); + } + + /** + * Returns whether a given user account is logged in. + * + * @param \Drupal\user\UserInterface $account + * The user account object to check. + * + * @return bool + * Return TRUE if the user is logged in, FALSE otherwise. + */ + protected function drupalUserIsLoggedIn($account) { + if (!isset($account->sessionId)) { + return FALSE; + } + // The session ID is hashed before being stored in the database. + // @see \Drupal\Core\Session\SessionHandler::read() + return (bool) db_query("SELECT sid FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid AND u.default_langcode = 1 WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->sessionId)))->fetchField(); + } + +} diff --git a/core/modules/simpletest/src/Form/SimpletestTestForm.php b/core/modules/simpletest/src/Form/SimpletestTestForm.php index 03f16f5..5c8265a 100644 --- a/core/modules/simpletest/src/Form/SimpletestTestForm.php +++ b/core/modules/simpletest/src/Form/SimpletestTestForm.php @@ -179,6 +179,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { + global $base_url; // Test discovery does not run upon form submission. simpletest_classloader_register(); @@ -209,6 +210,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } } if (!empty($tests_list)) { + putenv('SIMPLETEST_BASE_URL=' . $base_url); $test_id = simpletest_run_tests($tests_list, 'drupal'); $form_state->setRedirect( 'simpletest.result_form', diff --git a/core/modules/simpletest/src/WebAssert.php b/core/modules/simpletest/src/WebAssert.php new file mode 100644 index 0000000..daea904 --- /dev/null +++ b/core/modules/simpletest/src/WebAssert.php @@ -0,0 +1,71 @@ +session->getPage(); + $node = $container->findButton($button); + + if (NULL === $node) { + throw new ElementNotFoundException($this->session, 'button', 'id|name|label|value', $button); + } + + return $node; + } + + /** + * Checks that specific select field exists on the current page. + * + * @param string $select + * One of id|name|label|value for the select field. + * @param \Behat\Mink\Element\TraversableElement $container + * The document to check against. + * + * @return \Behat\Mink\Element\NodeElement + * The matching element + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * When the element doesn't exist. + */ + public function selectExists($select, TraversableElement $container = NULL) { + $container = $container ?: $this->session->getPage(); + $node = $container->find('named', array( + 'select', + $this->session->getSelectorsHandler()->xpathLiteral($select), + )); + + if (NULL === $node) { + throw new ElementNotFoundException($this->session, 'select', 'id|name|label|value', $select); + } + + return $node; + } +} diff --git a/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php b/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php new file mode 100755 index 0000000..6f4c473 --- /dev/null +++ b/core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php @@ -0,0 +1,62 @@ +drupalCreateUser(); + $this->drupalLogin($account); + + // Visit a Drupal page that requires login. + $this->drupalGet('/test-page'); + $this->assertSession()->statusCodeEquals(200); + + // Test page contains some text. + $this->assertSession()->pageTextContains('Test page text.'); + } + + /** + * Tests basic form functionality. + */ + public function testForm() { + // Ensure the proper response code for a _form route. + $this->drupalGet('/form-test/object-builder'); + $this->assertSession()->statusCodeEquals(200); + + // Ensure the form and text field exist. + $this->assertSession()->elementExists('css', 'form#form-test-form-test-object'); + $this->assertSession()->fieldExists('bananas'); + + $edit = ['bananas' => 'green']; + $this->submitForm($edit, 'Save', 'form-test-form-test-object'); + + $config_factory = $this->container->get('config.factory'); + $value = $config_factory->get('form_test.object')->get('bananas'); + $this->assertSame('green', $value); + } + +} diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index aa4acfd..78f2f0e 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -6,9 +6,10 @@ + - + ./tests ./modules/*/tests ../modules @@ -20,6 +21,15 @@ ./modules/config/tests/config_test/src + + ./modules/*/tests/src/Functional + + ./vendor + + ./drush/tests + + ./modules/config/tests/config_test/src + @@ -32,3 +42,4 @@ + diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 4e35b28..041165e 100644 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -339,6 +339,17 @@ function simpletest_script_init() { } } + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { + $base_url = 'https://'; + } + else { + $base_url = 'http://'; + } + $base_url .= $host; + if ($path !== '') { + $base_url .= $path; + } + putenv('SIMPLETEST_BASE_URL=' . $base_url); $_SERVER['HTTP_HOST'] = $host; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['SERVER_ADDR'] = '127.0.0.1';