UpdatePathTestBase.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. namespace Drupal\FunctionalTests\Update;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Site\Settings;
  5. use Drupal\Core\Test\TestRunnerKernel;
  6. use Drupal\Tests\BrowserTestBase;
  7. use Drupal\Core\Database\Database;
  8. use Drupal\Core\DependencyInjection\ContainerBuilder;
  9. use Drupal\Core\File\FileSystemInterface;
  10. use Drupal\Core\Language\Language;
  11. use Drupal\Core\Url;
  12. use Drupal\Tests\UpdatePathTestTrait;
  13. use Drupal\user\Entity\User;
  14. use Symfony\Component\DependencyInjection\Reference;
  15. use Symfony\Component\HttpFoundation\Request;
  16. /**
  17. * Provides a base class for writing an update test.
  18. *
  19. * To write an update test:
  20. * - Write the hook_update_N() implementations that you are testing.
  21. * - Create one or more database dump files, which will set the database to the
  22. * "before updates" state. Normally, these will add some configuration data to
  23. * the database, set up some tables/fields, etc.
  24. * - Create a class that extends this class.
  25. * - Ensure that the test is in the legacy group. Tests can have multiple
  26. * groups.
  27. * - In your setUp() method, point the $this->databaseDumpFiles variable to the
  28. * database dump files, and then call parent::setUp() to run the base setUp()
  29. * method in this class.
  30. * - In your test method, call $this->runUpdates() to run the necessary updates,
  31. * and then use test assertions to verify that the result is what you expect.
  32. * - In order to test both with a "bare" database dump as well as with a
  33. * database dump filled with content, extend your update path test class with
  34. * a new test class that overrides the bare database dump. Refer to
  35. * UpdatePathTestBaseFilledTest for an example.
  36. *
  37. * @ingroup update_api
  38. *
  39. * @see hook_update_N()
  40. */
  41. abstract class UpdatePathTestBase extends BrowserTestBase {
  42. use UpdatePathTestTrait {
  43. runUpdates as doRunUpdates;
  44. }
  45. /**
  46. * Modules to enable after the database is loaded.
  47. */
  48. protected static $modules = [];
  49. /**
  50. * The file path(s) to the dumped database(s) to load into the child site.
  51. *
  52. * The file system/tests/fixtures/update/drupal-8.bare.standard.php.gz is
  53. * normally included first -- this sets up the base database from a bare
  54. * standard Drupal installation.
  55. *
  56. * The file system/tests/fixtures/update/drupal-8.filled.standard.php.gz
  57. * can also be used in case we want to test with a database filled with
  58. * content, and with all core modules enabled.
  59. *
  60. * @var array
  61. */
  62. protected $databaseDumpFiles = [];
  63. /**
  64. * The install profile used in the database dump file.
  65. *
  66. * @var string
  67. */
  68. protected $installProfile = 'standard';
  69. /**
  70. * Flag that indicates whether the child site has been updated.
  71. *
  72. * @var bool
  73. */
  74. protected $upgradedSite = FALSE;
  75. /**
  76. * Array of errors triggered during the update process.
  77. *
  78. * @var array
  79. */
  80. protected $upgradeErrors = [];
  81. /**
  82. * Array of modules loaded when the test starts.
  83. *
  84. * @var array
  85. */
  86. protected $loadedModules = [];
  87. /**
  88. * Flag to indicate whether zlib is installed or not.
  89. *
  90. * @var bool
  91. */
  92. protected $zlibInstalled = TRUE;
  93. /**
  94. * Flag to indicate whether there are pending updates or not.
  95. *
  96. * @var bool
  97. */
  98. protected $pendingUpdates = TRUE;
  99. /**
  100. * The update URL.
  101. *
  102. * @var string
  103. */
  104. protected $updateUrl;
  105. /**
  106. * Disable strict config schema checking.
  107. *
  108. * The schema is verified at the end of running the update.
  109. *
  110. * @var bool
  111. */
  112. protected $strictConfigSchema = FALSE;
  113. /**
  114. * Constructs an UpdatePathTestCase object.
  115. *
  116. * @param $test_id
  117. * (optional) The ID of the test. Tests with the same id are reported
  118. * together.
  119. * @param array $data
  120. * (optional) The test case data. Defaults to none.
  121. * @param string $data_name
  122. * The test data name. Defaults to none.
  123. */
  124. public function __construct($test_id = NULL, array $data = [], $data_name = '') {
  125. parent::__construct($test_id, $data, $data_name);
  126. $this->zlibInstalled = function_exists('gzopen');
  127. }
  128. /**
  129. * Overrides WebTestBase::setUp() for update testing.
  130. *
  131. * The main difference in this method is that rather than performing the
  132. * installation via the installer, a database is loaded. Additional work is
  133. * then needed to set various things such as the config directories and the
  134. * container that would normally be done via the installer.
  135. */
  136. protected function setUp() {
  137. $request = Request::createFromGlobals();
  138. // Boot up Drupal into a state where calling the database API is possible.
  139. // This is used to initialize the database system, so we can load the dump
  140. // files.
  141. $autoloader = require $this->root . '/autoload.php';
  142. $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
  143. $kernel->loadLegacyIncludes();
  144. // Set the update url. This must be set here rather than in
  145. // self::__construct() or the old URL generator will leak additional test
  146. // sites. Additionally, we need to prevent the path alias processor from
  147. // running because we might not have a working alias system before running
  148. // the updates.
  149. $this->updateUrl = Url::fromRoute('system.db_update', [], ['path_processing' => FALSE]);
  150. $this->setupBaseUrl();
  151. // Install Drupal test site.
  152. $this->prepareEnvironment();
  153. $this->runDbTasks();
  154. // We are going to set a missing zlib requirement property for usage
  155. // during the performUpgrade() and tearDown() methods. Also set that the
  156. // tests failed.
  157. if (!$this->zlibInstalled) {
  158. parent::setUp();
  159. return;
  160. }
  161. $this->installDrupal();
  162. // Add the config directories to settings.php.
  163. $sync_directory = Settings::get('config_sync_directory');
  164. \Drupal::service('file_system')->prepareDirectory($sync_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
  165. // Ensure the default temp directory exist and is writable. The configured
  166. // temp directory may be removed during update.
  167. \Drupal::service('file_system')->prepareDirectory($this->tempFilesDirectory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
  168. // Set the container. parent::rebuildAll() would normally do this, but this
  169. // not safe to do here, because the database has not been updated yet.
  170. $this->container = \Drupal::getContainer();
  171. $this->replaceUser1();
  172. require_once $this->root . '/core/includes/update.inc';
  173. // Setup Mink.
  174. $this->initMink();
  175. // Set up the browser test output file.
  176. $this->initBrowserOutputFile();
  177. }
  178. /**
  179. * {@inheritdoc}
  180. */
  181. public function installDrupal() {
  182. $this->initUserSession();
  183. $this->prepareSettings();
  184. $this->doInstall();
  185. $this->initSettings();
  186. $request = Request::createFromGlobals();
  187. $container = $this->initKernel($request);
  188. $this->initConfig($container);
  189. }
  190. /**
  191. * {@inheritdoc}
  192. */
  193. protected function doInstall() {
  194. $this->runDbTasks();
  195. // Allow classes to set database dump files.
  196. $this->setDatabaseDumpFiles();
  197. // Load the database(s).
  198. foreach ($this->databaseDumpFiles as $file) {
  199. if (substr($file, -3) == '.gz') {
  200. $file = "compress.zlib://$file";
  201. }
  202. require $file;
  203. }
  204. }
  205. /**
  206. * {@inheritdoc}
  207. */
  208. protected function initFrontPage() {
  209. // Do nothing as Drupal is not installed yet.
  210. }
  211. /**
  212. * Set database dump files to be used.
  213. */
  214. abstract protected function setDatabaseDumpFiles();
  215. /**
  216. * Add settings that are missed since the installer isn't run.
  217. */
  218. protected function prepareSettings() {
  219. parent::prepareSettings();
  220. // Remember the profile which was used.
  221. $settings['settings']['install_profile'] = (object) [
  222. 'value' => $this->installProfile,
  223. 'required' => TRUE,
  224. ];
  225. // Generate a hash salt.
  226. $settings['settings']['hash_salt'] = (object) [
  227. 'value' => Crypt::randomBytesBase64(55),
  228. 'required' => TRUE,
  229. ];
  230. // Since the installer isn't run, add the database settings here too.
  231. $settings['databases']['default'] = (object) [
  232. 'value' => Database::getConnectionInfo(),
  233. 'required' => TRUE,
  234. ];
  235. // Force every update hook to only run one entity per batch.
  236. $settings['entity_update_batch_size'] = (object) [
  237. 'value' => 1,
  238. 'required' => TRUE,
  239. ];
  240. // Set up sync directory.
  241. $settings['settings']['config_sync_directory'] = (object) [
  242. 'value' => $this->publicFilesDirectory . '/config_sync',
  243. 'required' => TRUE,
  244. ];
  245. $this->writeSettings($settings);
  246. }
  247. /**
  248. * Helper function to run pending database updates.
  249. */
  250. protected function runUpdates() {
  251. if (!$this->zlibInstalled) {
  252. $this->fail('Missing zlib requirement for update tests.');
  253. return FALSE;
  254. }
  255. $this->doRunUpdates($this->updateUrl);
  256. }
  257. /**
  258. * Runs the install database tasks for the driver used by the test runner.
  259. */
  260. protected function runDbTasks() {
  261. // Create a minimal container so that t() works.
  262. // @see install_begin_request()
  263. $container = new ContainerBuilder();
  264. $container->setParameter('language.default_values', Language::$defaultValues);
  265. $container
  266. ->register('language.default', 'Drupal\Core\Language\LanguageDefault')
  267. ->addArgument('%language.default_values%');
  268. $container
  269. ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager')
  270. ->addArgument(new Reference('language.default'));
  271. \Drupal::setContainer($container);
  272. require_once __DIR__ . '/../../../../includes/install.inc';
  273. $connection_info = Database::getConnectionInfo();
  274. $driver = $connection_info['default']['driver'];
  275. $namespace = $connection_info['default']['namespace'] ?? NULL;
  276. $errors = db_installer_object($driver, $namespace)->runTasks();
  277. if (!empty($errors)) {
  278. $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
  279. }
  280. }
  281. /**
  282. * Replace User 1 with the user created here.
  283. */
  284. protected function replaceUser1() {
  285. /** @var \Drupal\user\UserInterface $account */
  286. // @todo: Saving the account before the update is problematic.
  287. // https://www.drupal.org/node/2560237
  288. $account = User::load(1);
  289. $account->setPassword($this->rootUser->pass_raw);
  290. $account->setEmail($this->rootUser->getEmail());
  291. $account->setUsername($this->rootUser->getAccountName());
  292. $account->save();
  293. }
  294. }