summaryrefslogtreecommitdiffstats
path: root/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
blob: c3fcb758fb8ed9a954a92ebecf13d2fb3cd93751 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
<?php

namespace Drupal\Tests\system\Functional\Module;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Logger\RfcLogLevel;

/**
 * Install/uninstall core module and confirm table creation/deletion.
 *
 * @group Module
 */
class InstallUninstallTest extends ModuleTestBase {

  /**
   * {@inheritdoc}
   */
  public static $modules = ['system_test', 'dblog', 'taxonomy', 'update_test_postupdate'];

  /**
   * Tests that a fixed set of modules can be installed and uninstalled.
   */
  public function testInstallUninstall() {
    // Set a variable so that the hook implementations in system_test.module
    // will display messages via
    // \Drupal\Core\Messenger\MessengerInterface::addStatus().
    $this->container->get('state')->set('system_test.verbose_module_hooks', TRUE);

    // Install and uninstall module_test to ensure hook_preinstall_module and
    // hook_preuninstall_module are fired as expected.
    $this->container->get('module_installer')->install(['module_test']);
    $this->assertEqual($this->container->get('state')->get('system_test_preinstall_module'), 'module_test');
    $this->container->get('module_installer')->uninstall(['module_test']);
    $this->assertEqual($this->container->get('state')->get('system_test_preuninstall_module'), 'module_test');
    $this->resetAll();

    $all_modules = system_rebuild_module_data();

    // Test help on required modules, but do not test uninstalling.
    $required_modules = array_filter($all_modules, function ($module) {
      if (!empty($module->info['required']) || $module->status == TRUE) {
        if ($module->info['package'] != 'Testing' && empty($module->info['hidden'])) {
          return TRUE;
        }
      }
      return FALSE;
    });

    $required_modules['help'] = $all_modules['help'];

    // Test uninstalling without hidden, required, and already enabled modules.
    $all_modules = array_filter($all_modules, function ($module) {
      if (!empty($module->info['hidden']) || !empty($module->info['required']) || $module->status == TRUE || $module->info['package'] == 'Testing') {
        return FALSE;
      }
      return TRUE;
    });

    // Install the Help module, and verify it installed successfully.
    unset($all_modules['help']);
    $this->assertModuleNotInstalled('help');
    $edit = [];
    $edit["modules[help][enable]"] = TRUE;
    $this->drupalPostForm('admin/modules', $edit, t('Install'));
    $this->assertText('has been enabled', 'Modules status has been updated.');
    $this->assertText(t('hook_modules_installed fired for help'));
    $this->assertModuleSuccessfullyInstalled('help');

    // Test help for the required modules.
    foreach ($required_modules as $name => $module) {
      $this->assertHelp($name, $module->info['name']);
    }

    // Go through each module in the list and try to install and uninstall
    // it with its dependencies.
    foreach ($all_modules as $name => $module) {
      $was_installed_list = \Drupal::moduleHandler()->getModuleList();

      // Start a list of modules that we expect to be installed this time.
      $modules_to_install = [$name];
      foreach (array_keys($module->requires) as $dependency) {
        if (isset($all_modules[$dependency])) {
          $modules_to_install[] = $dependency;
        }
      }

      // Check that each module is not yet enabled and does not have any
      // database tables yet.
      foreach ($modules_to_install as $module_to_install) {
        $this->assertModuleNotInstalled($module_to_install);
      }

      // Install the module.
      $edit = [];
      $package = $module->info['package'];
      $edit['modules[' . $name . '][enable]'] = TRUE;
      $this->drupalPostForm('admin/modules', $edit, t('Install'));

      // Handle experimental modules, which require a confirmation screen.
      if ($package == 'Core (Experimental)') {
        $this->assertText('Are you sure you wish to enable experimental modules?');
        if (count($modules_to_install) > 1) {
          // When there are experimental modules, needed dependencies do not
          // result in the same page title, but there will be expected text
          // indicating they need to be enabled.
          $this->assertText('You must enable');
        }
        $this->drupalPostForm(NULL, [], t('Continue'));
      }
      // Handle the case where modules were installed along with this one and
      // where we therefore hit a confirmation screen.
      elseif (count($modules_to_install) > 1) {
        // Verify that we are on the correct form and that the expected text
        // about enabling dependencies appears.
        $this->assertText('Some required modules must be enabled');
        $this->assertText('You must enable');
        $this->drupalPostForm(NULL, [], t('Continue'));
      }

      // List the module display names to check the confirmation message.
      $module_names = [];
      foreach ($modules_to_install as $module_to_install) {
        $module_names[] = $all_modules[$module_to_install]->info['name'];
      }
      $expected_text = \Drupal::translation()->formatPlural(count($module_names), 'Module @name has been enabled.', '@count modules have been enabled: @names.', [
        '@name' => $module_names[0],
        '@names' => implode(', ', $module_names),
      ]);
      $this->assertText($expected_text, 'Modules status has been updated.');

      // Check that hook_modules_installed() was invoked with the expected list
      // of modules, that each module's database tables now exist, and that
      // appropriate messages appear in the logs.
      foreach ($modules_to_install as $module_to_install) {
        $this->assertText(t('hook_modules_installed fired for @module', ['@module' => $module_to_install]));
        $this->assertLogMessage('system', "%module module installed.", ['%module' => $module_to_install], RfcLogLevel::INFO);
        $this->assertInstallModuleUpdates($module_to_install);
        $this->assertModuleSuccessfullyInstalled($module_to_install);
      }

      // Verify the help page.
      $this->assertHelp($name, $module->info['name']);

      // Uninstall the original module, plus everything else that was installed
      // with it.
      if ($name == 'forum') {
        // Forum has an extra step to be able to uninstall it.
        $this->preUninstallForum();
      }

      $now_installed_list = \Drupal::moduleHandler()->getModuleList();
      $added_modules = array_diff(array_keys($now_installed_list), array_keys($was_installed_list));
      while ($added_modules) {
        $initial_count = count($added_modules);
        foreach ($added_modules as $to_uninstall) {
          // See if we can currently uninstall this module (if its dependencies
          // have been uninstalled), and do so if we can.
          $this->drupalGet('admin/modules/uninstall');
          $field_name = "uninstall[$to_uninstall]";
          $has_checkbox = $this->xpath('//input[@type="checkbox" and @name="' . $field_name . '"]');
          $disabled = $this->xpath('//input[@type="checkbox" and @name="' . $field_name . '" and @disabled="disabled"]');

          if (!empty($has_checkbox) && empty($disabled)) {
            // This one is eligible for being uninstalled.
            $package = $all_modules[$to_uninstall]->info['package'];
            $this->assertSuccessfulUninstall($to_uninstall, $package);
            $added_modules = array_diff($added_modules, [$to_uninstall]);
          }
        }

        // If we were not able to find a module to uninstall, fail and exit the
        // loop.
        $final_count = count($added_modules);
        if ($initial_count == $final_count) {
          $this->fail('Remaining modules could not be uninstalled for ' . $name);
          break;
        }
      }
    }

    // Uninstall the help module and put it back into the list of modules.
    $all_modules['help'] = $required_modules['help'];
    $this->assertSuccessfulUninstall('help', $required_modules['help']->info['package']);

    // Now that all modules have been tested, go back and try to enable them
    // all again at once. This tests two things:
    // - That each module can be successfully enabled again after being
    //   uninstalled.
    // - That enabling more than one module at the same time does not lead to
    //   any errors.
    $edit = [];
    $experimental = FALSE;
    foreach ($all_modules as $name => $module) {
      $edit['modules[' . $name . '][enable]'] = TRUE;
      // Track whether there is at least one experimental module.
      if ($module->info['package'] == 'Core (Experimental)') {
        $experimental = TRUE;
      }
    }
    $this->drupalPostForm('admin/modules', $edit, t('Install'));

    // If there are experimental modules, click the confirm form.
    if ($experimental) {
      $this->assertText('Are you sure you wish to enable experimental modules?');
      $this->drupalPostForm(NULL, [], t('Continue'));
    }
    // The string tested here is translatable but we are only using a part of it
    // so using a translated string is wrong. Doing so would create a new string
    // to translate.
    $this->assertText(new FormattableMarkup('@count modules have been enabled: ', ['@count' => count($all_modules)]), 'Modules status has been updated.');
  }

  /**
   * Asserts that a module is not yet installed.
   *
   * @param string $name
   *   Name of the module to check.
   */
  protected function assertModuleNotInstalled($name) {
    $this->assertModules([$name], FALSE);
    $this->assertModuleTablesDoNotExist($name);
  }

  /**
   * Asserts that a module was successfully installed.
   *
   * @param string $name
   *   Name of the module to check.
   */
  protected function assertModuleSuccessfullyInstalled($name) {
    $this->assertModules([$name], TRUE);
    $this->assertModuleTablesExist($name);
    $this->assertModuleConfig($name);
  }

  /**
   * Uninstalls a module and asserts that it was done correctly.
   *
   * @param string $module
   *   The name of the module to uninstall.
   * @param string $package
   *   (optional) The package of the module to uninstall. Defaults
   *   to 'Core'.
   */
  protected function assertSuccessfulUninstall($module, $package = 'Core') {
    $edit = [];
    $edit['uninstall[' . $module . ']'] = TRUE;
    $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
    $this->drupalPostForm(NULL, NULL, t('Uninstall'));
    $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
    $this->assertModules([$module], FALSE);

    // Check that the appropriate hook was fired and the appropriate log
    // message appears. (But don't check for the log message if the dblog
    // module was just uninstalled, since the {watchdog} table won't be there
    // anymore.)
    $this->assertText(t('hook_modules_uninstalled fired for @module', ['@module' => $module]));
    $this->assertLogMessage('system', "%module module uninstalled.", ['%module' => $module], RfcLogLevel::INFO);

    // Check that the module's database tables no longer exist.
    $this->assertModuleTablesDoNotExist($module);
    // Check that the module's config files no longer exist.
    $this->assertNoModuleConfig($module);
    $this->assertUninstallModuleUpdates($module);
  }

  /**
   * Asserts the module post update functions after install.
   *
   * @param string $module
   *   The module that got installed.
   */
  protected function assertInstallModuleUpdates($module) {
    /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
    $post_update_registry = \Drupal::service('update.post_update_registry');
    $all_update_functions = $post_update_registry->getPendingUpdateFunctions();
    $empty_result = TRUE;
    foreach ($all_update_functions as $function) {
      list($function_module,) = explode('_post_update_', $function);
      if ($module === $function_module) {
        $empty_result = FALSE;
        break;
      }
    }
    $this->assertTrue($empty_result, 'Ensures that no pending post update functions are available.');

    $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
    switch ($module) {
      case 'block':
        $this->assertFalse(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates));
        break;
      case 'update_test_postupdate':
        $this->assertFalse(array_diff(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates));
        break;
    }
  }

  /**
   * Asserts the module post update functions after uninstall.
   *
   * @param string $module
   *   The module that got installed.
   */
  protected function assertUninstallModuleUpdates($module) {
    /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
    $post_update_registry = \Drupal::service('update.post_update_registry');
    $all_update_functions = $post_update_registry->getPendingUpdateFunctions();

    switch ($module) {
      case 'block':
        $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $all_update_functions), 'Asserts that no pending post update functions are available.');

        $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
        $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates), 'Asserts that no post update functions are stored in keyvalue store.');
        break;
    }
  }

  /**
   * Verifies a module's help.
   *
   * Verifies that the module help page from hook_help() exists and can be
   * displayed, and that it contains the phrase "Foo Bar module", where "Foo
   * Bar" is the name of the module from the .info.yml file.
   *
   * @param string $module
   *   Machine name of the module to verify.
   * @param string $name
   *   Human-readable name of the module to verify.
   */
  protected function assertHelp($module, $name) {
    $this->drupalGet('admin/help/' . $module);
    $this->assertResponse(200, "Help for $module displayed successfully");
    $this->assertText($name . ' module', "'$name module' is on the help page for $module");
    $this->assertLink('online documentation for the ' . $name . ' module', 0, "Correct online documentation link is in the help page for $module");
  }

  /**
   * Deletes forum taxonomy terms, so Forum can be uninstalled.
   */
  protected function preUninstallForum() {
    // There only should be a 'General discussion' term in the 'forums'
    // vocabulary, but just delete any terms there in case the name changes.
    $query = \Drupal::entityQuery('taxonomy_term');
    $query->condition('vid', 'forums');
    $ids = $query->execute();
    $storage = \Drupal::entityManager()->getStorage('taxonomy_term');
    $terms = $storage->loadMultiple($ids);
    $storage->delete($terms);
  }

}