drupal_web_test_case.php 75.4 KB
Newer Older
1 2 3 4
<?php
// $Id$

/**
5 6 7
 * Base class for Drupal tests.
 *
 * Do not extend this class, use one of the subclasses in this file.
8
 */
9
abstract class DrupalTestCase {
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
  /**
   * The test run ID.
   *
   * @var string
   */
  protected $testId;

  /**
   * The original database prefix, before it was changed for testing purposes.
   *
   * @var string
   */
  protected $originalPrefix = NULL;

  /**
   * The original file directory, before it was changed for testing purposes.
   *
   * @var string
   */
  protected $originalFileDirectory = NULL;

31
  /**
32
   * Time limit for the test.
33
   */
34
  protected $timeLimit = 180;
35

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
  /**
   * Current results of this test case.
   *
   * @var Array
   */
  public $results = array(
    '#pass' => 0,
    '#fail' => 0,
    '#exception' => 0,
  );

  /**
   * Assertions thrown in that test case.
   *
   * @var Array
   */
  protected $assertions = array();
53

54
  /**
55 56 57 58 59 60
   * This class is skipped when looking for the source of an assertion.
   *
   * When displaying which function an assert comes from, it's not too useful
   * to see "drupalWebTestCase->drupalLogin()', we would like to see the test
   * that called it. So we need to skip the classes defining these helper
   * methods.
61
   */
62
  protected $skipClasses = array(__CLASS__ => TRUE);
63

64 65 66
  /**
   * Constructor for DrupalWebTestCase.
   *
67
   * @param $test_id
68 69
   *   Tests with the same id are reported together.
   */
70 71
  public function __construct($test_id = NULL) {
    $this->testId = $test_id;
72 73 74
  }

  /**
75
   * Internal helper: stores the assert.
76 77
   *
   * @param $status
78 79
   *   Can be 'pass', 'fail', 'exception'.
   *   TRUE is a synonym for 'pass', FALSE for 'fail'.
80 81 82
   * @param $message
   *   The message string.
   * @param $group
83
   *   Which group this assert belongs to.
84
   * @param $caller
85
   *   By default, the assert comes from a function whose name starts with
86
   *   'test'. Instead, you can specify where this assert originates from
87
   *   by passing in an associative array as $caller. Key 'file' is
88 89 90
   *   the name of the source file, 'line' is the line number and 'function'
   *   is the caller function itself.
   */
91
  protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) {
92
    global $db_prefix;
93 94

    // Convert boolean status to string status.
95 96 97
    if (is_bool($status)) {
      $status = $status ? 'pass' : 'fail';
    }
98 99

    // Increment summary result counter.
100
    $this->results['#' . $status]++;
101 102 103 104

    // Get the function information about the call to the assertion method.
    if (!$caller) {
      $caller = $this->getAssertionCall();
105
    }
106 107

    // Switch to non-testing database to store results in.
108
    $current_db_prefix = $db_prefix;
109
    $db_prefix = $this->originalPrefix;
110 111

    // Creation assertion array that can be displayed while tests are running.
112 113
    $this->assertions[] = $assertion = array(
      'test_id' => $this->testId,
114
      'test_class' => get_class($this),
115 116
      'status' => $status,
      'message' => $message,
117 118 119 120
      'message_group' => $group,
      'function' => $caller['function'],
      'line' => $caller['line'],
      'file' => $caller['file'],
121
    );
122 123

    // Store assertion for display after the test has completed.
124 125 126
    db_insert('simpletest')
      ->fields($assertion)
      ->execute();
127 128

    // Return to testing prefix.
129
    $db_prefix = $current_db_prefix;
130 131 132 133 134 135 136 137
    // We do not use a ternary operator here to allow a breakpoint on
    // test failure.
    if ($status == 'pass') {
      return TRUE;
    }
    else {
      return FALSE;
    }
138 139
  }

140 141 142 143 144 145 146 147 148 149
  /**
   * Cycles through backtrace until the first non-assertion method is found.
   *
   * @return
   *   Array representing the true caller.
   */
  protected function getAssertionCall() {
    $backtrace = debug_backtrace();

    // The first element is the call. The second element is the caller.
150
    // We skip calls that occurred in one of the methods of our base classes
151
    // or in an assertion function.
152 153 154
   while (($caller = $backtrace[1]) &&
         ((isset($caller['class']) && isset($this->skipClasses[$caller['class']])) ||
           substr($caller['function'], 0, 6) == 'assert')) {
155 156 157 158 159 160 161
      // We remove that call.
      array_shift($backtrace);
    }

    return _drupal_get_last_caller($backtrace);
  }

162 163 164 165 166 167 168 169 170 171
  /**
   * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE).
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
172
   *   TRUE if the assertion succeeded, FALSE otherwise.
173 174
   */
  protected function assertTrue($value, $message = '', $group = 'Other') {
175
    return $this->assert((bool) $value, $message ? $message : t('Value is TRUE'), $group);
176 177 178 179 180 181 182 183 184 185 186 187
  }

  /**
   * Check to see if a value is false (an empty string, 0, NULL, or FALSE).
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
188
   *   TRUE if the assertion succeeded, FALSE otherwise.
189 190
   */
  protected function assertFalse($value, $message = '', $group = 'Other') {
191
    return $this->assert(!$value, $message ? $message : t('Value is FALSE'), $group);
192 193 194 195 196 197 198 199 200 201 202 203
  }

  /**
   * Check to see if a value is NULL.
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
204
   *   TRUE if the assertion succeeded, FALSE otherwise.
205 206
   */
  protected function assertNull($value, $message = '', $group = 'Other') {
207
    return $this->assert(!isset($value), $message ? $message : t('Value is NULL'), $group);
208 209 210 211 212 213 214 215 216 217 218 219
  }

  /**
   * Check to see if a value is not NULL.
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
220
   *   TRUE if the assertion succeeded, FALSE otherwise.
221 222
   */
  protected function assertNotNull($value, $message = '', $group = 'Other') {
223
    return $this->assert(isset($value), $message ? $message : t('Value is not NULL'), $group);
224 225 226 227 228 229 230 231 232 233 234 235 236 237
  }

  /**
   * Check to see if two values are equal.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
238
   *   TRUE if the assertion succeeded, FALSE otherwise.
239 240
   */
  protected function assertEqual($first, $second, $message = '', $group = 'Other') {
241
    return $this->assert($first == $second, $message ? $message : t('First value is equal to second value'), $group);
242 243 244 245 246 247 248 249 250 251 252 253 254 255
  }

  /**
   * Check to see if two values are not equal.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
256
   *   TRUE if the assertion succeeded, FALSE otherwise.
257 258
   */
  protected function assertNotEqual($first, $second, $message = '', $group = 'Other') {
259
    return $this->assert($first != $second, $message ? $message : t('First value is not equal to second value'), $group);
260 261 262 263 264 265 266 267 268 269 270 271 272 273
  }

  /**
   * Check to see if two values are identical.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
274
   *   TRUE if the assertion succeeded, FALSE otherwise.
275 276
   */
  protected function assertIdentical($first, $second, $message = '', $group = 'Other') {
277
    return $this->assert($first === $second, $message ? $message : t('First value is identical to second value'), $group);
278 279 280 281 282 283 284 285 286 287 288 289 290 291
  }

  /**
   * Check to see if two values are not identical.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
292
   *   TRUE if the assertion succeeded, FALSE otherwise.
293 294
   */
  protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') {
295
    return $this->assert($first !== $second, $message ? $message : t('First value is not identical to second value'), $group);
296 297 298 299 300 301 302 303 304 305 306 307 308
  }

  /**
   * Fire an assertion that is always positive.
   *
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
   *   TRUE.
   */
  protected function pass($message = NULL, $group = 'Other') {
309
    return $this->assert(TRUE, $message, $group);
310 311
  }

312
  /**
313
   * Fire an assertion that is always negative.
314
   *
315 316 317 318 319 320 321 322
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
   *   FALSE.
   */
  protected function fail($message = NULL, $group = 'Other') {
323
    return $this->assert(FALSE, $message, $group);
324 325 326 327 328 329 330 331 332
  }

  /**
   * Fire an error assertion.
   *
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
333
   * @param $caller
334
   *   The caller of the error.
335 336
   * @return
   *   FALSE.
337
   */
338
  protected function error($message = '', $group = 'Other', array $caller = NULL) {
339
    return $this->assert('exception', $message, $group, $caller);
340 341 342 343
  }

  /**
   * Run all tests in this class.
344
   */
345
  public function run() {
346 347 348 349
    // HTTP auth settings (<username>:<password>) for the simpletest browser
    // when sending requests to the test site.
    $this->httpauth_credentials = variable_get('simpletest_httpauth_credentials', NULL);

350 351 352 353 354 355
    set_error_handler(array($this, 'errorHandler'));
    $methods = array();
    // Iterate through all the methods in this class.
    foreach (get_class_methods(get_class($this)) as $method) {
      // If the current method starts with "test", run it - it's a test.
      if (strtolower(substr($method, 0, 4)) == 'test') {
356
        $this->setUp();
357 358 359 360 361 362 363
        try {
          $this->$method();
          // Finish up.
        }
        catch (Exception $e) {
          $this->exceptionHandler($e);
        }
364
        $this->tearDown();
365 366
      }
    }
367 368
    // Clear out the error messages and restore error handler.
    drupal_get_messages();
369 370 371 372 373 374
    restore_error_handler();
  }

  /**
   * Handle errors.
   *
375
   * Because this is registered in set_error_handler(), it has to be public.
376
   * @see set_error_handler
377
   *
378
   */
379
  public function errorHandler($severity, $message, $file = NULL, $line = NULL) {
380
    if ($severity & error_reporting()) {
381 382 383 384 385 386 387 388 389 390 391
      $error_map = array(
        E_STRICT => 'Run-time notice',
        E_WARNING => 'Warning',
        E_NOTICE => 'Notice',
        E_CORE_ERROR => 'Core error',
        E_CORE_WARNING => 'Core warning',
        E_USER_ERROR => 'User error',
        E_USER_WARNING => 'User warning',
        E_USER_NOTICE => 'User notice',
        E_RECOVERABLE_ERROR => 'Recoverable error',
      );
392 393 394

      $backtrace = debug_backtrace();
      $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace));
395 396
    }
    return TRUE;
397 398
  }

399 400 401 402 403
  /**
   * Handle exceptions.
   *
   * @see set_exception_handler
   */
404
  protected function exceptionHandler($exception) {
405 406 407 408 409 410 411 412 413
    $backtrace = $exception->getTrace();
    // Push on top of the backtrace the call that generated the exception.
    array_unshift($backtrace, array(
      'line' => $exception->getLine(),
      'file' => $exception->getFile(),
    ));
    $this->error($exception->getMessage(), 'Uncaught exception', _drupal_get_last_caller($backtrace));
  }

414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 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
  /**
   * Generates a random string of ASCII characters of codes 32 to 126.
   *
   * The generated string includes alpha-numeric characters and common misc
   * characters. Use this method when testing general input where the content
   * is not restricted.
   *
   * @param $length
   *   Length of random string to generate which will be appended to $db_prefix.
   * @return
   *   Randomly generated string.
   */
  public static function randomString($length = 8) {
    global $db_prefix;

    $str = '';
    for ($i = 0; $i < $length; $i++) {
      $str .= chr(mt_rand(32, 126));
    }
    return str_replace('simpletest', 's', $db_prefix) . $str;
  }

  /**
   * Generates a random string containing letters and numbers.
   *
   * The letters may be upper or lower case. This method is better for
   * restricted inputs that do not accept certain characters. For example,
   * when testing input fields that require machine readable values (ie without
   * spaces and non-standard characters) this method is best.
   *
   * @param $length
   *   Length of random string to generate which will be appended to $db_prefix.
   * @return
   *   Randomly generated string.
   */
  public static function randomName($length = 8) {
    global $db_prefix;

    $values = array_merge(range(65, 90), range(97, 122), range(48, 57));
    $max = count($values) - 1;
    $str = '';
    for ($i = 0; $i < $length; $i++) {
      $str .= chr($values[mt_rand(0, $max)]);
    }
    return str_replace('simpletest', 's', $db_prefix) . $str;
  }

}

/**
 * Test case for Drupal unit tests.
 *
 * These tests can not access the database nor files. Calling any Drupal
 * function that needs the database will throw exceptions. These include
 * watchdog(), drupal_function_exists(), module_implements(),
 * module_invoke_all() etc.
 */
class DrupalUnitTestCase extends DrupalTestCase {

  /**
   * Constructor for DrupalUnitTestCase.
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }
480

481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
  function setUp() {
    global $db_prefix, $conf;

    // Store necessary current values before switching to prefixed database.
    $this->originalPrefix = $db_prefix;
    $this->originalFileDirectory = file_directory_path();

    // Generate temporary prefixed database to ensure that tests have a clean starting point.
    $db_prefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
    $conf['file_directory_path'] = $this->originalFileDirectory . '/' . $db_prefix;

    // If locale is enabled then t() will try to access the database and
    // subsequently will fail as the database is not accessible.
    $module_list = module_list();
    if (isset($module_list['locale'])) {
      $this->originalModuleList = $module_list;
      unset($module_list['locale']);
      module_list(TRUE, FALSE, $module_list);
    }
  }

  function tearDown() {
    global $db_prefix, $conf;
    if (preg_match('/simpletest\d+/', $db_prefix)) {
      $conf['file_directory_path'] = $this->originalFileDirectory;
      // Return the database prefix to the original.
      $db_prefix = $this->originalPrefix;
      // Restore modules if necessary.
      if (isset($this->originalModuleList)) {
        module_list(TRUE, FALSE, $this->originalModuleList);
      }
    }
  }
}

/**
 * Test case for typical Drupal tests.
 */
class DrupalWebTestCase extends DrupalTestCase {
  /**
   * 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;

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

  /**
   * The content of the page currently loaded in the internal browser (plain text version).
   *
   * @var string
   */
  protected $plainTextContent;

  /**
   * The parsed version of the page.
   *
   * @var SimpleXMLElement
   */
  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.
   *
   * DrupalWebTestCase itself never sets this but always obeys what is set.
   */
  protected $additionalCurlOptions = array();

  /**
   * The original user, before it was changed to a clean uid = 1 for testing purposes.
   *
   * @var object
   */
  protected $originalUser = NULL;

  /**
   * HTTP authentication credentials (<username>:<password>).
   */
  protected $httpauth_credentials = NULL;

596 597 598 599 600 601 602 603 604 605
  /**
   * The current session name, if available.
   */
  protected $session_name = NULL;

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

606 607 608 609 610 611 612 613
  /**
   * Constructor for DrupalWebTestCase.
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629
  /**
   * Get a node from the database based on its title.
   *
   * @param title
   *   A node title, usually generated by $this->randomName().
   *
   * @return
   *   A node object matching $title.
   */
  function drupalGetNodeByTitle($title) {
    $nodes = node_load_multiple(array(), array('title' => $title));
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

630 631 632
  /**
   * Creates a node based on default settings.
   *
633 634
   * @param $settings
   *   An associative array of settings to change from the defaults, keys are
635
   *   node properties, for example 'body' => 'Hello, world!'.
636 637
   * @return
   *   Created node object.
638
   */
639
  protected function drupalCreateNode($settings = array()) {
640
    // Populate defaults array.
641
    $settings += array(
642 643 644
      'body'      => $this->randomName(32),
      'title'     => $this->randomName(8),
      'comment'   => 2,
645
      'changed'   => REQUEST_TIME,
646 647 648 649 650 651 652 653 654 655 656
      'format'    => FILTER_FORMAT_DEFAULT,
      'moderate'  => 0,
      'promote'   => 0,
      'revision'  => 1,
      'log'       => '',
      'status'    => 1,
      'sticky'    => 0,
      'type'      => 'page',
      'revisions' => NULL,
      'taxonomy'  => NULL,
    );
657 658 659 660

    // 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');
661
    }
662 663 664

    // Add the default teaser.
    if (!isset($settings['teaser'])) {
665
      $settings['teaser'] = $settings['body'];
666 667
    }

668 669 670 671 672 673 674 675 676 677 678 679 680
    // 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) {
        $settings['uid'] = $this->loggedInUser->uid;
      }
      else {
        global $user;
        $settings['uid'] = $user->uid;
      }
    }

    $node = (object) $settings;
681 682
    node_save($node);

683
    // Small hack to link revisions to our test user.
684 685 686 687
    db_update('node_revision')
      ->fields(array('uid' => $node->uid))
      ->condition('vid', $node->vid)
      ->execute();
688 689 690 691 692 693
    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
694
   * @param $settings
695 696
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
697 698
   * @return
   *   Created content type.
699
   */
700
  protected function drupalCreateContentType($settings = array()) {
701
    // Find a non-existent random type name.
702
    do {
703
      $name = strtolower($this->randomName(8));
704
    } while (node_type_get_type($name));
705

706
    // Populate defaults array.
707 708 709 710 711 712 713 714 715 716 717
    $defaults = array(
      'type' => $name,
      'name' => $name,
      'description' => '',
      'help' => '',
      'min_word_count' => 0,
      'title_label' => 'Title',
      'body_label' => 'Body',
      'has_title' => 1,
      'has_body' => 1,
    );
718
    // Imposed values for a custom type.
719 720 721 722 723 724 725 726 727 728 729
    $forced = array(
      'orig_type' => '',
      'old_type' => '',
      'module' => 'node',
      'custom' => 1,
      'modified' => 1,
      'locked' => 0,
    );
    $type = $forced + $settings + $defaults;
    $type = (object)$type;

730
    $saved_type = node_type_save($type);
731 732
    node_types_rebuild();

733
    $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type)));
734

735 736 737
    // Reset permissions so that permissions for this content type are available.
    $this->checkPermissions(array(), TRUE);

738 739 740 741 742 743
    return $type;
  }

  /**
   * Get a list files that can be used in tests.
   *
744 745 746 747 748 749
   * @param $type
   *   File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'.
   * @param $size
   *   File size in bytes to match. Please check the tests/files folder.
   * @return
   *   List of files that match filter.
750
   */
751
  protected function drupalGetTestFiles($type, $size = NULL) {
752 753 754 755
    $files = array();

    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
756
      // Use original file directory instead of one created during setUp().
757
      $path = $this->originalFileDirectory . '/simpletest';
758
      $files = file_scan_directory($path, '/' . $type . '\-.*/');
759 760 761 762

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
763
          $stats = stat($file->filepath);
764
          if ($stats['size'] != $size) {
765
            unset($files[$file->filepath]);
766 767 768 769 770 771 772 773 774
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
775
   * Compare two files based on size and file name.
776
   */
777
  protected function drupalCompareFiles($file1, $file2) {
778 779 780 781
    $compare_size = filesize($file1->filepath) - filesize($file2->filepath);
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
782 783
    }
    else {
784 785
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
786 787 788 789 790 791 792
    }
  }

  /**
   * Create a user with a given set of permissions. The permissions correspond to the
   * names given on the privileges page.
   *
793 794 795 796
   * @param $permissions
   *   Array of permission names to assign to user.
   * @return
   *   A fully loaded user object with pass_raw property, or FALSE if account
797 798
   *   creation fails.
   */
799
  protected function drupalCreateUser($permissions = array('access comments', 'access content', 'post comments', 'post comments without approval')) {
800
    // Create a role with the given permission set.
801
    if (!($rid = $this->drupalCreateRole($permissions))) {
802 803 804 805 806 807
      return FALSE;
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name']   = $this->randomName();
808
    $edit['mail']   = $edit['name'] . '@example.com';
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
    $edit['roles']  = array($rid => $rid);
    $edit['pass']   = user_password();
    $edit['status'] = 1;

    $account = user_save('', $edit);

    $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login'));
    if (empty($account->uid)) {
      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.
   *
828 829 830 831
   * @param $permissions
   *   Array of permission names to assign to role.
   * @return
   *   Role ID of newly created role, or FALSE if role creation failed.
832
   */
833
  protected function drupalCreateRole(array $permissions) {
834 835 836 837
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

838 839
    // Create new role.
    $role_name = $this->randomName();
840 841 842 843
    db_insert('role')
      ->fields(array('name' => $role_name))
      ->execute();
    $role = db_query('SELECT * FROM {role} WHERE name = :name', array(':name' => $role_name))->fetchObject();
844 845 846
    $this->assertTrue($role, t('Created role of name: @role_name, id: @rid', array('@role_name' => $role_name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role'));
    if ($role && !empty($role->rid)) {
      // Assign permissions to role and mark it for clean-up.
847
      $query = db_insert('role_permission')->fields(array('rid', 'permission'));
848
      foreach ($permissions as $permission_string) {
849 850 851 852
        $query->values(array(
          'rid' => $role->rid,
          'permission' => $permission_string,
        ));
853
      }
854 855
      $query->execute();
      $count = db_query('SELECT COUNT(*) FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchField();
856
      $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
857 858 859 860 861 862 863
      return $role->rid;
    }
    else {
      return FALSE;
    }
  }

864 865 866
  /**
   * Check to make sure that the array of permissions are valid.
   *
867 868 869 870 871 872
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
873
   */
874
  protected function checkPermissions(array $permissions, $reset = FALSE) {
875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
    static $available;

    if (!isset($available) || $reset) {
      $available = array_keys(module_invoke_all('perm'));
    }

    $valid = TRUE;
    foreach ($permissions as $permission) {
      if (!in_array($permission, $available)) {
        $this->fail(t('Invalid permission %permission.', array('%permission' => $permission)), t('Role'));
        $valid = FALSE;
      }
    }
    return $valid;
  }

891
  /**
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911
   * 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.
   *
   * Please note that neither the global $user nor the passed in user object is
   * 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()),
   * e.g. to login the same user again, then it must be re-assigned manually.
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
   *   $account = user_load($account->uid);
   *   $account->pass_raw = $pass_raw;
   * @endcode
912
   *
913 914
   * @param $user
   *   User object representing the user to login.
915 916
   *
   * @see drupalCreateUser()
917
   */
918
  protected function drupalLogin(stdClass $user) {
919
    if ($this->loggedInUser) {
920 921 922 923 924 925 926 927 928
      $this->drupalLogout();
    }

    $edit = array(
      'name' => $user->name,
      'pass' => $user->pass_raw
    );
    $this->drupalPost('user', $edit, t('Log in'));

929 930 931
    // If a "log out" link appears on the page, it is almost certainly because
    // the login was successful.
    $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $user->name)), t('User login'));
932

933 934 935
    if ($pass) {
      $this->loggedInUser = $user;
    }
936 937
  }

938 939 940 941 942 943 944 945
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
    return md5($this->session_id . $value . $private_key);
  }

946 947 948
  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
949
  protected function drupalLogout() {
950
    // Make a request to the logout page.
951
    $this->drupalGet('user/logout');
952 953 954 955 956 957

    // Load the user page, the idea being if you were properly logged out you should be seeing a login screen.
    $this->drupalGet('user');
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

958 959 960
    if ($pass) {
      $this->loggedInUser = FALSE;
    }
961 962 963
  }

  /**
964 965 966 967
   * Generates a random database prefix, runs the install scripts on the
   * prefixed database and enable the specified modules. After installation
   * many caches are flushed and the internal browser is setup so that the
   * page requests will run on the new prefix. A temporary files directory
968
   * is created with the same name as the database prefix.
969
   *
970
   * @param ...
971
   *   List of modules to enable for the duration of the test.
972
   */
973
  protected function setUp() {
974
    global $db_prefix, $user, $language;
975 976

    // Store necessary current values before switching to prefixed database.
977 978
    $this->originalLanguage = $language;
    $this->originalLanguageDefault = variable_get('language_default');
979
    $this->originalPrefix = $db_prefix;
980
    $this->originalFileDirectory = file_directory_path();
981
    $clean_url_original = variable_get('clean_url', 0);
982 983

    // Generate temporary prefixed database to ensure that tests have a clean starting point.
984
    $db_prefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
985

986
    include_once DRUPAL_ROOT . '/includes/install.inc';
987
    drupal_install_system();
988

989 990
    $this->preloadRegistry();

991
    // Add the specified modules to the list of modules in the default profile.
992
    $args = func_get_args();
993
    $modules = array_unique(array_merge(drupal_get_profile_modules('default', 'en'), $args));
994
    drupal_install_modules($modules, TRUE);
995

996
    // Because the schema is static cached, we need to flush
997
    // it between each run. If we don't, then it will contain
998 999 1000
    // stale data for the previous run's database prefix and all
    // calls to it will fail.
    drupal_get_schema(NULL, TRUE);
1001

1002
    // Run default profile tasks.
1003 1004
    $task = 'profile';
    default_profile_tasks($task, '');
1005 1006

    // Rebuild caches.
1007
    node_types_rebuild();
1008 1009
    actions_synchronize();
    _drupal_flush_css_js();
1010
    $this->refreshVariables();
1011
    $this->checkPermissions(array(), TRUE);
1012

1013 1014 1015
    // Log in with a clean $user.
    $this->originalUser = $user;
    drupal_save_session(FALSE);
1016
    $user = user_load(1);
1017

1018
    // Restore necessary variables.
1019 1020 1021
    variable_set('install_profile', 'default');
    variable_set('install_task', 'profile-finished');
    variable_set('clean_url', $clean_url_original);
1022
    variable_set('site_mail', 'simpletest@example.com');
1023 1024 1025
    // Set up English language.
    unset($GLOBALS['conf']['language_default']);
    $language = language_default();
1026

1027 1028 1029 1030
    // Make sure our drupal_mail_wrapper function is called instead of the
    // default mail handler.
    variable_set('smtp_library', drupal_get_path('module', 'simpletest') . '/drupal_web_test_case.php');

1031
    // Use temporary files directory with the same prefix as database.
1032
    variable_set('file_directory_path', $this->originalFileDirectory . '/' . $db_prefix);
1033
    $directory = file_directory_path();
1034 1035 1036
    // Create the files directory.
    file_check_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);

1037
    set_time_limit($this->timeLimit);
1038 1039
  }

1040
  /**
1041 1042 1043 1044
   * This method is called by DrupalWebTestCase::setUp, and preloads the
   * registry from the testing site to cut down on the time it takes to
   * setup a clean environment for the current test run.
   */
1045 1046 1047 1048 1049
  protected function preloadRegistry() {
    db_query('INSERT INTO {registry} SELECT * FROM ' . $this->originalPrefix . 'registry');
    db_query('INSERT INTO {registry_file} SELECT * FROM ' . $this->originalPrefix . 'registry_file');
  }

1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061
  /**
   * Refresh the in-memory set of variables. Useful after a page request is made
   * that changes a variable in a different thread.
   *
   * In other words calling a settings page with $this->drupalPost() with a changed
   * value would update a variable to reflect that change, but in the thread that
   * made the call (thread running the test) the changed variable would not be
   * picked up.
   *
   * This method clears the variables cache and loads a fresh copy from the database
   * to ensure that the most up-to-date set of variables is loaded.
   */
1062
  protected function refreshVariables() {
1063 1064 1065 1066 1067
    global $conf;
    cache_clear_all('variables', 'cache');
    $conf = variable_init();
  }

1068 1069 1070 1071
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
1072
  protected function tearDown() {
1073
    global $db_prefix, $user, $language;
1074 1075 1076 1077 1078 1079 1080

    $emailCount = count(variable_get('simpletest_emails', array()));
    if ($emailCount) {
      $message = format_plural($emailCount, t('!count e-mail was sent during this test.'), t('!count e-mails were sent during this test.'), array('!count' => $emailCount));
      $this->pass($message, t('E-mail'));
    }

1081 1082
    if (preg_match('/simpletest\d+/', $db_prefix)) {
      // Delete temporary files directory and reset files directory path.
1083
      file_unmanaged_delete_recursive(file_directory_path());
1084
      variable_set('file_directory_path', $this->originalFileDirectory);
1085

1086
      // Remove all prefixed tables (all the tables in the schema).
1087 1088 1089 1090 1091
      $schema = drupal_get_schema(NULL, TRUE);
      $ret = array();
      foreach ($schema as $name => $table) {
        db_drop_table($ret, $name);
      }
1092 1093

      // Return the database prefix to the original.
1094
      $db_prefix = $this->originalPrefix;
1095

1096 1097 1098 1099
      // Return the user to the original one.
      $user = $this->originalUser;
      drupal_save_session(TRUE);

1100
      // Ensure that internal logged in variable and cURL options are reset.
1101
      $this->loggedInUser = FALSE;
1102
      $this->additionalCurlOptions = array();
1103

1104 1105
      // Reload module list and implementations to ensure that test module hooks
      // aren't called after tests.
1106
      module_list(TRUE);
1107
      module_implements(MODULE_IMPLEMENTS_CLEAR_CACHE);
1108

1109 1110 1111
      // Reset the Field API.
      field_cache_clear();

1112 1113
      // Rebuild caches.
      $this->refreshVariables();
1114

1115
      // Reset language.
1116 1117 1118 1119
      $language = $this->originalLanguage;
      if ($this->originalLanguageDefault) {
        $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault;
      }
1120

1121
      // Close the CURL handler.
1122 1123 1124 1125 1126
      $this->curlClose();
    }
  }

  /**
1127
   * Initializes the cURL connection.
1128
   *
1129 1130 1131 1132
   * 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.
1133
   */
1134
  protected function curlInitialize() {
1135
    global $base_url, $db_prefix;
1136 1137 1138 1139
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
      $curl_options = $this->additionalCurlOptions + array(
        CURLOPT_COOKIEJAR => $this->cookieFile,
1140 1141
        CURLOPT_URL => $base_url,
        CURLOPT_FOLLOWLOCATION => TRUE,
1142
        CURLOPT_MAXREDIRS => 5,
1143
        CURLOPT_RETURNTRANSFER => TRUE,
1144 1145
        CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https.
        CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https.
1146
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
1147
      );
1148 1149
      if (preg_match('/simpletest\d+/', $db_prefix, $matches)) {
        $curl_options[CURLOPT_USERAGENT] = $matches[0];
1150
      }
1151 1152
      if (isset($this->httpauth_credentials)) {
        $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials;
1153
      }
1154
      curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
1155 1156 1157

      // By default, the child session name should be the same as the parent.
      $this->session_name = session_name();
1158 1159 1160 1161
    }
  }

  /**
1162
   * Performs a cURL exec with the specified options after calling curlConnect().
1163
   *
1164 1165
   * @param $curl_options
   *   Custom cURL options.
1166 1167
   * @return
   *   Content returned from the exec.
1168 1169
   */
  protected function curlExec($curl_options) {
1170
    $this->curlInitialize();
1171
    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
1172 1173 1174 1175 1176 1177 1178 1179
    if (!empty($curl_options[CURLOPT_POST])) {
      // This is a fix for the Curl library to prevent Expect: 100-continue
      // headers in POST requests, that may cause unexpected HTTP response
      // codes from some webservers (like lighttpd that returns a 417 error
      // code). It is done by setting an empty "Expect" header field that is
      // not overwritten by Curl.
      $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:';
    }
1180
    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
1181 1182 1183

    // Reset headers and the session ID.
    $this->session_id = NULL;
1184
    $this->headers = array();
1185

1186
    $this->drupalSetContent(curl_exec($this->curlHandle), curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
1187 1188 1189 1190 1191 1192 1193 1194
    $message_vars = array(
      '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'),
      '@url' => $url,
      '@status' => curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE),
      '!length' => format_size(strlen($this->content))
    );
    $message = t('!method @url returned @status (!length).', $message_vars);
    $this->assertTrue($this->content !== FALSE, $message, t('Browser'));
1195
    return $this->drupalGetContent();
1196 1197
  }

1198 1199 1200 1201 1202
  /**
   * Reads headers and registers errors received from the tested site.
   *
   * @see _drupal_log_error().
   *
1203 1204 1205 1206
   * @param $curlHandler
   *   The cURL handler.
   * @param $header
   *   An header.
1207
   */
1208
  protected function curlHeaderCallback($curlHandler, $header) {
1209
    $this->headers[] = $header;
1210

1211 1212 1213 1214 1215 1216 1217
    // Errors are being sent via X-Drupal-Assertion-* headers,
    // generated by _drupal_log_error() in the exact form required
    // by DrupalWebTestCase::error().
    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
      // Call DrupalWebTestCase::error() with the parameters from the header.
      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
    }
1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228

    // Save the session cookie, if set.
    if (preg_match('/^Set-Cookie: ' . preg_quote($this->session_name) . '=([a-z90-9]+)/', $header, $matches)) {
      if ($matches[1] != 'deleted') {
        $this->session_id = $matches[1];
      }
      else {
        $this->session_id = NULL;
      }
    }

1229 1230 1231 1232
    // This is required by cURL.
    return strlen($header);
  }

1233 1234 1235 1236
  /**
   * Close the cURL handler and unset the handler.
   */
  protected function curlClose() {
1237 1238 1239
    if (isset($this->curlHandle)) {
      curl_close($this->curlHandle);
      unset($this->curlHandle);
1240 1241 1242 1243
    }
  }

  /**
1244
   * Parse content returned from curlExec using DOM and SimpleXML.
1245
   *
1246 1247
   * @return
   *   A SimpleXMLElement or FALSE on failure.
1248 1249 1250
   */
  protected function parse() {
    if (!$this->elements) {
1251
      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
1252
      // them.
1253
      @$htmlDom = DOMDocument::loadHTML($this->content);
1254
      if ($htmlDom) {
1255
        $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser'));
1256 1257 1258 1259 1260
        // It's much easier to work with simplexml than DOM, luckily enough
        // we can just simply import our DOM tree.
        $this->elements = simplexml_import_dom($htmlDom);
      }
    }
1261 1262 1263
    if (!$this->elements) {
      $this->fail(t('Parsed page successfully.'), t('Browser'));
    }
1264

1265 1266 1267 1268 1269 1270
    return $this->elements;
  }

  /**
   * Retrieves a Drupal path or an absolute path.
   *
1271
   * @param $path
1272
   *   Drupal path or URL to load into internal browser
1273
   * @param $options
1274
   *   Options to be forwarded to url().
1275 1276 1277
   * @param $headers
   *   An array containing additional HTTP request headers, each formatted as
   *   "name: value".
1278
   * @return
1279
   *   The retrieved HTML string, also available as $this->drupalGetContent()
1280
   */
1281
  protected function drupalGet($path, array $options = array(), array $headers = array()) {
1282
    $options['absolute'] = TRUE;
1283

1284 1285
    // We re-using a CURL connection here. If that connection still has certain
    // options set, it might change the GET into a POST. Make sure we clear out
1286
    // previous options.
1287
    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
1288
    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
1289 1290 1291 1292 1293

    // Replace original page output with new output from redirected page(s).
    if (($new = $this->checkForMetaRefresh())) {
      $out = $new;
    }
1294
    return $out;
1295 1296 1297
  }

  /**
1298 1299
   * Execute a POST request on a Drupal page.
   * It will be done as usual POST request with SimpleBrowser.
1300
   *
1301
   * @param $path
1302
   *   Location of the post form. Either a Drupal path or an absolute path or
1303 1304 1305 1306 1307 1308 1309 1310 1311 1312
   *   NULL to post to the current page. For multi-stage forms you can set the
   *   path to NULL and have it post to the last received page. Example:
   *
   *   // First step in form.
   *   $edit = array(...);
   *   $this->drupalPost('some_url', $edit, t('Save'));
   *
   *   // Second step in form.
   *   $edit = array(...);
   *   $this->drupalPost(NULL, $edit, t('Save'));
1313
   * @param  $edit
1314
   *   Field data in an associative array. Changes the current input fields
1315
   *   (where possible) to the values indicated. A checkbox can be set to
1316 1317 1318
   *   TRUE to be checked and FALSE to be unchecked. Note that when a form
   *   contains file upload fields, other fields cannot start with the '@'
   *   character.