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