Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / tests / Drupal / FunctionalTests / Update / UpdatePathTestBase.php
1 <?php
2
3 namespace Drupal\FunctionalTests\Update;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Test\TestRunnerKernel;
7 use Drupal\Tests\BrowserTestBase;
8 use Drupal\Tests\SchemaCheckTestTrait;
9 use Drupal\Core\Database\Database;
10 use Drupal\Core\DependencyInjection\ContainerBuilder;
11 use Drupal\Core\Language\Language;
12 use Drupal\Core\Url;
13 use Drupal\user\Entity\User;
14 use Symfony\Component\DependencyInjection\Reference;
15 use Symfony\Component\HttpFoundation\Request;
16
17 /**
18  * Provides a base class for writing an update test.
19  *
20  * To write an update test:
21  * - Write the hook_update_N() implementations that you are testing.
22  * - Create one or more database dump files, which will set the database to the
23  *   "before updates" state. Normally, these will add some configuration data to
24  *   the database, set up some tables/fields, etc.
25  * - Create a class that extends this class.
26  * - Ensure that the test is in the legacy group. Tests can have multiple
27  *   groups.
28  * - In your setUp() method, point the $this->databaseDumpFiles variable to the
29  *   database dump files, and then call parent::setUp() to run the base setUp()
30  *   method in this class.
31  * - In your test method, call $this->runUpdates() to run the necessary updates,
32  *   and then use test assertions to verify that the result is what you expect.
33  * - In order to test both with a "bare" database dump as well as with a
34  *   database dump filled with content, extend your update path test class with
35  *   a new test class that overrides the bare database dump. Refer to
36  *   UpdatePathTestBaseFilledTest for an example.
37  *
38  * @ingroup update_api
39  *
40  * @see hook_update_N()
41  */
42 abstract class UpdatePathTestBase extends BrowserTestBase {
43
44   use SchemaCheckTestTrait;
45
46   /**
47    * Modules to enable after the database is loaded.
48    */
49   protected static $modules = [];
50
51   /**
52    * The file path(s) to the dumped database(s) to load into the child site.
53    *
54    * The file system/tests/fixtures/update/drupal-8.bare.standard.php.gz is
55    * normally included first -- this sets up the base database from a bare
56    * standard Drupal installation.
57    *
58    * The file system/tests/fixtures/update/drupal-8.filled.standard.php.gz
59    * can also be used in case we want to test with a database filled with
60    * content, and with all core modules enabled.
61    *
62    * @var array
63    */
64   protected $databaseDumpFiles = [];
65
66   /**
67    * The install profile used in the database dump file.
68    *
69    * @var string
70    */
71   protected $installProfile = 'standard';
72
73   /**
74    * Flag that indicates whether the child site has been updated.
75    *
76    * @var bool
77    */
78   protected $upgradedSite = FALSE;
79
80   /**
81    * Array of errors triggered during the update process.
82    *
83    * @var array
84    */
85   protected $upgradeErrors = [];
86
87   /**
88    * Array of modules loaded when the test starts.
89    *
90    * @var array
91    */
92   protected $loadedModules = [];
93
94   /**
95    * Flag to indicate whether zlib is installed or not.
96    *
97    * @var bool
98    */
99   protected $zlibInstalled = TRUE;
100
101   /**
102    * Flag to indicate whether there are pending updates or not.
103    *
104    * @var bool
105    */
106   protected $pendingUpdates = TRUE;
107
108   /**
109    * The update URL.
110    *
111    * @var string
112    */
113   protected $updateUrl;
114
115   /**
116    * Disable strict config schema checking.
117    *
118    * The schema is verified at the end of running the update.
119    *
120    * @var bool
121    */
122   protected $strictConfigSchema = FALSE;
123
124   /**
125    * Fail the test if there are failed updates.
126    *
127    * @var bool
128    */
129   protected $checkFailedUpdates = TRUE;
130
131   /**
132    * Constructs an UpdatePathTestCase object.
133    *
134    * @param $test_id
135    *   (optional) The ID of the test. Tests with the same id are reported
136    *   together.
137    */
138   public function __construct($test_id = NULL) {
139     parent::__construct($test_id);
140     $this->zlibInstalled = function_exists('gzopen');
141   }
142
143   /**
144    * Overrides WebTestBase::setUp() for update testing.
145    *
146    * The main difference in this method is that rather than performing the
147    * installation via the installer, a database is loaded. Additional work is
148    * then needed to set various things such as the config directories and the
149    * container that would normally be done via the installer.
150    */
151   protected function setUp() {
152     $request = Request::createFromGlobals();
153
154     // Boot up Drupal into a state where calling the database API is possible.
155     // This is used to initialize the database system, so we can load the dump
156     // files.
157     $autoloader = require $this->root . '/autoload.php';
158     $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
159     $kernel->loadLegacyIncludes();
160
161     // Set the update url. This must be set here rather than in
162     // self::__construct() or the old URL generator will leak additional test
163     // sites.
164     $this->updateUrl = Url::fromRoute('system.db_update');
165
166     $this->setupBaseUrl();
167
168     // Install Drupal test site.
169     $this->prepareEnvironment();
170     $this->runDbTasks();
171     // Allow classes to set database dump files.
172     $this->setDatabaseDumpFiles();
173
174     // We are going to set a missing zlib requirement property for usage
175     // during the performUpgrade() and tearDown() methods. Also set that the
176     // tests failed.
177     if (!$this->zlibInstalled) {
178       parent::setUp();
179       return;
180     }
181     $this->installDrupal();
182
183     // Add the config directories to settings.php.
184     drupal_install_config_directories();
185
186     // Set the container. parent::rebuildAll() would normally do this, but this
187     // not safe to do here, because the database has not been updated yet.
188     $this->container = \Drupal::getContainer();
189
190     $this->replaceUser1();
191
192     require_once $this->root . '/core/includes/update.inc';
193
194     // Setup Mink.
195     $this->initMink();
196
197     // Set up the browser test output file.
198     $this->initBrowserOutputFile();
199   }
200
201   /**
202    * {@inheritdoc}
203    */
204   public function installDrupal() {
205     $this->initUserSession();
206     $this->prepareSettings();
207     $this->doInstall();
208     $this->initSettings();
209
210     $request = Request::createFromGlobals();
211     $container = $this->initKernel($request);
212     $this->initConfig($container);
213   }
214
215   /**
216    * {@inheritdoc}
217    */
218   protected function doInstall() {
219     $this->runDbTasks();
220     // Allow classes to set database dump files.
221     $this->setDatabaseDumpFiles();
222
223     // Load the database(s).
224     foreach ($this->databaseDumpFiles as $file) {
225       if (substr($file, -3) == '.gz') {
226         $file = "compress.zlib://$file";
227       }
228       require $file;
229     }
230   }
231
232   /**
233    * {@inheritdoc}
234    */
235   protected function initFrontPage() {
236     // Do nothing as Drupal is not installed yet.
237   }
238
239   /**
240    * Set database dump files to be used.
241    */
242   abstract protected function setDatabaseDumpFiles();
243
244   /**
245    * Add settings that are missed since the installer isn't run.
246    */
247   protected function prepareSettings() {
248     parent::prepareSettings();
249
250     // Remember the profile which was used.
251     $settings['settings']['install_profile'] = (object) [
252       'value' => $this->installProfile,
253       'required' => TRUE,
254     ];
255     // Generate a hash salt.
256     $settings['settings']['hash_salt'] = (object) [
257       'value'    => Crypt::randomBytesBase64(55),
258       'required' => TRUE,
259     ];
260
261     // Since the installer isn't run, add the database settings here too.
262     $settings['databases']['default'] = (object) [
263       'value' => Database::getConnectionInfo(),
264       'required' => TRUE,
265     ];
266
267     $this->writeSettings($settings);
268   }
269
270   /**
271    * Helper function to run pending database updates.
272    */
273   protected function runUpdates() {
274     if (!$this->zlibInstalled) {
275       $this->fail('Missing zlib requirement for update tests.');
276       return FALSE;
277     }
278     // The site might be broken at the time so logging in using the UI might
279     // not work, so we use the API itself.
280     drupal_rewrite_settings([
281       'settings' => [
282         'update_free_access' => (object) [
283           'value' => TRUE,
284           'required' => TRUE,
285         ],
286       ],
287     ]);
288
289     $this->drupalGet($this->updateUrl);
290     $this->clickLink(t('Continue'));
291
292     $this->doSelectionTest();
293     // Run the update hooks.
294     $this->clickLink(t('Apply pending updates'));
295     $this->checkForMetaRefresh();
296
297     // Ensure there are no failed updates.
298     if ($this->checkFailedUpdates) {
299       $failure = $this->cssSelect('.failure');
300       if ($failure) {
301         $this->fail('The update failed with the following message: "' . reset($failure)->getText() . '"');
302       }
303
304       // Ensure that there are no pending updates.
305       foreach (['update', 'post_update'] as $update_type) {
306         switch ($update_type) {
307           case 'update':
308             $all_updates = update_get_update_list();
309             break;
310           case 'post_update':
311             $all_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation();
312             break;
313         }
314         foreach ($all_updates as $module => $updates) {
315           if (!empty($updates['pending'])) {
316             foreach (array_keys($updates['pending']) as $update_name) {
317               $this->fail("The $update_name() update function from the $module module did not run.");
318             }
319           }
320         }
321       }
322       // Reset the static cache of drupal_get_installed_schema_version() so that
323       // more complex update path testing works.
324       drupal_static_reset('drupal_get_installed_schema_version');
325
326       // The config schema can be incorrect while the update functions are being
327       // executed. But once the update has been completed, it needs to be valid
328       // again. Assert the schema of all configuration objects now.
329       $names = $this->container->get('config.storage')->listAll();
330       /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
331       $typed_config = $this->container->get('config.typed');
332       $typed_config->clearCachedDefinitions();
333       foreach ($names as $name) {
334         $config = $this->config($name);
335         $this->assertConfigSchema($typed_config, $name, $config->get());
336       }
337
338       // Ensure that the update hooks updated all entity schema.
339       $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates();
340       if ($needs_updates) {
341         foreach (\Drupal::entityDefinitionUpdateManager()->getChangeSummary() as $entity_type_id => $summary) {
342           $entity_type_label = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getLabel();
343           foreach ($summary as $message) {
344             $this->fail("$entity_type_label: $message");
345           }
346         }
347         // The above calls to `fail()` should prevent this from ever being
348         // called, but it is here in case something goes really wrong.
349         $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.');
350       }
351     }
352   }
353
354   /**
355    * Runs the install database tasks for the driver used by the test runner.
356    */
357   protected function runDbTasks() {
358     // Create a minimal container so that t() works.
359     // @see install_begin_request()
360     $container = new ContainerBuilder();
361     $container->setParameter('language.default_values', Language::$defaultValues);
362     $container
363       ->register('language.default', 'Drupal\Core\Language\LanguageDefault')
364       ->addArgument('%language.default_values%');
365     $container
366       ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager')
367       ->addArgument(new Reference('language.default'));
368     \Drupal::setContainer($container);
369
370     require_once __DIR__ . '/../../../../includes/install.inc';
371     $connection = Database::getConnection();
372     $errors = db_installer_object($connection->driver())->runTasks();
373     if (!empty($errors)) {
374       $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
375     }
376   }
377
378   /**
379    * Replace User 1 with the user created here.
380    */
381   protected function replaceUser1() {
382     /** @var \Drupal\user\UserInterface $account */
383     // @todo: Saving the account before the update is problematic.
384     //   https://www.drupal.org/node/2560237
385     $account = User::load(1);
386     $account->setPassword($this->rootUser->pass_raw);
387     $account->setEmail($this->rootUser->getEmail());
388     $account->setUsername($this->rootUser->getUsername());
389     $account->save();
390   }
391
392   /**
393    * Tests the selection page.
394    */
395   protected function doSelectionTest() {
396     // No-op. Tests wishing to do test the selection page or the general
397     // update.php environment before running update.php can override this method
398     // and implement their required tests.
399   }
400
401 }