1<?php 2 3namespace Drupal\Tests\Core\Command; 4 5use Drupal\Core\Test\TestDatabase; 6use Drupal\Tests\BrowserTestBase; 7use GuzzleHttp\Client; 8use GuzzleHttp\Cookie\CookieJar; 9use PHPUnit\Framework\TestCase; 10use Symfony\Component\Process\PhpExecutableFinder; 11use Symfony\Component\Process\Process; 12 13/** 14 * Tests the quick-start commands. 15 * 16 * These tests are run in a separate process because they load Drupal code via 17 * an include. 18 * 19 * @runTestsInSeparateProcesses 20 * @preserveGlobalState disabled 21 * 22 * @group Command 23 */ 24class QuickStartTest extends TestCase { 25 26 /** 27 * The PHP executable path. 28 * 29 * @var string 30 */ 31 protected $php; 32 33 /** 34 * A test database object. 35 * 36 * @var \Drupal\Core\Test\TestDatabase 37 */ 38 protected $testDb; 39 40 /** 41 * The Drupal root directory. 42 * 43 * @var string 44 */ 45 protected $root; 46 47 /** 48 * {@inheritdoc} 49 */ 50 public function setUp() { 51 parent::setUp(); 52 $php_executable_finder = new PhpExecutableFinder(); 53 $this->php = $php_executable_finder->find(); 54 $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))); 55 chdir($this->root); 56 if (!is_writable("{$this->root}/sites/simpletest")) { 57 $this->markTestSkipped('This test requires a writable sites/simpletest directory'); 58 } 59 // Get a lock and a valid site path. 60 $this->testDb = new TestDatabase(); 61 include $this->root . '/core/includes/bootstrap.inc'; 62 } 63 64 /** 65 * {@inheritdoc} 66 */ 67 public function tearDown() { 68 if ($this->testDb) { 69 $test_site_directory = $this->root . DIRECTORY_SEPARATOR . $this->testDb->getTestSitePath(); 70 if (file_exists($test_site_directory)) { 71 // @todo use the tear down command from 72 // https://www.drupal.org/project/drupal/issues/2926633 73 // Delete test site directory. 74 $this->fileUnmanagedDeleteRecursive($test_site_directory, [ 75 BrowserTestBase::class, 76 'filePreDeleteCallback', 77 ]); 78 } 79 } 80 parent::tearDown(); 81 } 82 83 /** 84 * Tests the quick-start command. 85 */ 86 public function testQuickStartCommand() { 87 if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) { 88 $this->markTestSkipped(); 89 } 90 91 // Install a site using the standard profile to ensure the one time login 92 // link generation works. 93 94 $install_command = [ 95 $this->php, 96 'core/scripts/drupal', 97 'quick-start', 98 'standard', 99 "--site-name='Test site {$this->testDb->getDatabasePrefix()}'", 100 '--suppress-login', 101 ]; 102 $process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 103 $process->inheritEnvironmentVariables(); 104 $process->setTimeout(500); 105 $process->start(); 106 $guzzle = new Client(); 107 $port = FALSE; 108 while ($process->isRunning()) { 109 if (preg_match('/127.0.0.1:(\d+)/', $process->getOutput(), $match)) { 110 $port = $match[1]; 111 break; 112 } 113 // Wait for more output. 114 sleep(1); 115 } 116 // The progress bar uses STDERR to write messages. 117 $this->assertContains('Congratulations, you installed Drupal!', $process->getErrorOutput()); 118 $this->assertNotFalse($port, "Web server running on port $port"); 119 120 // Give the server a couple of seconds to be ready. 121 sleep(2); 122 $this->assertContains("127.0.0.1:$port/user/reset/1/", $process->getOutput()); 123 124 // Generate a cookie so we can make a request against the installed site. 125 define('DRUPAL_TEST_IN_CHILD_SITE', FALSE); 126 chmod($this->testDb->getTestSitePath(), 0755); 127 $cookieJar = CookieJar::fromArray([ 128 'SIMPLETEST_USER_AGENT' => drupal_generate_test_ua($this->testDb->getDatabasePrefix()), 129 ], '127.0.0.1'); 130 131 $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]); 132 $content = (string) $response->getBody(); 133 $this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content); 134 135 // Stop the web server. 136 $process->stop(); 137 } 138 139 /** 140 * Tests that the installer throws a requirement error on older PHP versions. 141 */ 142 public function testPhpRequirement() { 143 if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) >= 0) { 144 $this->markTestSkipped(); 145 } 146 147 $install_command = [ 148 $this->php, 149 'core/scripts/drupal', 150 'quick-start', 151 'standard', 152 "--site-name='Test site {$this->testDb->getDatabasePrefix()}'", 153 '--suppress-login', 154 ]; 155 $process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 156 $process->inheritEnvironmentVariables(); 157 $process->setTimeout(500); 158 $process->start(); 159 while ($process->isRunning()) { 160 // Wait for more output. 161 sleep(1); 162 } 163 164 $error_output = $process->getErrorOutput(); 165 $this->assertContains('Your PHP installation is too old.', $error_output); 166 $this->assertContains('Drupal requires at least PHP', $error_output); 167 $this->assertContains(DRUPAL_MINIMUM_SUPPORTED_PHP, $error_output); 168 169 // Stop the web server. 170 $process->stop(); 171 } 172 173 /** 174 * Tests the quick-start commands. 175 */ 176 public function testQuickStartInstallAndServerCommands() { 177 if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) { 178 $this->markTestSkipped(); 179 } 180 181 // Install a site. 182 $install_command = [ 183 $this->php, 184 'core/scripts/drupal', 185 'install', 186 'testing', 187 "--site-name='Test site {$this->testDb->getDatabasePrefix()}'", 188 ]; 189 $install_process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 190 $install_process->inheritEnvironmentVariables(); 191 $install_process->setTimeout(500); 192 $result = $install_process->run(); 193 // The progress bar uses STDERR to write messages. 194 $this->assertContains('Congratulations, you installed Drupal!', $install_process->getErrorOutput()); 195 $this->assertSame(0, $result); 196 197 // Run the PHP built-in webserver. 198 $server_command = [ 199 $this->php, 200 'core/scripts/drupal', 201 'server', 202 '--suppress-login', 203 ]; 204 $server_process = new Process($server_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 205 $server_process->inheritEnvironmentVariables(); 206 $server_process->start(); 207 $guzzle = new Client(); 208 $port = FALSE; 209 while ($server_process->isRunning()) { 210 if (preg_match('/127.0.0.1:(\d+)/', $server_process->getOutput(), $match)) { 211 $port = $match[1]; 212 break; 213 } 214 // Wait for more output. 215 sleep(1); 216 } 217 $this->assertEquals('', $server_process->getErrorOutput()); 218 $this->assertContains("127.0.0.1:$port/user/reset/1/", $server_process->getOutput()); 219 $this->assertNotFalse($port, "Web server running on port $port"); 220 221 // Give the server a couple of seconds to be ready. 222 sleep(2); 223 224 // Generate a cookie so we can make a request against the installed site. 225 define('DRUPAL_TEST_IN_CHILD_SITE', FALSE); 226 chmod($this->testDb->getTestSitePath(), 0755); 227 $cookieJar = CookieJar::fromArray([ 228 'SIMPLETEST_USER_AGENT' => drupal_generate_test_ua($this->testDb->getDatabasePrefix()), 229 ], '127.0.0.1'); 230 231 $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]); 232 $content = (string) $response->getBody(); 233 $this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content); 234 235 // Try to re-install over the top of an existing site. 236 $install_command = [ 237 $this->php, 238 'core/scripts/drupal', 239 'install', 240 'testing', 241 "--site-name='Test another site {$this->testDb->getDatabasePrefix()}'", 242 ]; 243 $install_process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 244 $install_process->inheritEnvironmentVariables(); 245 $install_process->setTimeout(500); 246 $result = $install_process->run(); 247 $this->assertContains('Drupal is already installed.', $install_process->getOutput()); 248 $this->assertSame(0, $result); 249 250 // Ensure the site name has not changed. 251 $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]); 252 $content = (string) $response->getBody(); 253 $this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content); 254 255 // Stop the web server. 256 $server_process->stop(); 257 } 258 259 /** 260 * Tests the install command with an invalid profile. 261 */ 262 public function testQuickStartCommandProfileValidation() { 263 // Install a site using the standard profile to ensure the one time login 264 // link generation works. 265 $install_command = [ 266 $this->php, 267 'core/scripts/drupal', 268 'quick-start', 269 'umami', 270 "--site-name='Test site {$this->testDb->getDatabasePrefix()}' --suppress-login", 271 ]; 272 $process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 273 $process->inheritEnvironmentVariables(); 274 $process->run(); 275 $this->assertContains('\'umami\' is not a valid install profile. Did you mean \'demo_umami\'?', $process->getErrorOutput()); 276 } 277 278 /** 279 * Tests the server command when there is no installation. 280 */ 281 public function testServerWithNoInstall() { 282 $server_command = [ 283 $this->php, 284 'core/scripts/drupal', 285 'server', 286 '--suppress-login', 287 ]; 288 $server_process = new Process($server_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]); 289 $server_process->inheritEnvironmentVariables(); 290 $server_process->run(); 291 $this->assertContains('No installation found. Use the \'install\' command.', $server_process->getErrorOutput()); 292 } 293 294 /** 295 * Deletes all files and directories in the specified path recursively. 296 * 297 * Note this method has no dependencies on Drupal core to ensure that the 298 * test site can be torn down even if something in the test site is broken. 299 * 300 * @param string $path 301 * A string containing either an URI or a file or directory path. 302 * @param callable $callback 303 * (optional) Callback function to run on each file prior to deleting it and 304 * on each directory prior to traversing it. For example, can be used to 305 * modify permissions. 306 * 307 * @return bool 308 * TRUE for success or if path does not exist, FALSE in the event of an 309 * error. 310 * 311 * @see file_unmanaged_delete_recursive() 312 */ 313 protected function fileUnmanagedDeleteRecursive($path, $callback = NULL) { 314 if (isset($callback)) { 315 call_user_func($callback, $path); 316 } 317 if (is_dir($path)) { 318 $dir = dir($path); 319 while (($entry = $dir->read()) !== FALSE) { 320 if ($entry == '.' || $entry == '..') { 321 continue; 322 } 323 $entry_path = $path . '/' . $entry; 324 $this->fileUnmanagedDeleteRecursive($entry_path, $callback); 325 } 326 $dir->close(); 327 328 return rmdir($path); 329 } 330 return unlink($path); 331 } 332 333} 334