WebTestBase.php 109 KB
Newer Older
1
2
3
<?php

/**
4
 * @file
5
 * Contains \Drupal\simpletest\WebTestBase.
6
 */
7

8
namespace Drupal\simpletest;
9

10
use Drupal\block\Entity\Block;
11
use Drupal\Component\FileCache\FileCacheFactory;
12
use Drupal\Component\Serialization\Json;
13
use Drupal\Component\Serialization\Yaml;
14
use Drupal\Component\Utility\Html;
15
use Drupal\Component\Utility\NestedArray;
16
use Drupal\Component\Utility\UrlHelper;
17
use Drupal\Core\Cache\Cache;
18
use Drupal\Component\Utility\SafeMarkup;
19
use Drupal\Core\Database\Database;
20
use Drupal\Core\DrupalKernel;
21
use Drupal\Core\Entity\EntityInterface;
22
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
23
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
24
use Drupal\Core\Extension\MissingDependencyException;
25
use Drupal\Core\Render\Element;
26
use Drupal\Core\Session\AccountInterface;
27
use Drupal\Core\Session\AnonymousUserSession;
28
use Drupal\Core\Session\UserSession;
29
use Drupal\Core\Site\Settings;
30
use Drupal\Core\StreamWrapper\PublicStream;
31
use Drupal\Core\Url;
32
use Drupal\node\Entity\NodeType;
33
use Symfony\Component\DependencyInjection\ContainerInterface;
34
use Symfony\Component\HttpFoundation\Request;
35
use Zend\Diactoros\Uri;
36
37
38

/**
 * Test case for typical Drupal tests.
39
40
 *
 * @ingroup testing
41
 */
42
43
abstract class WebTestBase extends TestBase {

44
45
  use AssertContentTrait;

46
47
48
49
50
51
  use UserCreationTrait {
    createUser as drupalCreateUser;
    createRole as drupalCreateRole;
    createAdminRole as drupalCreateAdminRole;
  }

52
53
54
55
56
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
57
  protected $profile = 'testing';
58

59
60
61
62
63
64
65
66
67
68
69
70
71
72
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

  /**
   * The handle of the current cURL connection.
   *
   * @var resource
   */
  protected $curlHandle;

73
74
75
76
77
78
79
  /**
   * Whether or not to assert the presence of the X-Drupal-Ajax-Token.
   *
   * @var bool
   */
  protected $assertAjaxHeader = TRUE;

80
81
82
83
84
85
86
  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

87
88
89
90
91
  /**
   * The cookies of the page currently loaded in the internal browser.
   *
   * @var array
   */
92
  protected $cookies = array();
93

94
95
96
97
  /**
   * Indicates that headers should be dumped if verbose output is enabled.
   *
   * Headers are dumped to verbose by drupalGet(), drupalHead(), and
98
   * drupalPostForm().
99
100
101
102
103
   *
   * @var bool
   */
  protected $dumpHeaders = FALSE;

104
105
106
  /**
   * The current user logged in using the internal browser.
   *
107
   * @var \Drupal\Core\Session\AccountInterface|bool
108
109
110
   */
  protected $loggedInUser = FALSE;

111
112
113
114
115
116
117
118
  /**
   * The "#1" admin user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $rootUser;


119
120
121
122
123
124
125
126
127
128
129
  /**
   * The current cookie file used by cURL.
   *
   * We do not reuse the cookies in further runs, so we do not need a file
   * but we still need cookie handling, so we set the jar to NULL.
   */
  protected $cookieFile = NULL;

  /**
   * Additional cURL options.
   *
130
131
   * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
   * is set.
132
133
134
   */
  protected $additionalCurlOptions = array();

135
136
137
138
139
140
141
  /**
   * The original batch, before it was changed for testing purposes.
   *
   * @var array
   */
  protected $originalBatch;

142
  /**
143
   * The original user, before it was changed to a clean uid = 1 for testing.
144
145
146
147
148
   *
   * @var object
   */
  protected $originalUser = NULL;

149
  /**
150
   * The original shutdown handlers array, before it was cleaned for testing.
151
152
153
154
155
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

156
157
158
  /**
   * The current session ID, if available.
   */
159
  protected $sessionId = NULL;
160

161
162
163
164
165
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

166
167
168
169
170
  /**
   * The maximum number of redirects to follow when handling responses.
   */
  protected $maximumRedirects = 5;

171
172
173
  /**
   * The number of redirects followed during the handling of a request.
   */
174
  protected $redirectCount;
175

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190

  /**
   * The number of meta refresh redirects to follow, or NULL if unlimited.
   *
   * @var null|int
   */
  protected $maximumMetaRefreshCount = NULL;

  /**
   * The number of meta refresh redirects followed during ::drupalGet().
   *
   * @var int
   */
  protected $metaRefreshCount = 0;

191
192
  /**
   * The kernel used in this test.
193
194
   *
   * @var \Drupal\Core\DrupalKernel
195
196
197
   */
  protected $kernel;

198
199
200
201
202
  /**
   * The config directories used in this test.
   */
  protected $configDirectories = array();

203
204
205
206
207
208
209
  /**
   * Cookies to set on curl requests.
   *
   * @var array
   */
  protected $curlCookies = array();

210
211
212
213
214
215
216
  /**
   * An array of custom translations suitable for drupal_rewrite_settings().
   *
   * @var array
   */
  protected $customTranslations;

217
218
219
220
221
222
223
  /**
   * The class loader to use for installation and initialization of setup.
   *
   * @var \Symfony\Component\Classloader\Classloader
   */
  protected $classLoader;

224
  /**
225
   * Constructor for \Drupal\simpletest\WebTestBase.
226
227
228
229
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
230
    $this->classLoader = require DRUPAL_ROOT . '/autoload.php';
231
232
  }

233
234
235
  /**
   * Get a node from the database based on its title.
   *
236
   * @param string|\Drupal\Component\Render\MarkupInterface $title
237
   *   A node title, usually generated by $this->randomMachineName().
238
   * @param $reset
239
   *   (optional) Whether to reset the entity cache.
240
   *
241
   * @return \Drupal\node\NodeInterface
242
   *   A node entity matching $title.
243
   */
244
  function drupalGetNodeByTitle($title, $reset = FALSE) {
245
    if ($reset) {
246
      \Drupal::entityManager()->getStorage('node')->resetCache();
247
    }
248
    // Cast MarkupInterface objects to string.
249
    $title = (string) $title;
250
    $nodes = entity_load_multiple_by_properties('node', array('title' => $title));
251
252
253
254
255
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

256
257
258
  /**
   * Creates a node based on default settings.
   *
259
260
261
262
263
264
265
266
267
268
269
270
271
   * @param array $settings
   *   (optional) An associative array of settings for the node, as used in
   *   entity_create(). Override the defaults by specifying the key and value
   *   in the array, for example:
   *   @code
   *     $this->drupalCreateNode(array(
   *       'title' => t('Hello, world!'),
   *       'type' => 'article',
   *     ));
   *   @endcode
   *   The following defaults are provided:
   *   - body: Random string using the default filter format:
   *     @code
272
   *       $settings['body'][0] = array(
273
   *         'value' => $this->randomMachineName(32),
274
275
276
277
278
   *         'format' => filter_default_format(),
   *       );
   *     @endcode
   *   - title: Random string.
   *   - type: 'page'.
279
   *   - uid: The currently logged in user, or anonymous.
280
   *
281
   * @return \Drupal\node\NodeInterface
282
283
284
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
285
    // Populate defaults array.
286
    $settings += array(
287
288
289
290
      'body'      => array(array(
        'value' => $this->randomMachineName(32),
        'format' => filter_default_format(),
      )),
291
      'title'     => $this->randomMachineName(8),
292
      'type'      => 'page',
293
      'uid'       => \Drupal::currentUser()->id(),
294
    );
295
296
    $node = entity_create('node', $settings);
    $node->save();
297
298
299
300
301
302
303

    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
304
   * @param array $values
305
306
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
307
   *
308
   * @return \Drupal\node\Entity\NodeType
309
   *   Created content type.
310
   */
311
  protected function drupalCreateContentType(array $values = array()) {
312
    // Find a non-existent random type name.
313
314
    if (!isset($values['type'])) {
      do {
315
        $id = strtolower($this->randomMachineName(8));
316
      } while (NodeType::load($id));
317
318
319
320
321
322
323
    }
    else {
      $id = $values['type'];
    }
    $values += array(
      'type' => $id,
      'name' => $id,
324
    );
325
326
    $type = entity_create('node_type', $values);
    $status = $type->save();
327
    node_add_body_field($type);
328
    \Drupal::service('router.builder')->rebuild();
329

330
    $this->assertEqual($status, SAVED_NEW, SafeMarkup::format('Created content type %type.', array('%type' => $type->id())));
331

332
333
334
    return $type;
  }

335
336
337
338
339
  /**
   * Builds the renderable view of an entity.
   *
   * Entities postpone the composition of their renderable arrays to #pre_render
   * functions in order to maximize cache efficacy. This means that the full
340
341
   * renderable array for an entity is constructed in drupal_render(). Some
   * tests require the complete renderable array for an entity outside of the
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
   * drupal_render process in order to verify the presence of specific values.
   * This method isolates the steps in the render process that produce an
   * entity's renderable array.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to prepare a renderable array for.
   * @param string $view_mode
   *   (optional) The view mode that should be used to build the entity.
   * @param null $langcode
   *   (optional) For which language the entity should be prepared, defaults to
   *   the current content language.
   * @param bool $reset
   *   (optional) Whether to clear the cache for this entity.
   * @return array
   *
   * @see drupal_render()
   */
  protected function drupalBuildEntityView(EntityInterface $entity, $view_mode = 'full', $langcode = NULL, $reset = FALSE) {
360
361
362
363
    $ensure_fully_built = function(&$elements) use (&$ensure_fully_built) {
      // If the default values for this element have not been loaded yet, populate
      // them.
      if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
364
        $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
      }

      // Make any final changes to the element before it is rendered. This means
      // that the $element or the children can be altered or corrected before the
      // element is rendered into the final text.
      if (isset($elements['#pre_render'])) {
        foreach ($elements['#pre_render'] as $callable) {
          $elements = call_user_func($callable, $elements);
        }
      }

      // And recurse.
      $children = Element::children($elements, TRUE);
      foreach ($children as $key) {
        $ensure_fully_built($elements[$key]);
      }
    };

383
384
385
386
    $render_controller = $this->container->get('entity.manager')->getViewBuilder($entity->getEntityTypeId());
    if ($reset) {
      $render_controller->resetCache(array($entity->id()));
    }
387
388
    $build = $render_controller->view($entity, $view_mode, $langcode);
    $ensure_fully_built($build);
389

390
    return $build;
391
392
  }

393
394
395
396
397
  /**
   * Creates a block instance based on default settings.
   *
   * @param string $plugin_id
   *   The plugin ID of the block type for this block instance.
398
399
   * @param array $settings
   *   (optional) An associative array of settings for the block entity.
400
   *   Override the defaults by specifying the key and value in the array, for
401
402
403
   *   example:
   *   @code
   *     $this->drupalPlaceBlock('system_powered_by_block', array(
404
   *       'label' => t('Hello, world!'),
405
406
407
   *     ));
   *   @endcode
   *   The following defaults are provided:
408
   *   - label: Random string.
409
   *   - ID: Random string.
410
   *   - region: 'sidebar_first'.
411
   *   - theme: The default theme.
412
   *   - visibility: Empty array.
413
   *
414
   * @return \Drupal\block\Entity\Block
415
   *   The block entity.
416
417
418
419
   *
   * @todo
   *   Add support for creating custom block instances.
   */
420
421
  protected function drupalPlaceBlock($plugin_id, array $settings = array()) {
    $settings += array(
422
      'plugin' => $plugin_id,
423
      'region' => 'sidebar_first',
424
      'id' => strtolower($this->randomMachineName(8)),
425
      'theme' => $this->config('system.theme')->get('default'),
426
      'label' => $this->randomMachineName(8),
427
      'visibility' => array(),
428
      'weight' => 0,
429
    );
430
431
    $values = [];
    foreach (array('region', 'id', 'theme', 'plugin', 'weight', 'visibility') as $key) {
432
      $values[$key] = $settings[$key];
433
      // Remove extra values that do not belong in the settings array.
434
435
      unset($settings[$key]);
    }
436
437
    foreach ($values['visibility'] as $id => $visibility) {
      $values['visibility'][$id]['id'] = $id;
438
    }
439
    $values['settings'] = $settings;
440
441
442
    $block = entity_create('block', $values);
    $block->save();
    return $block;
443
444
  }

445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
  /**
   * Checks to see whether a block appears on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   */
  protected function assertBlockAppears(Block $block) {
    $result = $this->findBlockInstance($block);
    $this->assertTrue(!empty($result), format_string('Ensure the block @id appears on the page', array('@id' => $block->id())));
  }

  /**
   * Checks to see whether a block does not appears on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   */
  protected function assertNoBlockAppears(Block $block) {
    $result = $this->findBlockInstance($block);
    $this->assertFalse(!empty($result), format_string('Ensure the block @id does not appear on the page', array('@id' => $block->id())));
  }

  /**
   * Find a block instance on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   *
   * @return array
   *   The result from the xpath query.
   */
  protected function findBlockInstance(Block $block) {
    return $this->xpath('//div[@id = :id]', array(':id' => 'block-' . $block->id()));
  }

480
  /**
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
   * Gets a list of files that can be used in tests.
   *
   * The first time this method is called, it will call
   * simpletest_generate_file() to generate binary and ASCII text files in the
   * public:// directory. It will also copy all files in
   * core/modules/simpletest/files to public://. These contain image, SQL, PHP,
   * JavaScript, and HTML files.
   *
   * All filenames are prefixed with their type and have appropriate extensions:
   * - text-*.txt
   * - binary-*.txt
   * - html-*.html and html-*.txt
   * - image-*.png, image-*.jpg, and image-*.gif
   * - javascript-*.txt and javascript-*.script
   * - php-*.txt and php-*.php
   * - sql-*.txt and sql-*.sql
   *
   * Any subsequent calls will not generate any new files, or copy the files
   * over again. However, if a test class adds a new file to public:// that
   * is prefixed with one of the above types, it will get returned as well, even
   * on subsequent calls.
502
   *
503
   * @param $type
504
505
   *   File type, possible values: 'binary', 'html', 'image', 'javascript',
   *   'php', 'sql', 'text'.
506
   * @param $size
507
508
   *   (optional) File size in bytes to match. Defaults to NULL, which will not
   *   filter the returned list by size.
509
   *
510
   * @return
511
   *   List of files in public:// that match the filter(s).
512
   */
513
  protected function drupalGetTestFiles($type, $size = NULL) {
514
515
516
517
518
519
520
521
    if (empty($this->generatedTestFiles)) {
      // Generate binary test files.
      $lines = array(64, 1024);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('binary-' . $count++, 64, $line, 'binary');
      }

522
      // Generate ASCII text test files.
523
524
525
      $lines = array(16, 256, 1024, 2048, 20480);
      $count = 0;
      foreach ($lines as $line) {
526
        simpletest_generate_file('text-' . $count++, 64, $line, 'text');
527
528
529
530
531
532
      }

      // Copy other test files from simpletest.
      $original = drupal_get_path('module', 'simpletest') . '/files';
      $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/');
      foreach ($files as $file) {
533
        file_unmanaged_copy($file->uri, PublicStream::basePath());
534
      }
535

536
537
538
539
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
540
541
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
542
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
543
544
545
546

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
547
          $stats = stat($file->uri);
548
          if ($stats['size'] != $size) {
549
            unset($files[$file->uri]);
550
551
552
553
554
555
556
557
558
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
559
   * Compare two files based on size and file name.
560
   */
561
  protected function drupalCompareFiles($file1, $file2) {
562
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
563
564
565
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
566
567
    }
    else {
568
569
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
570
571
572
573
    }
  }

  /**
574
575
576
577
578
   * Log in a user with the internal browser.
   *
   * If a user is already logged in, then the current user is logged out before
   * logging in the specified user.
   *
579
   * Please note that neither the current user nor the passed-in user object is
580
581
582
   * populated with data of the logged in user. If you need full access to the
   * user object after logging in, it must be updated manually. If you also need
   * access to the plain-text password of the user (set by drupalCreateUser()),
583
   * e.g. to log in the same user again, then it must be re-assigned manually.
584
585
586
587
588
589
590
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
591
   *   $account = User::load($account->id());
592
593
   *   $account->pass_raw = $pass_raw;
   * @endcode
594
   *
595
   * @param \Drupal\Core\Session\AccountInterface $account
596
   *   User object representing the user to log in.
597
598
   *
   * @see drupalCreateUser()
599
   */
600
  protected function drupalLogin(AccountInterface $account) {
601
    if ($this->loggedInUser) {
602
603
604
605
      $this->drupalLogout();
    }

    $edit = array(
606
      'name' => $account->getUsername(),
607
      'pass' => $account->pass_raw
608
    );
609
    $this->drupalPostForm('user/login', $edit, t('Log in'));
610

611
    // @see WebTestBase::drupalUserIsLoggedIn()
612
613
    if (isset($this->sessionId)) {
      $account->session_id = $this->sessionId;
614
    }
615
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
616
    if ($pass) {
617
      $this->loggedInUser = $account;
618
      $this->container->get('current_user')->setAccount($account);
619
    }
620
621
  }

622
623
624
  /**
   * Returns whether a given user account is logged in.
   *
625
   * @param \Drupal\user\UserInterface $account
626
627
628
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
629
630
631
632
633
    $logged_in = FALSE;

    if (isset($account->session_id)) {
      $session_handler = $this->container->get('session_handler.storage');
      $logged_in = (bool) $session_handler->read($account->session_id);
634
    }
635

636
    return $logged_in;
637
638
  }

639
640
641
642
  /**
   * Logs a user out of the internal browser and confirms.
   *
   * Confirms logout by checking the login page.
643
   */
644
  protected function drupalLogout() {
645
646
647
    // Make a request to the logout page, and redirect to the user page, the
    // idea being if you were properly logged out you should be seeing a login
    // screen.
648
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user/login')));
649
650
651
    $this->assertResponse(200, 'User was logged out.');
    $pass = $this->assertField('name', 'Username field found.', 'Logout');
    $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
652

653
    if ($pass) {
654
655
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
656
      $this->loggedInUser = FALSE;
657
      $this->container->get('current_user')->setAccount(new AnonymousUserSession());
658
    }
659
660
  }

661
662
663
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
664
665
   * Installs Drupal with the installation profile specified in
   * \Drupal\simpletest\WebTestBase::$profile into the prefixed database.
666
   *
667
668
669
   * Afterwards, installs any additional modules specified in the static
   * \Drupal\simpletest\WebTestBase::$modules property of each class in the
   * class hierarchy.
670
671
672
673
674
675
676
   *
   * After installation all caches are flushed and several configuration values
   * are reset to the values of the parent site executing the test, since the
   * default values may be incompatible with the environment in which tests are
   * being executed.
   */
  protected function setUp() {
677
678
    // Preserve original batch for later restoration.
    $this->setBatch();
679

680
681
    // Initialize user 1 and session name.
    $this->initUserSession();
682

683
684
685
    // Get parameters for install_drupal() before removing global variables.
    $parameters = $this->installParameters();

686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
    // Prepare the child site settings.
    $this->prepareSettings();

    // Execute the non-interactive installer.
    $this->doInstall($parameters);

    // Import new settings.php written by the installer.
    $this->initSettings();

    // Initialize the request and container post-install.
    $container = $this->initKernel(\Drupal::request());

    // Initialize and override certain configurations.
    $this->initConfig($container);

    // Collect modules to install.
    $this->installModulesFromClassProperty($container);

    // Restore the original batch.
    $this->restoreBatch();

    // Reset/rebuild everything.
    $this->rebuildAll();
  }

  /**
   * Execute the non-interactive installer.
   *
   * @param array $parameters
   *   Parameters to pass to install_drupal().
   *
   * @see install_drupal()
   */
  protected function doInstall(array $parameters = []) {
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
    install_drupal($this->classLoader, $this->installParameters());
  }

  /**
   * Prepares site settings and services before installation.
   */
  protected function prepareSettings() {
728
729
730
731
    // Prepare installer settings that are not install_drupal() parameters.
    // Copy and prepare an actual settings.php, so as to resemble a regular
    // installation.
    // Not using File API; a potential error must trigger a PHP warning.
732
733
    $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php');
734
735
736
737

    // All file system paths are created by System module during installation.
    // @see system_requirements()
    // @see TestBase::prepareEnvironment()
738
    $settings['settings']['file_public_path'] = (object) [
739
      'value' => $this->publicFilesDirectory,
740
      'required' => TRUE,
741
742
    ];
    $settings['settings']['file_private_path'] = (object) [
743
      'value' => $this->privateFilesDirectory,
744
      'required' => TRUE,
745
    ];
746
747
748
    // Save the original site directory path, so that extensions in the
    // site-specific directory can still be discovered in the test site
    // environment.
749
    // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
750
    $settings['settings']['test_parent_site'] = (object) [
751
752
      'value' => $this->originalSite,
      'required' => TRUE,
753
    ];
754
    // Add the parent profile's search path to the child site's search paths.
755
    // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
756
    $settings['conf']['simpletest.settings']['parent_profile'] = (object) [
757
758
      'value' => $this->originalProfile,
      'required' => TRUE,
759
760
    ];
    $settings['settings']['apcu_ensure_unique_prefix'] = (object) [
761
762
      'value' => FALSE,
      'required' => TRUE,
763
    ];
764
    $this->writeSettings($settings);
765
766
767
768
769
770
771
    // Allow for test-specific overrides.
    $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
    if (file_exists($settings_testing_file)) {
      // Copy the testing-specific settings.php overrides in place.
      copy($settings_testing_file, $directory . '/settings.testing.php');
      // Add the name of the testing class to settings.php and include the
      // testing specific overrides
772
      file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) ."';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' ."\n", FILE_APPEND);
773
774
    }
    $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
775
776
777
    if (!file_exists($settings_services_file)) {
      // Otherwise, use the default services as a starting point for overrides.
      $settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
778
    }
779
780
    // Copy the testing-specific service overrides in place.
    copy($settings_services_file, $directory . '/services.yml');
781
782
783
    if ($this->strictConfigSchema) {
      // Add a listener to validate configuration schema on save.
      $yaml = new \Symfony\Component\Yaml\Yaml();
784
785
      $content = file_get_contents($directory . '/services.yml');
      $services = $yaml->parse($content);
786
787
      $services['services']['simpletest.config_schema_checker'] = [
        'class' => 'Drupal\Core\Config\Testing\ConfigSchemaChecker',
788
        'arguments' => ['@config.typed', $this->getConfigSchemaExclusions()],
789
790
791
792
        'tags' => [['name' => 'event_subscriber']]
      ];
      file_put_contents($directory . '/services.yml', $yaml->dump($services));
    }
793
    // Since Drupal is bootstrapped already, install_begin_request() will not
794
795
    // bootstrap again. Hence, we have to reload the newly written custom
    // settings.php manually.
796
797
    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
  }
798

799
800
801
802
803
  /**
   * Initialize settings created during install.
   */
  protected function initSettings() {
    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
804
805
806
807
808
809
810
811
812
813
    foreach ($GLOBALS['config_directories'] as $type => $path) {
      $this->configDirectories[$type] = $path;
    }

    // After writing settings.php, the installer removes write permissions
    // from the site directory. To allow drupal_generate_test_ua() to write
    // a file containing the private key for drupal_valid_test_ua(), the site
    // directory has to be writable.
    // TestBase::restoreEnvironment() will delete the entire site directory.
    // Not using File API; a potential error must trigger a PHP warning.
814
815
    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
  }
816

817
818
819
820
821
822
823
  /**
   * Initialize various configurations post-installation.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   */
  protected function initConfig(ContainerInterface $container) {
824
    $config = $container->get('config.factory');
825

826
827
828
829
    // Manually create and configure private and temporary files directories.
    // While these could be preset/enforced in settings.php like the public
    // files directory above, some tests expect them to be configurable in the
    // UI. If declared in settings.php, they would no longer be configurable.
830
831
    file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY);
    file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY);
832
    $config->getEditable('system.file')
833
      ->set('path.temporary', $this->tempFilesDirectory)
834
835
836
      ->save();

    // Manually configure the test mail collector implementation to prevent
837
    // tests from sending out emails and collect them in state instead.
838
839
    // While this should be enforced via settings.php prior to installation,
    // some tests expect to be able to test mail system implementations.
840
    $config->getEditable('system.mail')
841
      ->set('interface.default', 'test_mail_collector')
842
843
      ->save();

844
845
846
    // By default, verbosely display all errors and disable all production
    // environment optimizations for all tests to avoid needless overhead and
    // ensure a sane default experience for test authors.
847
    // @see https://www.drupal.org/node/2259167
848
    $config->getEditable('system.logging')
849
850
      ->set('error_level', 'verbose')
      ->save();
851
    $config->getEditable('system.performance')
852
853
      ->set('css.preprocess', FALSE)
      ->set('js.preprocess', FALSE)
854
      ->save();
855
856
857
858
859
860
861
862
863

    // Set an explicit time zone to not rely on the system one, which may vary
    // from setup to setup. The Australia/Sydney time zone is chosen so all
    // tests are run using an edge case scenario (UTC+10 and DST). This choice
    // is made to prevent time zone related regressions and reduce the
    // fragility of the testing system in general.
    $config->getEditable('system.date')
      ->set('timezone.default', 'Australia/Sydney')
      ->save();
864
  }
865

866
867
868
869
  /**
   * Reset and rebuild the environment after setup.
   */
  protected function rebuildAll() {
870
871
872
    // Reset/rebuild all data structures after enabling the modules, primarily
    // to synchronize all data structures and caches between the test runner and
    // the child site.
873
    // @see \Drupal\Core\DrupalKernel::bootCode()
874
875
    // @todo Test-specific setUp() methods may set up further fixtures; find a
    //   way to execute this after setUp() is done, or to eliminate it entirely.
876
    $this->resetAll();
877
    $this->kernel->prepareLegacyRequest(\Drupal::request());
878

879
880
881
882
883
    // Explicitly call register() again on the container registered in \Drupal.
    // @todo This should already be called through
    //   DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that
    //   appears to be calling a different container.
    $this->container->get('stream_wrapper_manager')->register();
884
885
  }

886
887
888
889
890
  /**
   * Returns the parameters that will be used when Simpletest installs Drupal.
   *
   * @see install_drupal()
   * @see install_state_defaults()
891
892
893
   *
   * @return array
   *   Array of parameters for use in install_drupal().
894
895
896
   */
  protected function installParameters() {
    $connection_info = Database::getConnectionInfo();
897
    $driver = $connection_info['default']['driver'];
898
    $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
899
900
901
902
    unset($connection_info['default']['driver']);
    unset($connection_info['default']['namespace']);
    unset($connection_info['default']['pdo']);
    unset($connection_info['default']['init_commands']);
903
904
905
906
907
908
909
    // Remove database connection info that is not used by SQLite.
    if ($driver == 'sqlite') {
      unset($connection_info['default']['username']);
      unset($connection_info['default']['password']);
      unset($connection_info['default']['host']);
      unset($connection_info['default']['port']);
    }
910
911
912
913
914
915
916
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
917
918
919
920
        'install_settings_form' => array(
          'driver' => $driver,
          $driver => $connection_info['default'],
        ),
921
922
923
924
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
925
926
            'name' => $this->rootUser->name,
            'mail' => $this->rootUser->getEmail(),
927
            'pass' => array(
928
929
              'pass1' => $this->rootUser->pass_raw,
              'pass2' => $this->rootUser->pass_raw,
930
931
            ),
          ),
932
933
934
          // \Drupal\Core\Render\Element\Checkboxes::valueCallback() requires
          // NULL instead of FALSE values for programmatic form submissions to
          // disable a checkbox.
935
936
937
938
939
940
941
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );
942
943
944

    // If we only have one db driver available, we cannot set the driver.
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
945
    if (count($this->getDatabaseTypes()) == 1) {
946
947
      unset($parameters['forms']['install_settings_form']['driver']);
    }
948
949
950
    return $parameters;
  }

951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
  /**
   * Preserve the original batch, and instantiate the test batch.
   */
  protected function setBatch() {
    // When running tests through the Simpletest UI (vs. on the command line),
    // Simpletest's batch conflicts with the installer's batch. Batch API does
    // not support the concept of nested batches (in which the nested is not
    // progressive), so we need to temporarily pretend there was no batch.
    // Backup the currently running Simpletest batch.
    $this->originalBatch = batch_get();

    // Reset the static batch to remove Simpletest's batch operations.
    $batch = &batch_get();
    $batch = [];
  }

  /**
   * Restore the original batch.
   *
   * @see ::setBatch
   */
  protected function restoreBatch() {
    // Restore the original Simpletest batch.
    $batch = &batch_get();
    $batch = $this->originalBatch;
  }

  /**
   * Initializes user 1 for the site to be installed.
   */
  protected function initUserSession() {
    // Define information about the user 1 account.
    $this->rootUser = new UserSession(array(
      'uid' => 1,
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomMachineName(),
    ));

    // The child site derives its session name from the database prefix when
    // running web tests.
    $this->generateSessionName($this->databasePrefix);
  }

  /**
   * Initializes the kernel after installation.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   Request object.
   *
   * @return \Symfony\Component\DependencyInjection\ContainerInterface
   *   The container.
   */
  protected function initKernel(Request $request) {
    $this->kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod', TRUE);
    $this->kernel->prepareLegacyRequest($request);
    // Force the container to be built from scratch instead of loaded from the
    // disk. This forces us to not accidentally load the parent site.
    return $this->kernel->rebuildContainer();
  }

  /**
   * Install modules defined by `static::$modules`.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   */
  protected function installModulesFromClassProperty(ContainerInterface $container) {
    $class = get_class($this);
    $modules = [];
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
    if ($modules) {
      $modules = array_unique($modules);
      try {
        $success = $container->get('module_installer')->install($modules, TRUE);
        $this->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', ['%modules' => implode(', ', $modules)]));
      }
      catch (MissingDependencyException $e) {
        // The exception message has all the details.
        $this->fail($e->getMessage());
      }

      $this->rebuildContainer();
    }
  }

1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
  /**
   * Returns all supported database driver installer objects.
   *
   * This wraps drupal_get_database_types() for use without a current container.
   *
   * @return \Drupal\Core\Database\Install\Tasks[]
   *   An array of available database driver installer objects.
   */
  protected function getDatabaseTypes() {
    \Drupal::setContainer($this->originalContainer);
    $database_types = drupal_get_database_types();
1053
    \Drupal::unsetContainer();
1054
1055
1056
    return $database_types;
  }

1057
  /**
1058
   * Rewrites the settings.php file of the test site.
1059
   *
1060
1061
1062
   * @param array $settings
   *   An array of settings to write out, in the format expected by
   *   drupal_rewrite_settings().
1063
1064
1065
   *
   * @see drupal_rewrite_settings()
   */
1066
  protected function writeSettings(array $settings) {
1067
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
1068
1069
1070
1071
1072
    $filename = $this->siteDirectory . '/settings.php';
    // system_requirements() removes write permissions from settings.php
    // whenever it is invoked.
    // Not using File API; a potential error must trigger a PHP warning.
    chmod($filename, 0666);
1073
1074
1075
    drupal_rewrite_settings($settings, $filename);
  }

1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
  /**
   * Changes parameters in the services.yml file.
   *
   * @param $name
   *   The name of the parameter.
   * @param $value
   *   The value of the parameter.
   */
  protected function setContainerParameter($name, $value) {
    $filename = $this->siteDirectory . '/services.yml';
    chmod($filename, 0666);

    $services = Yaml::decode(file_get_contents($filename));
    $services['parameters'][$name] = $value;
    file_put_contents($filename, Yaml::encode($services));

1092
1093
1094
    // Ensure that the cache is deleted for the yaml file loader.
    $file_cache = FileCacheFactory::get('container_yaml_loader');
    $file_cache->delete($filename);
1095
1096
  }

1097
  /**
1098
   * Queues custom translations to be written to settings.php.
1099
   *
1100
1101
   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
   * translations.
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
   *
   * @param string $langcode
   *   The langcode to add translations for.
   * @param array $values
   *   Array of values containing the untranslated string and its translation.
   *   For example:
   *   @code
   *   array(
   *     '' => array('Sunday' => 'domingo'),
   *     'Long month name' => array('March' => 'marzo'),
   *   );
   *   @endcode
1114
1115
   *   Pass an empty array to remove all existing custom translations for the
   *   given $langcode.
1116
1117
   */
  protected function addCustomTranslations($langcode, array $values) {
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
    // If $values is empty, then the test expects all custom translations to be
    // cleared.
    if (empty($values)) {
      $this->customTranslations[$langcode] = array();
    }
    // Otherwise, $values are expected to be merged into previously passed
    // values, while retaining keys that are not explicitly set.
    else {
      foreach ($values as $context => $translations) {
        foreach ($translations as $original => $translation) {
          $this->customTranslations[$langcode][$context][$original] = $translation;
        }
1130
1131
1132
1133
1134
      }
    }
  }

  /**
1135
1136
1137
1138
   * Writes custom translations to the test site's settings.php.
   *
   * Use TestBase::addCustomTranslations() to queue custom translations before
   * calling this method.
1139
1140
   */
  protected function writeCustomTranslations() {
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
    $settings = array();
    foreach ($this->