Skip to content
mollom.test 204 KiB
Newer Older
Dries Buytaert's avatar
Dries Buytaert committed
<?php

/**
 * @file
 * Tests for the Mollom module.
 */

/**
 * Indicates that Mollom testing keys are reseller keys.
 *
 * If the above keys are reseller keys, make sure to change this value to TRUE.
 * If you set this to TRUE and you are testing with non-reseller keys, the
 * tests will fail due to unprivileged API access.
 */
define('MOLLOM_TEST_RESELLER_KEY', FALSE);

/**
 * Common base test class for Mollom tests.
 */
class MollomWebTestCase extends DrupalWebTestCase {
  protected $profile = 'testing';
Dries Buytaert's avatar
Dries Buytaert committed

  /**
   * The text the user should see when they are blocked from submitting a form
   * because the Mollom servers are unreachable.
   */
  protected $fallback_message = 'The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.';

  /**
   * The text the user should see if there submission was determinted to be spam.
   */
  protected $spam_message = 'Your submission has triggered the spam filter and will not be accepted.';

  /**
   * The text the user should see if they did not fill out the CAPTCHA correctly.
   */
  protected $incorrect_message = 'The word verification was not completed correctly. Please complete this new word verification and try again.';
Dries Buytaert's avatar
Dries Buytaert committed

  /**
   * The text the user should see if the textual analysis was unsure about the
   * content.
   */
  protected $unsure_message = "To complete this form, please complete the word verification below.";
Dries Buytaert's avatar
Dries Buytaert committed

  /**
   * The text the user should see if the textual analysis determined that there
   * was profane content.
   */
  protected $profanity_message = "Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.";

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * The public key used during testing.
   */
  protected $public_key;
Dries Buytaert's avatar
Dries Buytaert committed

  /**
   * The private key used during testing.
   */
  protected $private_key;
Dries Buytaert's avatar
Dries Buytaert committed

  /**
   * The Mollom client class implementation to use.
   *
   * By default, we use MollomDrupalTest and test against production Mollom
   * testing servers.
   * Assign MollomDrupalTestLocal to test against local dummy/fake REST server.
   *
   * @see mollom.drupal.inc
   *
   * @var string
   */
  protected $mollomClass = 'MollomDrupalTest';
  /**
   * Flag indicating whether to automatically create testing API keys.
   *
   * If mollom_testing_mode is enabled, Mollom module automatically uses the
   * MollomDrupalTest client implementation. This implementation automatically
   * creates testing API keys when being instantiated (and ensures to re-create
   * testing API keys in case they vanish). The behavior is executed by default,
   * but depends on the 'mollom_testing_create_keys' variable being TRUE.
   *
   * Some functional test cases verify that expected errors are displayed in
   * case no or invalid API keys are configured. For such test cases, set this
   * flag to FALSE to skip the automatic creation of testing keys.
   *
   * @see MollomDrupalTest::$createKeys
   * @see MollomDrupalTest::createKeys()
   */
  protected $createKeys = TRUE;

  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    // Hide this base class in assertions.
    $this->skipClasses[__CLASS__] = TRUE;
  }

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * Set up an administrative user account and testing keys.
   */

    // Grab the configured endpoints from the variables because the
    // parent::setup() call will clear our variables.
    $mollom_api_endpoint = variable_get('mollom_api_endpoint', '');
    $mollom_test_api_endpoint = variable_get('mollom_test_api_endpoint', '');

    // Re-initialize stored session_id and watchdog messages.
    $this->resetResponseID();
    $modules = (isset($modules[0]) ? $modules[0] : array());
    // Automatically enable local testing server implementation.
    if (strstr($this->mollomClass, 'Local') && !in_array('mollom_test_server', $modules)) {
      $modules[] = 'mollom_test_server';
    // If not explicitly disabled by a test, setup with Mollom.
    if (empty($this->disableDefaultSetup)) {
      $modules[] = 'mollom';
    // Database logging is unconditionally required to assert watchdog messages.
    $modules[] = 'dblog';
    parent::setUp($modules);

    // Save the Mollom client implementation to use for running the tests.
    variable_set('mollom_class', $this->mollomClass);
    // Save the flag telling the testing client implementation whether to
    // automatically create testing API keys.
    variable_set('mollom_testing_create_keys', $this->createKeys);

    // Set the API server endpoints.
    if ($mollom_api_endpoint) {
      variable_set('mollom_api_endpoint', $mollom_api_endpoint);
    }
    if ($mollom_test_api_endpoint) {
      variable_set('mollom_test_api_endpoint', $mollom_test_api_endpoint);
    }

    // drupal_set_message() starts a session, which disables page caching, and
    // in turn, page/form cache related tests would not behave correctly.
    variable_set('mollom_testing_mode_omit_warning', TRUE);

    // Log all messages.
    variable_set('mollom_log_minimum_severity', WATCHDOG_DEBUG);

    // D7's new default theme Bartik is bogus in various locations, which leads
    // to failing tests.
    // @todo Remove this override.
    variable_set('theme_default', 'garland');

    // If not explicitly disabled by a test, setup and validate testing keys,
    // and create a default admin user.
    if (empty($this->disableDefaultSetup)) {
        'administer content types',
        'administer permissions',
        $permissions[] = 'access comments';
        $permissions[] = 'post comments';
        $permissions[] = 'skip comment approval';
        $permissions[] = 'administer comments';
      }
      $this->admin_user = $this->drupalCreateUser($permissions);
Dries Buytaert's avatar
Dries Buytaert committed

      if ($this->createKeys) {
        $this->setKeys();
        $this->assertValidKeys();
      }
Dries Buytaert's avatar
Dries Buytaert committed
  }

  function tearDown() {
    // Delete the testing site.
    // Not (always) possible when working with local testing server, since
    // getServerRecord() removes server records upon retrieval, so the site
    // record may no longer exist.
    // @todo Only remove keys after running the last test in a test case.
    /*
    if ($this->mollomClass == 'MollomDrupalTest') {
Dries Buytaert's avatar
Dries Buytaert committed
    // Capture any (remaining) watchdog messages.
    $this->assertMollomWatchdogMessages();
    parent::tearDown();
  }

  /**
   * Assert any watchdog messages based on their severity.
   *
   * This function can be (repeatedly) invoked to assert new watchdog messages.
   * All watchdog messages with a higher severity than WATCHDOG_NOTICE are
Dries Buytaert's avatar
Dries Buytaert committed
   *
   * @param $max_severity
   *   (optional) A maximum watchdog severity level message constant that log
   *   messages must have to pass the assertion. All messages with a higher
   *   severity will fail. Defaults to WATCHDOG_NOTICE. If a severity level
   *   higher than WATCHDOG_NOTICE is passed, then at least one severe message
   *   is expected.
Dries Buytaert's avatar
Dries Buytaert committed
   *
   * @todo Add this to Drupal core.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function assertMollomWatchdogMessages($max_severity = WATCHDOG_NOTICE) {
    // Ensure that all messages have been written before attempting to verify
    // them. Actions executed within the test class may lead to log messages,
    // but those get only logged when hook_exit() is triggered.
    // mollom.module may not be installed by a test and thus not loaded yet.
    drupal_load('module', 'mollom');
    mollom_log_write();

Dries Buytaert's avatar
Dries Buytaert committed
    module_load_include('inc', 'dblog', 'dblog.admin');

    $this->messages = array();
    $query = db_select('watchdog', 'w')
      ->fields('w')
      ->orderBy('w.timestamp', 'ASC');

    // The comparison logic applied in this function is a bit confusing, since
    // the values of watchdog severity level constants defined by RFC 3164 are
    // negated to their actual "severity level" meaning:
    // WATCHDOG_EMERGENCY is 0, WATCHDOG_NOTICE is 5, WATCHDOG_DEBUG is 7.

    $fail_expected = ($max_severity < WATCHDOG_NOTICE);
    $had_severe_message = FALSE;
    foreach ($query->execute() as $row) {
      $this->messages[$row->wid] = $row;
      // Only messages with a maximum severity of $max_severity or less severe
      // messages must pass. More severe messages need to fail. See note about
      // severity level constant values above.
      $output = theme_dblog_message(array('event' => $row, 'link' => FALSE));
      if ($row->severity >= $max_severity) {
        // Visually separate debug log messages from other messages.
        if ($row->severity == WATCHDOG_DEBUG) {
          $this->error($output, 'User notice');
        }
        else {
          $this->pass(check_plain($row->type) . ': ' . $output, t('Watchdog'));
        $this->fail(check_plain($row->type) . ': ' . $output, t('Watchdog'));
Dries Buytaert's avatar
Dries Buytaert committed
      }
      // In case a severe message is expected, non-severe messages always pass,
      // since we would trigger a false positive test failure otherwise.
      // However, in order to actually assert the expectation, there must have
      // been at least one severe log message.
      $had_severe_message = ($had_severe_message || $row->severity < WATCHDOG_NOTICE);
    }
    // Assert that there was a severe message, in case we expected one.
    if ($fail_expected && !$had_severe_message) {
      $this->fail(t('Severe log message was found.'), t('Watchdog'));
    }
    // Delete processed watchdog messages.
    if (!empty($this->messages)) {
      $seen_ids = array_keys($this->messages);
      db_delete('watchdog')->condition('wid', $seen_ids)->execute();
    }
  }

  /**
   * Assert that the Mollom session id remains the same.
   *
   * The Mollom session id is only known to one server. If we are communicating
   * with a different Mollom server (due to a refreshed server list or being
   * redirected), then we will get a new session_id.
   *
   * @param $type
   *   The type of ID to assert; e.g., 'contentId', 'captchaId'.
   * @param $id
   *   The ID of $type in the last request, as returned from Mollom.
   * @param $new_expected
   *   (optional) Boolean indicating whether a new ID is expected; e.g., after
   *   incorrectly solving a CAPTCHA.
   */
  protected function assertResponseID($type, $id, $new_expected = FALSE) {
    if (!isset($this->responseIds[$type]) || $new_expected) {
      // Use assertTrue() instead of pass(), to test !empty().
      $this->assertTrue($id, t('New %type: %id', array(
        '%type' => $type,
        '%id' => $id,
      )));
      $this->responseIds[$type] = $id;
      $this->assertSame($type, $id, $this->responseIds[$type]);
Dries Buytaert's avatar
Dries Buytaert committed
    }
    return $this->responseIds[$type];
  }

  /**
   * Reset the statically cached Mollom session id.
   *
   * @param $type
   *   The type of ID to reset; e.g., 'contentId', 'captchaId'.
  protected function resetResponseID($type = NULL) {
    if (isset($type)) {
      unset($this->responseIds[$type]);
    }
    else {
      unset($this->responseIds);
    }
  }

  /**
   * Assert a Mollom session id in a form.
   *
   * This is a wrapper around assertResponseID() allows to assert that a proper
   * Mollom session id is found in the form contained in the internal browser
   * output. The usual flow is:
   * - drupalGet() or drupalPost() requests or submits a form.
   * - drupalGet() and drupalPost() invoke assertMollomWatchdogMessages()
   *   internally, which records all new watchdog messages.
   * - This function, assertResponseIDInForm(), is invoked to assert that there
   *   is a Mollom session id and, depending on the recorded watchdog messages,
   *   that it either equals the last known session id or the new session id is
   *   used for future comparisons in case of a server redirect.
   * - The return value of this function is used to invoke assertMollomData(),
   *   to verify that the proper session id was stored in the database.
   *
   * @param $type
   *   The type of ID to assert; e.g., 'contentId', 'captchaId'.
   * @param $new_expected
   *   (optional) Boolean indicating whether a new ID is expected; e.g., after
   *   incorrectly solving a CAPTCHA.
  protected function assertResponseIDInForm($type, $new_expected = FALSE) {
    $id = $this->getFieldValueByName('mollom[' . $type . ']');
    return $this->assertResponseID($type, $id, $new_expected);
   * Instantiate a Mollom client and make it available on $this->mollom;
   *
   * @param $force
   *   (Optional) If true, then a new class is always instantiated.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function getClient($force = FALSE) {
    if ($force || !isset($this->mollom)) {
      // mollom.module may not be enabled in the parent site executing the test.
      drupal_load('module', 'mollom');
      $this->mollom = mollom($this->mollomClass, $force);
   * Setup Mollom API keys for testing.
   * New keys are only created if MollomWebTestCase::$createKeys or respectively
   * the 'mollom_testing_create_keys' variable is set to TRUE.
   *
   * @param bool $once
   *   (optional) Whether to disable the 'mollom_testing_create_keys' variable
   *   after the first call (and thus omit API key verifications on every page
   *   request). Defaults to FALSE; i.e., API keys are verified repetitively.
   *
   * @see MollomWebTestCase::$createKeys
   * @see MollomDrupalTest::__construct()
   * @see MollomDrupalTest::createKeys()
   */
  protected function setKeys($once = FALSE) {
    // Instantiate a Mollom client class.
    // Depending on MollomWebTestCase::$createKeys and ultimately the
    // 'mollom_testing_create_keys' variable, MollomDrupalTest::__construct()
    // will automatically setup testing API keys.
    $this->getClient();
Dries Buytaert's avatar
Dries Buytaert committed

    // Make API keys available to test methods.
    if (!empty($this->mollom->publicKey)) {
      $this->publicKey = $this->mollom->publicKey;
      $this->privateKey = $this->mollom->privateKey;

      // Multiple tests might be executed in a single request. Every test sets
      // up a new child site from scratch. The Mollom class with testing API
      // keys still exists in the test, but the configuration is gone.
      $this->mollom->saveKeys();
    if ($once) {
      variable_set('mollom_testing_create_keys', FALSE);
    }
   * Calls _mollom_status() directly to verify that current API keys are valid.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function assertValidKeys() {
    $this->assertMollomWatchdogMessages();
    $this->assertIdentical($status['isVerified'], TRUE, t('Mollom servers can be contacted and testing API keys are valid.'));
  /**
   * Deletes the current testing site.
   */
  protected function deleteKeys() {
    if (!empty($this->mollom->publicKey)) {
      $this->mollom->deleteSite($this->mollom->publicKey);
    }
    unset($this->publicKey, $this->privateKey, $this->mollom);
  /**
   * Saves a mollom_form entity to protect a given form with Mollom.
   *
   * @param string $form_id
   *   The form id to protect.
   * @param int $mode
   *   The protection mode. Defaults to MOLLOM_MODE_ANALYSIS.
   * @param array $values
   *   (optional) An associative array of properties to additionally set on the
   *   mollom_form entity.
   *
   * @return int
   *   The save status, as returned by mollom_form_save().
   */
  protected function setProtection($form_id, $mode = MOLLOM_MODE_ANALYSIS, $values = array()) {
    if (!$mollom_form = mollom_form_load($form_id)) {
      $mollom_form = mollom_form_new($form_id);
    }
    $mollom_form['mode'] = $mode;
    if ($values) {
      foreach ($values as $property => $value) {
        $mollom_form[$property] = $value;
      }
    }
    $status = mollom_form_save($mollom_form);
    return $status;
  }

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * Configure Mollom protection for a given form.
   *
   * @param $form_id
   *   The form id to configure.
Dries Buytaert's avatar
Dries Buytaert committed
   * @param $fields
   *   (optional) A list of form elements to enable for text analysis. If
   *   omitted and the form registers individual elements, all fields are
   *   enabled by default.
   * @param $edit
   *   (optional) An array of POST data to pass through to drupalPost() when
   *   configuring the form's protection.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function setProtectionUI($form_id, $mode = MOLLOM_MODE_ANALYSIS, $fields = NULL, $edit = array()) {
    // Always start from overview page, also to make debugging easier.
    $this->drupalGet('admin/config/content/mollom');
Lisa Backer's avatar
Lisa Backer committed
    // Determine whether the form is already protected.
    $exists = db_query_range('SELECT 1 FROM {mollom_form} WHERE form_id = :form_id', 0, 1, array(':form_id' => $form_id))->fetchField();
Dries Buytaert's avatar
Dries Buytaert committed
    // Add a new form.
    if (!$exists) {
      $this->clickLink(t('Add form'));
Dries Buytaert's avatar
Dries Buytaert committed
        'mollom[form_id]' => $form_id,
      );
      $this->drupalPost(NULL, $add_form_edit, t('Next'));
Dries Buytaert's avatar
Dries Buytaert committed
    }
    // Edit an existing form.
    else {
      $this->assertLinkByHref('admin/config/content/mollom/manage/' . $form_id);
      $this->drupalGet('admin/config/content/mollom/manage/' . $form_id);
    $form_list = mollom_form_list();
    $form_info = mollom_form_info($form_id, $form_list[$form_id]['module']);
    if (!empty($form_info['elements'])) {
      $edit += array(
        'mollom[checks][spam]' => TRUE,
      );
    }
Dries Buytaert's avatar
Dries Buytaert committed
    foreach (array_keys($form_info['elements']) as $field) {
      if (!isset($fields) || in_array($field, $fields)) {
        // If the user specified all fields by default or to include this
        // field, set its checkbox value to TRUE.
        $edit['mollom[enabled_fields][' . rawurlencode($field) . ']'] = TRUE;
Dries Buytaert's avatar
Dries Buytaert committed
      }
      else {
        // Otherwise set the field's checkbox value to FALSE.
        $edit['mollom[enabled_fields][' . rawurlencode($field) . ']'] = FALSE;
Dries Buytaert's avatar
Dries Buytaert committed
      }
    }
    $this->drupalPost(NULL, $edit, t('Save'));
Lisa Backer's avatar
Lisa Backer committed
    if (!$exists) {
Dries Buytaert's avatar
Dries Buytaert committed
      $this->assertText(t('The form protection has been added.'));
    }
    else {
      $this->assertText(t('The form protection has been updated.'));
    }
  }

  /**
   * Remove Mollom protection for a given form.
   *
   * @param $form_id
   *   The form id to configure.
   */
  protected function delProtection($form_id) {
    // Determine whether the form is protected.
    $exists = db_query_range('SELECT 1 FROM {mollom_form} WHERE form_id = :form_id', 0, 1, array(':form_id' => $form_id));
Dries Buytaert's avatar
Dries Buytaert committed
    if ($exists) {
      $this->drupalGet('admin/config/content/mollom/unprotect/' . $form_id);
Dries Buytaert's avatar
Dries Buytaert committed
      $this->assertText(t('Mollom will no longer protect this form from spam.'), t('Unprotect confirmation form found.'));
      $this->drupalPost(NULL, array(), t('Confirm'));
    }
  }

  /**
   * Assert that Mollom session data was stored for a submission.
   *
   * @param $entity
   *   The entity type to search for in {mollom}.
   * @param $id
   *   The entity id to search for in {mollom}.
   * @param $response_type
   *   (optional) The type of ID to assert; e.g., 'contentId', 'captchaId'.
   * @param $response_id
   *   (optional) The ID of $type to assert additionally.
  protected function assertMollomData($entity, $id, $response_type = '', $response_id = NULL) {
    $data = mollom_data_load($entity, $id);
    $this->assertTrue($data->id, t('Mollom session data for %entity @id exists: <pre>@data</pre>', array(
      '%entity' => $entity,
      '@id' => $id,
      '@data' => var_export($data, TRUE),
    )));
    if (isset($response_id)) {
      $this->assertSame(t('Stored @type ID', array('@type' => $response_type)), $data->$response_type, $response_id);
  }

  /**
   * Assert that no Mollom session data exists for a certain entity.
   */
  protected function assertNoMollomData($entity, $id) {
    $data = mollom_data_load($entity, $id);
    $this->assertFalse($data, t('No Mollom session data exists for %entity @id.', array('%entity' => $entity, '@id' => $id)));
  }

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * Assert that the CAPTCHA field is found on the current page.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function assertCaptchaField() {
    $inputs = $this->xpath('//input[@type=:type and @name=:name]', array(
      ':type' => 'text',
      ':name' => 'mollom[captcha]',
    ));
    $labels = $this->xpath('//label[@for=:for]/span[@class=:class]', array(
      ':for' => 'edit-mollom-captcha',
      ':class' => 'form-required',
    ));
    $this->assert(!empty($inputs[0]) && !empty($labels[0]), 'Required CAPTCHA field found.');

    $image = $this->xpath('//img[@alt=:alt]', array(':alt' => t("Type the characters you see in this picture.")));
    $this->assert(!empty($image), 'CAPTCHA image found.');
   * Assert that the CAPTCHA field is not found on the current page.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function assertNoCaptchaField() {
    $this->assertNoText($this->unsure_message);
    $this->assertNoText($this->incorrect_message);
Dries Buytaert's avatar
Dries Buytaert committed
    $this->assertNoFieldByXPath('//input[@type="text"][@name="mollom[captcha]"]', '', 'CAPTCHA field not found.');
    $image = $this->xpath('//img[@alt=:alt]', array(':alt' => t("Type the characters you see in this picture.")));
    $this->assert(empty($image), 'CAPTCHA image not found.');
  /**
   * Assert that the privacy policy link is found on the current page.
   */
  protected function assertPrivacyLink() {
    $elements = $this->xpath('//div[contains(@class, "mollom-privacy")]');
    $this->assertTrue($elements, t('Privacy policy container found.'));
  }

  /**
   * Assert that the privacy policy link is not found on the current page.
   */
  protected function assertNoPrivacyLink() {
    $elements = $this->xpath('//div[contains(@class, "mollom-privacy")]');
    $this->assertFalse($elements, t('Privacy policy container not found.'));
  }

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * Test submitting a form with a correct CAPTCHA value.
   *
   * @param $url
   *   The URL of the form, or NULL to use the current page.
   * @param $edit
   *   An array of form values used in drupalPost().
   * @param $button
   *   The text of the form button to click in drupalPost().
   * @param $success_message
   *   An optional message to test does appear after submission.
   */
  protected function postCorrectCaptcha($url, array $edit = array(), $button, $success_message = '') {
    if (isset($url)) {
      $this->drupalGet($url);
    }
    $this->assertCaptchaField();
Dries Buytaert's avatar
Dries Buytaert committed
    $edit['mollom[captcha]'] = 'correct';
Dries Buytaert's avatar
Dries Buytaert committed
    $this->assertNoCaptchaField();
    $this->assertNoText($this->incorrect_message);
    if ($success_message) {
      $this->assertText($success_message);
    }
  }

  /**
   * Test submitting a form with an incorrect CAPTCHA value.
   *
   * @param $url
   *   The URL of the form, or NULL to use the current page.
   * @param $edit
   *   An array of form values used in drupalPost().
   * @param $button
   *   The text of the form button to click in drupalPost().
   * @param $success_message
   *   An optional message to test does not appear after submission.
   */
  protected function postIncorrectCaptcha($url, array $edit = array(), $button, $success_message = '') {
    if (isset($url)) {
      $this->drupalGet($url);
    }
    $this->assertCaptchaField();
Dries Buytaert's avatar
Dries Buytaert committed
    $edit['mollom[captcha]'] = 'incorrect';
    $before_url = $this->getUrl();
    $this->drupalPost(NULL, $edit, $button);
    $this->assertCaptchaField();
Dries Buytaert's avatar
Dries Buytaert committed
    $this->assertText($this->incorrect_message);
    if ($success_message) {
      $this->assertNoText($success_message);
    }
  }

  /**
   * Test submitting a form with 'spam' values.
   *
   * @param $url
   *   The URL of the form, or NULL to use the current page.
   * @param $spam_fields
   *   An array of form field names to inject spam content into.
   * @param $edit
   *   An array of non-spam form values used in drupalPost().
   * @param $button
   *   The text of the form button to click in drupalPost().
   * @param $success_message
   *   An optional message to test does not appear after submission.
   */
  protected function assertSpamSubmit($url, array $spam_fields, array $edit = array(), $button, $success_message = '') {
    $edit += array_fill_keys($spam_fields, 'spam');
    $this->drupalPost($url, $edit, $button);
Dries Buytaert's avatar
Dries Buytaert committed
    $this->assertText($this->spam_message);
    if ($success_message) {
      $this->assertNoText($success_message);
    }
  }

  /**
   * Test submitting a form with 'ham' values.
   *
   * @param $url
   *   The URL of the form, or NULL to use the current page.
   * @param $ham_fields
   *   An array of form field names to inject ham content into.
   * @param $edit
   *   An array of non-spam form values used in drupalPost().
   * @param $button
   *   The text of the form button to click in drupalPost().
   * @param $success_message
   *   An optional message to test does appear after submission.
   */
  protected function assertHamSubmit($url, array $ham_fields, array $edit = array(), $button, $success_message = '') {
    $edit += array_fill_keys($ham_fields, 'ham');
    $this->drupalPost($url, $edit, $button);
    $this->assertNoCaptchaField($url);
    $this->assertNoText($this->spam_message);
    if ($success_message) {
      $this->assertText($success_message);
    }
  }

  /**
   * Test submitting a form with unsure values and resulting CAPTCHA submissions.
   *
   * @param $url
   *   The URL of the form, or NULL to use the current page.
   * @param $unsure_fields
   *   An array of form field names to inject unsure content into.
   * @param $edit
   *   An array of non-spam form values used in drupalPost().
   * @param $button
   *   The text of the form button to click in drupalPost().
   * @param $success_message
   *   An optional message to test does appear after sucessful form and CAPTCHA
   *   submission.
   */
  protected function assertUnsureSubmit($url, array $unsure_fields, array $edit = array(), $button, $success_message = '') {
    $edit += array_fill_keys($unsure_fields, 'unsure');
    $this->drupalPost($url, $edit, $button);
Dries Buytaert's avatar
Dries Buytaert committed
    $this->assertText($this->unsure_message);
    if ($success_message) {
      $this->assertNoText($success_message);
    }

    $this->postIncorrectCaptcha(NULL, $edit, $button, $success_message);
    $this->postCorrectCaptcha(NULL, $edit, $button, $success_message);
  }

  /**
   * Asserts that the most recently sent mail contains a "Report to Mollom" link.
   *
   * Contrary to DrupalWebTestCase::assertMail(), this function removes the last
   * sent mail from the internally recorded stack.
   *
   * @param string $entity_type
   *   (optional) The expected entity type contained in the report link.
   *   Defaults to 'mollom_content'.
   *
   * @return array|false
   *   FALSE if the link was not found, or an associative array containing:
   *   - url: The full report link URL.
   *   - entity: The entity type contained in the report link URL.
   *   - id: The entity ID contained in the report link URL.
   *   - mail: The full mail message array, as recorded by TestingMailSystem.
   *   - external: TRUE.
   *   The array can be passed directly as $options to drupalGet().
   */
  protected function assertMailMollomReportLink($entity_type = 'mollom_content') {
    // Grab the last sent mail.
    // @see DrupalWebTestCase::assertMail()
    $captured_emails = variable_get('drupal_test_email_collector', array());
    $message = array_pop($captured_emails);
    variable_set('drupal_test_email_collector', $captured_emails);

    $found = FALSE;
    // Determine the report URI pattern for the passed entity type.
    $path = FALSE;
    foreach (mollom_form_list() as $form_id => $info) {
      if (isset($info['entity']) && $info['entity'] == $entity_type && isset($info['report path'])) {
        $path = $info['report path'];
        break;
      }
    }
    if ($path) {
      $path = strtr($path, array('%id' => '([^\s]+)'));
      if (preg_match('@http.+?' . $path . '@', $message['body'], $matches)) {
        $found = array(
          'url' => $matches[0],
          'entity' => $entity_type,
          'id' => $matches[1],
          'mail' => $message,
          'external' => TRUE,
        );
      }
    }
    elseif (preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $message['body'], $matches)) {
      $found = array(
        'url' => $matches[0],
        'entity' => $matches[1],
        'id' => $matches[2],
        'mail' => $message,
      );
    }
    $this->assertTrue($found, t('Report to Mollom link found in e-mail: %url', array('%url' => $found['url'])));
    $this->assertSame('Report link entity type', $found['entity'], $entity_type);
    $this->assertMollomData($found['entity'], $found['id']);
    return $found;
  }

  /**
   * Asserts that the most recently sent mail does NOT contain a "Report to Mollom" link.
   *
   * Contrary to DrupalWebTestCase::assertMail(), this function removes the last
   * sent mail from the internally recorded stack.
   *
   * @return bool
   *   TRUE if no link was found, FALSE otherwise.
   */
  protected function assertNoMailMollomReportLink() {
    // Grab the last sent mail.
    // @see DrupalWebTestCase::assertMail()
    $captured_emails = variable_get('drupal_test_email_collector', array());
    $message = array_pop($captured_emails);
    if (empty($message)) {
      $this->fail('No mail to assert.');
      return;
    }
    variable_set('drupal_test_email_collector', $captured_emails);

    $found = preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $message['body'], $matches);
    $this->assertFalse($found, 'Report to Mollom link not found in e-mail.');
    if ($found) {
      debug($message);
    }
  }

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * Retrieve a field value by ID.
   */
  protected function getFieldValueByID($id) {
    $fields = $this->xpath($this->constructFieldXpath('id', $id));
    return (string) $fields[0]['value'];
  }

  /**
   * Retrieve a field value by name.
   */
  protected function getFieldValueByName($name) {
    $fields = $this->xpath($this->constructFieldXpath('name', $name));
    return (string) $fields[0]['value'];
   * Retrieve sent request parameter values from testing server implementation.
Dries Buytaert's avatar
Dries Buytaert committed
   *
   * @param $resource
   *   (optional) The resource name to retrieve submitted values from. Defaults
   *   to 'content'.
   * @param $retain
   *   (optional) Whether to retain the (last) record being read. Defaults to
   *   FALSE; i.e., the record being read is removed.
   * @see MollomWebTestCase::resetServerRecords()
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function getServerRecord($resource = 'content', $retain = FALSE) {
    $function = 'mollom_test_server_' . $resource;
    // Ensure that we do not read obsolete/outdated data from variable_get()'s
    // static cache while variables might have been updated in the child site.
    $this->refreshVariables();

    // Retrieve last recorded values.
    $storage = variable_get($function, array());
    $return = ($retain ? end($storage) : array_shift($storage));
    variable_set($function, $storage);

Dries Buytaert's avatar
Dries Buytaert committed
    return $return;
  }

   * @param $resource
   *   (optional) The resource name to reset records of. Defaults to 'content'.
  protected function resetServerRecords($resource = 'content') {
    $function = 'mollom_test_server_' . $resource;
  /**
   * Wraps drupalGet() for additional watchdog message assertion.
   *
   * @param $options
   *   In addition to regular $options that are passed to url():
   *   - watchdog: (optional) Boolean whether to assert that only non-severe
   *     watchdog messages have been logged. Defaults to TRUE. Use FALSE to
   *     negate the watchdog message severity assertion.
   *
   * @see DrupalWebTestCase->drupalGet()
   * @see MollomWebTestCase->assertMollomWatchdogMessages()
   * @see MollomWebTestCase->assertResponseID()
   */
  protected function drupalGet($path, array $options = array(), array $headers = array()) {
    $output = parent::drupalGet($path, $options, $headers);
    $options += array('watchdog' => WATCHDOG_NOTICE);
    $this->assertMollomWatchdogMessages($options['watchdog']);
    return $output;
  }

  /**
   * Wraps drupalPost() for additional watchdog message assertion.
   *
   * @param $options
   *   In addition to regular $options that are passed to url():
   *   - watchdog: (optional) Boolean whether to assert that only non-severe
   *     watchdog messages have been logged. Defaults to TRUE. Use FALSE to
   *     negate the watchdog message severity assertion.
   *
   * @see MollomWebTestCase->assertMollomWatchdogMessages()
   * @see MollomWebTestCase->assertResponseID()
   * @see DrupalWebTestCase->drupalPost()
   */
  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
    $output = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post);
    $options += array('watchdog' => WATCHDOG_NOTICE);
    $this->assertMollomWatchdogMessages($options['watchdog']);
    return $output;
  }

Dries Buytaert's avatar
Dries Buytaert committed
  /**
   * Asserts that two values belonging to the same variable are equal.
Dries Buytaert's avatar
Dries Buytaert committed
   *
   * Checks to see whether two values, which belong to the same variable name or
   * identifier, are equal and logs a readable assertion message.
Dries Buytaert's avatar
Dries Buytaert committed
   *
   * @param $name
   *   A name or identifier to use in the assertion message.
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
Dries Buytaert's avatar
Dries Buytaert committed
   * @return
   *   TRUE if the assertion succeeded, FALSE otherwise.
   *
   * @see MollomWebTestCase::assertNotSame()
   *
   * @todo D8: Move into core. This improved assertEqual() did not get into D7,
   *   since the function signature differs and it's plenty of work to manually
   *   update all assertEqual() invocations throughout all tests.
Dries Buytaert's avatar
Dries Buytaert committed
   */
  protected function assertSame($name, $first, $second) {
    $message = t("@name: @first is equal to @second.", array(
Dries Buytaert's avatar
Dries Buytaert committed
      '@name' => $name,
      '@first' => var_export($first, TRUE),
      '@second' => var_export($second, TRUE),
Dries Buytaert's avatar
Dries Buytaert committed
    ));
    $this->assertEqual($first, $second, $message);
  }
   * Asserts that two values belonging to the same variable are not equal.
   * Checks to see whether two values, which belong to the same variable name or
   * identifier, are not equal and logs a readable assertion message.
   *
   * @param $name
   *   A name or identifier to use in the assertion message.
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @return
   *   TRUE if the assertion succeeded, FALSE otherwise.
  protected function assertNotSame($name, $first, $second) {
    $message = t("@name: '@first' is not equal to '@second'.", array(
      '@first' => var_export($first, TRUE),
      '@second' => var_export($second, TRUE),

  /**
   * Enables aggressive page caching options to resemble reverse-proxies.
   */
  protected function enablePageCache() {
    variable_set('cache', 1);
    variable_set('page_cache_maximum_age', 180);
    // A minimum cache lifetime causes cache_clear_all() to start a session.
    //variable_set('cache_lifetime', 60);
  }

  /**
   * Asserts a successful mollom_test_form submission.
   *
   * @param $old_mid
   *   (optional) The existing test record id to assert.
   */
  protected function assertTestSubmitData($old_mid = NULL) {
    $this->assertText('Successful form submission.');
    $mid = $this->getFieldValueByName('mid');
    if (isset($old_mid)) {
      $this->assertSame('Test record id', $mid, $old_mid);
    }
    else {