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

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

8
namespace Drupal\simpletest;
9

10
use Drupal\Component\Utility\Crypt;
11
use Drupal\Component\Utility\NestedArray;
12
use Drupal\Component\Utility\String;
13
use Drupal\Core\DrupalKernel;
14
15
use Drupal\Core\Database\Database;
use Drupal\Core\Database\ConnectionNotDefinedException;
16
use Drupal\Core\Language\Language;
17
18
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\UserSession;
19
use Drupal\Core\StreamWrapper\PublicStream;
20
use Drupal\Core\Datetime\DrupalDateTime;
21
use Drupal\block\Entity\Block;
22
use Symfony\Component\HttpFoundation\Request;
23
24
25
26

/**
 * Test case for typical Drupal tests.
 */
27
28
abstract class WebTestBase extends TestBase {

29
30
31
32
33
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
34
  protected $profile = 'testing';
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

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

  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

57
58
59
60
  /**
   * Indicates that headers should be dumped if verbose output is enabled.
   *
   * Headers are dumped to verbose by drupalGet(), drupalHead(), and
61
   * drupalPostForm().
62
63
64
65
66
   *
   * @var bool
   */
  protected $dumpHeaders = FALSE;

67
68
69
70
71
72
73
74
  /**
   * The content of the page currently loaded in the internal browser.
   *
   * @var string
   */
  protected $content;

  /**
75
   * The plain-text content of the currently-loaded page.
76
77
78
79
80
   *
   * @var string
   */
  protected $plainTextContent;

81
  /**
82
   * The value of drupalSettings for the currently-loaded page.
83
   *
84
   * drupalSettings refers to the drupalSettings JavaScript variable.
85
86
87
88
89
   *
   * @var Array
   */
  protected $drupalSettings;

90
91
92
  /**
   * The parsed version of the page.
   *
93
   * @var \SimpleXMLElement
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
   */
  protected $elements = NULL;

  /**
   * The current user logged in using the internal browser.
   *
   * @var bool
   */
  protected $loggedInUser = FALSE;

  /**
   * 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.
   *
115
116
   * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
   * is set.
117
118
119
120
   */
  protected $additionalCurlOptions = array();

  /**
121
   * The original user, before it was changed to a clean uid = 1 for testing.
122
123
124
125
126
   *
   * @var object
   */
  protected $originalUser = NULL;

127
  /**
128
   * The original shutdown handlers array, before it was cleaned for testing.
129
130
131
132
133
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

134
  /**
135
   * HTTP authentication method.
136
137
138
   */
  protected $httpauth_method = CURLAUTH_BASIC;

139
140
141
142
143
  /**
   * HTTP authentication credentials (<username>:<password>).
   */
  protected $httpauth_credentials = NULL;

144
145
146
147
148
149
150
151
152
153
  /**
   * The current session name, if available.
   */
  protected $session_name = NULL;

  /**
   * The current session ID, if available.
   */
  protected $session_id = NULL;

154
155
156
157
158
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

159
160
161
162
163
  /**
   * The maximum number of redirects to follow when handling responses.
   */
  protected $maximumRedirects = 5;

164
165
166
167
168
  /**
   * The number of redirects followed during the handling of a request.
   */
  protected $redirect_count;

169
170
171
172
173
  /**
   * The kernel used in this test.
   */
  protected $kernel;

174
175
176
177
178
179
180
  /**
   * Cookies to set on curl requests.
   *
   * @var array
   */
  protected $curlCookies = array();

181
182
183
184
185
186
187
  /**
   * An array of custom translations suitable for drupal_rewrite_settings().
   *
   * @var array
   */
  protected $customTranslations;

188
  /**
189
   * Constructor for \Drupal\simpletest\WebTestBase.
190
191
192
193
194
195
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

196
197
198
  /**
   * Get a node from the database based on its title.
   *
199
   * @param $title
200
   *   A node title, usually generated by $this->randomName().
201
   * @param $reset
202
   *   (optional) Whether to reset the entity cache.
203
   *
204
   * @return \Drupal\node\NodeInterface
205
   *   A node entity matching $title.
206
   */
207
  function drupalGetNodeByTitle($title, $reset = FALSE) {
208
    if ($reset) {
209
      \Drupal::entityManager()->getStorageController('node')->resetCache();
210
211
    }
    $nodes = entity_load_multiple_by_properties('node', array('title' => $title));
212
213
214
215
216
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

217
218
219
  /**
   * Creates a node based on default settings.
   *
220
221
222
223
224
225
226
227
228
229
230
231
232
   * @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
233
   *       $settings['body'][0] = array(
234
235
236
237
238
   *         'value' => $this->randomName(32),
   *         'format' => filter_default_format(),
   *       );
   *     @endcode
   *   - title: Random string.
239
   *   - comment: COMMENT_OPEN.
240
241
242
243
244
245
   *   - changed: REQUEST_TIME.
   *   - promote: NODE_NOT_PROMOTED.
   *   - log: Empty string.
   *   - status: NODE_PUBLISHED.
   *   - sticky: NODE_NOT_STICKY.
   *   - type: 'page'.
246
   *   - langcode: Language::LANGCODE_NOT_SPECIFIED.
247
248
249
250
   *   - uid: The currently logged in user, or the user running test.
   *   - revision: 1. (Backwards-compatible binary flag indicating whether a
   *     new revision should be created; use 1 to specify a new revision.)
   *
251
   * @return \Drupal\node\NodeInterface
252
253
254
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
255
    // Populate defaults array.
256
    $settings += array(
257
      'body'      => array(array()),
258
      'title'     => $this->randomName(8),
259
      'changed'   => REQUEST_TIME,
260
      'promote'   => NODE_NOT_PROMOTED,
261
262
      'revision'  => 1,
      'log'       => '',
263
264
      'status'    => NODE_PUBLISHED,
      'sticky'    => NODE_NOT_STICKY,
265
      'type'      => 'page',
266
      'langcode'  => Language::LANGCODE_NOT_SPECIFIED,
267
    );
268
269
270
271

    // Use the original node's created time for existing nodes.
    if (isset($settings['created']) && !isset($settings['date'])) {
      $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O');
272
    }
273
274
275
276
277

    // If the node's user uid is not specified manually, use the currently
    // logged in user if available, or else the user running the test.
    if (!isset($settings['uid'])) {
      if ($this->loggedInUser) {
278
        $settings['uid'] = $this->loggedInUser->id();
279
280
      }
      else {
281
        $user = \Drupal::currentUser() ?: $GLOBALS['user'];
282
        $settings['uid'] = $user->id();
283
284
285
      }
    }

286
    // Merge body field value and format separately.
287
    $settings['body'][0] += array(
288
      'value' => $this->randomName(32),
289
      'format' => filter_default_format(),
290
291
    );

292
    $node = entity_create('node', $settings);
293
294
295
    if (!empty($settings['revision'])) {
      $node->setNewRevision();
    }
296
    $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
315
316
317
318
319
320
321
322
323
    if (!isset($values['type'])) {
      do {
        $id = strtolower($this->randomName(8));
      } while (node_type_load($id));
    }
    else {
      $id = $values['type'];
    }
    $values += array(
      'type' => $id,
      'name' => $id,
324
    );
325
326
    $type = entity_create('node_type', $values);
    $status = $type->save();
327
    menu_router_rebuild();
328

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

331
332
    // Reset permissions so that permissions for this content type are
    // available.
333
334
    $this->checkPermissions(array(), TRUE);

335
336
337
    return $type;
  }

338
339
340
341
342
343
344
345
  /**
   * Creates a block instance based on default settings.
   *
   * Note: Until this can be done programmatically, the active user account
   * must have permission to administer blocks.
   *
   * @param string $plugin_id
   *   The plugin ID of the block type for this block instance.
346
347
   * @param array $settings
   *   (optional) An associative array of settings for the block entity.
348
   *   Override the defaults by specifying the key and value in the array, for
349
350
351
   *   example:
   *   @code
   *     $this->drupalPlaceBlock('system_powered_by_block', array(
352
   *       'label' => t('Hello, world!'),
353
354
355
   *     ));
   *   @endcode
   *   The following defaults are provided:
356
   *   - label: Random string.
357
   *   - id: Random string.
358
   *   - region: 'sidebar_first'.
359
   *   - theme: The default theme.
360
   *   - visibility: Empty array.
361
   *
362
   * @return \Drupal\block\Entity\Block
363
   *   The block entity.
364
365
366
367
   *
   * @todo
   *   Add support for creating custom block instances.
   */
368
369
  protected function drupalPlaceBlock($plugin_id, array $settings = array()) {
    $settings += array(
370
      'plugin' => $plugin_id,
371
      'region' => 'sidebar_first',
372
      'id' => strtolower($this->randomName(8)),
373
      'theme' => \Drupal::config('system.theme')->get('default'),
374
375
      'label' => $this->randomName(8),
      'visibility' => array(),
376
      'weight' => 0,
377
    );
378
    foreach (array('region', 'id', 'theme', 'plugin', 'visibility', 'weight') as $key) {
379
      $values[$key] = $settings[$key];
380
      // Remove extra values that do not belong in the settings array.
381
382
383
      unset($settings[$key]);
    }
    $values['settings'] = $settings;
384
385
386
    $block = entity_create('block', $values);
    $block->save();
    return $block;
387
388
  }

389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
  /**
   * 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()));
  }

424
  /**
425
   * Gets a list files that can be used in tests.
426
   *
427
   * @param $type
428
429
   *   File type, possible values: 'binary', 'html', 'image', 'javascript',
   *   'php', 'sql', 'text'.
430
431
   * @param $size
   *   File size in bytes to match. Please check the tests/files folder.
432
   *
433
434
   * @return
   *   List of files that match filter.
435
   */
436
  protected function drupalGetTestFiles($type, $size = NULL) {
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
    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');
      }

      // Generate text test files.
      $lines = array(16, 256, 1024, 2048, 20480);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('text-' . $count++, 64, $line);
      }

      // 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) {
456
        file_unmanaged_copy($file->uri, PublicStream::basePath());
457
      }
458

459
460
461
462
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
463
464
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
465
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
466
467
468
469

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
470
          $stats = stat($file->uri);
471
          if ($stats['size'] != $size) {
472
            unset($files[$file->uri]);
473
474
475
476
477
478
479
480
481
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
482
   * Compare two files based on size and file name.
483
   */
484
  protected function drupalCompareFiles($file1, $file2) {
485
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
486
487
488
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
489
490
    }
    else {
491
492
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
493
494
495
496
    }
  }

  /**
497
   * Create a user with a given set of permissions.
498
   *
499
500
501
   * @param array $permissions
   *   Array of permission names to assign to user. Note that the user always
   *   has the default permissions derived from the "authenticated users" role.
502
   * @param string $name
503
   *   The user name.
504
   *
505
   * @return \Drupal\user\Entity\User|false
506
   *   A fully loaded user object with pass_raw property, or FALSE if account
507
508
   *   creation fails.
   */
509
  protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
510
511
512
513
514
515
516
    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
517
518
519
520
    }

    // Create a user assigned to that role.
    $edit = array();
521
    $edit['name']   = !empty($name) ? $name : $this->randomName();
522
    $edit['mail']   = $edit['name'] . '@example.com';
523
524
    $edit['pass']   = user_password();
    $edit['status'] = 1;
525
    if ($rid) {
526
      $edit['roles'] = array($rid);
527
    }
528

529
530
    $account = entity_create('user', $edit);
    $account->save();
531

532
533
    $this->assertTrue($account->id(), String::format('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), 'User login');
    if (!$account->id()) {
534
535
536
537
538
539
540
541
542
543
544
      return FALSE;
    }

    // Add the raw password so that we can log in as this user.
    $account->pass_raw = $edit['pass'];
    return $account;
  }

  /**
   * Internal helper function; Create a role with specified permissions.
   *
545
   * @param array $permissions
546
   *   Array of permission names to assign to role.
547
548
549
550
   * @param string $rid
   *   (optional) The role ID (machine name). Defaults to a random name.
   * @param string $name
   *   (optional) The label for the role. Defaults to a random string.
551
552
553
   * @param integer $weight
   *   (optional) The weight for the role. Defaults NULL so that entity_create()
   *   sets the weight to maximum + 1.
554
555
   *
   * @return string
556
   *   Role ID of newly created role, or FALSE if role creation failed.
557
   */
558
  protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
559
560
561
562
563
564
    // Generate a random, lowercase machine name if none was passed.
    if (!isset($rid)) {
      $rid = strtolower($this->randomName(8));
    }
    // Generate a random label.
    if (!isset($name)) {
565
566
567
      // In the role UI role names are trimmed and random string can start or
      // end with a space.
      $name = trim($this->randomString(8));
568
569
    }

570
    // Check the all the permissions strings are valid.
571
572
573
574
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

575
    // Create new role.
576
577
578
579
580
581
582
583
    $role = entity_create('user_role', array(
      'id' => $rid,
      'label' => $name,
    ));
    if (!is_null($weight)) {
      $role->set('weight', $weight);
    }
    $result = $role->save();
584

585
    $this->assertIdentical($result, SAVED_NEW, String::format('Created role ID @rid with name @name.', array(
586
587
      '@name' => var_export($role->label(), TRUE),
      '@rid' => var_export($role->id(), TRUE),
588
    )), 'Role');
589
590
591
592

    if ($result === SAVED_NEW) {
      // Grant the specified permissions to the role, if any.
      if (!empty($permissions)) {
593
        user_role_grant_permissions($role->id(), $permissions);
594
        $assigned_permissions = entity_load('user_role', $role->id())->permissions;
595
596
        $missing_permissions = array_diff($permissions, $assigned_permissions);
        if (!$missing_permissions) {
597
          $this->pass(String::format('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), 'Role');
598
599
        }
        else {
600
          $this->fail(String::format('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions))), 'Role');
601
602
        }
      }
603
      return $role->id();
604
605
606
607
608
609
    }
    else {
      return FALSE;
    }
  }

610
611
612
  /**
   * Check to make sure that the array of permissions are valid.
   *
613
614
615
616
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
617
   *
618
619
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
620
   */
621
  protected function checkPermissions(array $permissions, $reset = FALSE) {
622
    $available = &drupal_static(__FUNCTION__);
623
624

    if (!isset($available) || $reset) {
625
      $available = array_keys(module_invoke_all('permission'));
626
627
628
629
630
    }

    $valid = TRUE;
    foreach ($permissions as $permission) {
      if (!in_array($permission, $available)) {
631
        $this->fail(String::format('Invalid permission %permission.', array('%permission' => $permission)), 'Role');
632
633
634
635
636
637
        $valid = FALSE;
      }
    }
    return $valid;
  }

638
  /**
639
640
641
642
643
   * 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.
   *
644
   * Please note that neither the global $user nor the passed-in user object is
645
646
647
   * 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()),
648
   * e.g. to log in the same user again, then it must be re-assigned manually.
649
650
651
652
653
654
655
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
656
   *   $account = user_load($account->id());
657
658
   *   $account->pass_raw = $pass_raw;
   * @endcode
659
   *
660
   * @param \Drupal\Core\Session\AccountInterface $account
661
   *   User object representing the user to log in.
662
663
   *
   * @see drupalCreateUser()
664
   */
665
  protected function drupalLogin(AccountInterface $account) {
666
    if ($this->loggedInUser) {
667
668
669
670
      $this->drupalLogout();
    }

    $edit = array(
671
      'name' => $account->getUsername(),
672
      'pass' => $account->pass_raw
673
    );
674
    $this->drupalPostForm('user', $edit, t('Log in'));
675

676
677
    // @see WebTestBase::drupalUserIsLoggedIn()
    if (isset($this->session_id)) {
678
      $account->session_id = $this->session_id;
679
    }
680
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
681
    if ($pass) {
682
      $this->loggedInUser = $account;
683
      $this->container->set('current_user', $account);
684
685
686
      // @todo Temporary workaround for not being able to use synchronized
      //   services in non dumped container.
      $this->container->get('access_subscriber')->setCurrentUser($account);
687
    }
688
689
  }

690
691
692
  /**
   * Returns whether a given user account is logged in.
   *
693
   * @param \Drupal\user\UserInterface $account
694
695
696
697
698
699
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
    if (!isset($account->session_id)) {
      return FALSE;
    }
700
701
702
    // @see _drupal_session_read(). The session ID is hashed before being stored
    // in the database.
    return (bool) db_query("SELECT sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->session_id)))->fetchField();
703
704
  }

705
706
707
708
709
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
710
    return Crypt::hmacBase64($value, $this->session_id . $private_key);
711
712
  }

713
714
715
716
  /**
   * Logs a user out of the internal browser and confirms.
   *
   * Confirms logout by checking the login page.
717
   */
718
  protected function drupalLogout() {
719
720
721
    // 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.
722
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
723
724
725
    $this->assertResponse(200, 'User was logged out.');
    $pass = $this->assertField('name', 'Username field found.', 'Logout');
    $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
726

727
    if ($pass) {
728
729
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
730
      $this->loggedInUser = FALSE;
731
      $this->container->set('current_user', drupal_anonymous_user());
732
    }
733
734
  }

735
736
737
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
738
739
740
741
742
743
   * Installs Drupal with the installation profile specified in
   * \Drupal\simpletest\WebTestBase::$profile into the prefixed database.

   * Afterwards, installs any additional modules specified in the static
   * \Drupal\simpletest\WebTestBase::$modules property of each class in the
   * class hierarchy.
744
745
746
747
748
749
750
   *
   * 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() {
751
752
753
754
755
756
757
    // 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();

758
    // Define information about the user 1 account.
759
    $this->root_user = new UserSession(array(
760
      'uid' => 1,
761
762
763
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomName(),
764
    ));
765
766
767
768

    // Reset the static batch to remove Simpletest's batch operations.
    $batch = &batch_get();
    $batch = array();
769

770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
    // Get parameters for install_drupal() before removing global variables.
    $parameters = $this->installParameters();

    // 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.
    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');

    // All file system paths are created by System module during installation.
    // @see system_requirements()
    // @see TestBase::prepareEnvironment()
    $settings['settings']['file_public_path'] = (object) array(
      'value' => $this->public_files_directory,
      'required' => TRUE,
    );
786
787
788
789
790
791
792
793
    // Save the original site directory path, so that extensions in the
    // site-specific directory can still be discovered in the test site
    // environment.
    // @see \Drupal\Core\SystemListing::scan()
    $settings['settings']['test_parent_site'] = (object) array(
      'value' => $this->originalSite,
      'required' => TRUE,
    );
794
795
796
797
798
799
800
801
802
803
804
805
    // Add the parent profile's search path to the child site's search paths.
    // @see drupal_system_listing()
    $settings['conf']['simpletest.settings']['parent_profile'] = (object) array(
      'value' => $this->originalProfile,
      'required' => TRUE,
    );
    $this->writeSettings($settings);

    // Since Drupal is bootstrapped already, install_begin_request() will not
    // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to
    // reload the newly written custom settings.php manually.
    drupal_settings_initialize();
806

807
808
    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
809
    install_drupal($parameters);
810

811
812
813
814
815
816
817
818
819
820
821
822
823
    // Import new settings.php written by the installer.
    drupal_settings_initialize();
    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.
    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
824

825
    $this->rebuildContainer();
826

827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
    // 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.
    file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY);
    file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY);
    \Drupal::config('system.file')
      ->set('path.private', $this->private_files_directory)
      ->set('path.temporary', $this->temp_files_directory)
      ->save();

    // Manually configure the test mail collector implementation to prevent
    // tests from sending out e-mails and collect them in state instead.
    // While this should be enforced via settings.php prior to installation,
    // some tests expect to be able to test mail system implementations.
    \Drupal::config('system.mail')
      ->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')
      ->save();

846
847
848
    // Restore the original Simpletest batch.
    $batch = &batch_get();
    $batch = $this->originalBatch;
849

850
851
    // Collect modules to install.
    $class = get_class($this);
852
    $modules = array();
853
854
855
856
857
858
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
859
    if ($modules) {
860
      $modules = array_unique($modules);
861
      $success = \Drupal::moduleHandler()->install($modules, TRUE);
862
      $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
863
      $this->rebuildContainer();
864
    }
865

866
867
868
869
870
871
872
873
874
875
    // Like DRUPAL_BOOTSTRAP_CONFIGURATION above, any further bootstrap phases
    // are not re-executed by the installer, as Drupal is bootstrapped already.
    // 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.
    // Affects e.g. file_get_stream_wrappers().
    // @see _drupal_bootstrap_code()
    // @see _drupal_bootstrap_full()
    // @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

878
879
880
881
882
883
    // Temporary fix so that when running from run-tests.sh we don't get an
    // empty current path which would indicate we're on the home page.
    $path = current_path();
    if (empty($path)) {
      _current_path('run-tests');
    }
884
885
  }

886
887
888
889
890
891
892
893
  /**
   * Returns the parameters that will be used when Simpletest installs Drupal.
   *
   * @see install_drupal()
   * @see install_state_defaults()
   */
  protected function installParameters() {
    $connection_info = Database::getConnectionInfo();
894
895
896
897
898
    $driver = $connection_info['default']['driver'];
    unset($connection_info['default']['driver']);
    unset($connection_info['default']['namespace']);
    unset($connection_info['default']['pdo']);
    unset($connection_info['default']['init_commands']);
899
900
901
902
903
904
905
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
906
907
908
909
        'install_settings_form' => array(
          'driver' => $driver,
          $driver => $connection_info['default'],
        ),
910
911
912
913
914
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
            'name' => $this->root_user->name,
915
            'mail' => $this->root_user->getEmail(),
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
            'pass' => array(
              'pass1' => $this->root_user->pass_raw,
              'pass2' => $this->root_user->pass_raw,
            ),
          ),
          // form_type_checkboxes_value() requires NULL instead of FALSE values
          // for programmatic form submissions to disable a checkbox.
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );
    return $parameters;
  }

933
  /**
934
   * Rewrites the settings.php file of the test site.
935
   *
936
937
938
   * @param array $settings
   *   An array of settings to write out, in the format expected by
   *   drupal_rewrite_settings().
939
940
941
   *
   * @see drupal_rewrite_settings()
   */
942
  protected function writeSettings(array $settings) {
943
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
944
945
946
947
948
    $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);
949
950
951
    drupal_rewrite_settings($settings, $filename);
  }

952
  /**
953
   * Queues custom translations to be written to settings.php.
954
   *
955
956
   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
   * translations.
957
958
959
960
961
962
963
964
965
966
967
968
   *
   * @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
969
970
   *   Pass an empty array to remove all existing custom translations for the
   *   given $langcode.
971
972
   */
  protected function addCustomTranslations($langcode, array $values) {
973
974
975
976
977
978
979
980
981
982
983
984
    // 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;
        }
985
986
987
988
989
      }
    }
  }

  /**
990
991
992
993
   * Writes custom translations to the test site's settings.php.
   *
   * Use TestBase::addCustomTranslations() to queue custom translations before
   * calling this method.
994
995
   */
  protected function writeCustomTranslations() {
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
    $settings = array();
    foreach ($this->customTranslations as $langcode => $values) {
      $settings_key = 'locale_custom_strings_' . $langcode;

      // Update in-memory settings directly.
      $this->settingsSet($settings_key, $values);

      $settings['settings'][$settings_key] = (object) array(
        'value' => $values,
        'required' => TRUE,
      );
    }
    // Only rewrite settings if there are any translation changes to write.
    if (!empty($settings)) {
      $this->writeSettings($settings);
    }
1012
1013
  }

1014
  /**
1015
   * Overrides \Drupal\simpletest\TestBase::rebuildContainer().
1016
   */
1017
1018
  protected function rebuildContainer($environment = 'prod') {
    parent::rebuildContainer($environment);
1019
1020
1021
1022
1023
    // Make sure the url generator has a request object, otherwise calls to
    // $this->drupalGet() will fail.
    $this->prepareRequestForGenerator();
  }

1024
  /**
1025
   * Resets all data structures after having enabled new modules.
1026
   *
1027
1028
1029
   * This method is called by \Drupal\simpletest\WebTestBase::setUp() after
   * enabling the requested modules. It must be called again when additional
   * modules are enabled later.
1030
1031
   */
  protected function resetAll() {
1032
    // Clear all database and static caches and rebuild data structures.
1033
    drupal_flush_all_caches();
1034
    $this->container = \Drupal::getContainer();
1035

1036
    // Reset static variables and reload permissions.
1037
1038
1039
1040
    $this->refreshVariables();
    $this->checkPermissions(array(), TRUE);
  }

1041
  /**
1042
   * Refreshes in-memory configuration and state information.
1043
   *
1044
1045
   * Useful after a page request is made that changes configuration or state in
   * a different thread.
1046
   *
1047
   * In other words calling a settings page with $this->drupalPostForm() with a
1048
1049
   * changed value would update configuration to reflect that change, but in the
   * thread that made the call (thread running the test) the changed values
1050
1051
   * would not be picked up.
   *
1052
   * This method clears the cache and loads a fresh copy.
1053
   */
1054
  protected function refreshVariables() {
1055
1056
    // Clear the tag cache.
    drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache');
1057
    drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::deletedTags');
1058

1059
1060
    $this->container->get('config.factory')->reset();
    $this->container->get('state')->resetCache();
1061
1062
  }

1063
  /**
1064
1065
1066
1067
   * Cleans up after testing.
   *
   * Deletes created files and temporary files directory, deletes the tables
   * created by setUp(), and resets the database prefix.
1068
   */
1069
  protected function tearDown() {
1070
1071
1072
1073
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }
1074
    parent::tearDown();
1075

1076
1077
1078
    // Ensure that internal logged in variable and cURL options are reset.
    $this->loggedInUser = FALSE;
    $this->additionalCurlOptions = array();
1079

1080
1081
    // Close the CURL handler and reset the cookies array used for upgrade
    // testing so test classes containing multiple tests are not polluted.
1082
    $this->curlClose();
1083
    $this->curlCookies = array();
1084
1085
1086
  }

  /**
1087
   * Initializes the cURL connection.
1088
   *
1089
1090
1091
1092
   * If the simpletest_httpauth_credentials variable is set, this function will
   * add HTTP authentication headers. This is necessary for testing sites that
   * are protected by login credentials from public access.
   * See the description of $curl_options for other options.
1093
   */
1094
  protected function curlInitialize() {
1095
    global $base_url;
1096

1097
1098
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
1099
1100
1101
1102
1103
1104
1105

      // Some versions/configurations of cURL break on a NULL cookie jar, so
      // supply a real file.
      if (empty($this->cookieFile)) {
        $this->cookieFile = $this->public_files_directory . '/cookie.jar';
      }

Dries's avatar