<?php
// $Id: mollom.test,v 1.1.2.73 2010/10/29 14:50:26 dries Exp $

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

/**
 * Defines Mollom testing keys.
 *
 * These keys will only work in test mode, so consult the Mollom API
 * documentation for details. Do NOT use PRODUCTION keys for testing!
 */
define('MOLLOM_TEST_PUBLIC_KEY', '9cc3d2e43971de758ecddad61a3d12ec');
define('MOLLOM_TEST_PRIVATE_KEY', '603a8d11099f17faaab49139bfc7d00a');

/**
 * 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 {
  /**
   * The Mollom administrator user account.
   */
  protected $admin_user;

  /**
   * A normal web user account.
   */
  protected $web_user;

  /**
   * 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.';

  /**
   * 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.";

  /**
   * The public key used during testing.
   */
  protected $public_key = '';

  /**
   * The private key used during testing.
   */
  protected $private_key = '';

  /**
   * A boolean that is TRUE if the above keys are for a reseller account.
   */
  protected $is_reseller = FALSE;

  /**
   * Set up an administrative user account and testing keys.
   */
  function setUp() {
    // Re-initialize stored session_id and watchdog messages.
    $this->resetSessionID();
    $this->messages = array();

    // @see DrupalWebTestCase::setUp()
    $modules = func_get_args();
    if (isset($modules[0]) && is_array($modules[0])) {
      $modules = $modules[0];
    }

    // If not explicitly disabled by a test, setup with Mollom and default admin
    // user.
    if (empty($this->disableDefaultSetup)) {
      // Call parent::setUp() allowing Mollom test cases to pass further modules.
      $modules[] = 'mollom';
      $modules[] = 'dblog';
      call_user_func_array(array($this, 'parent::setUp'), $modules);

      $this->admin_user = $this->drupalCreateUser(array(
        'administer mollom',
        'access administration pages',
        'administer content types',
        'administer comments',
        'administer permissions',
        'administer users',
      ));
    }
    else {
      $modules[] = 'dblog';
      call_user_func_array(array($this, 'parent::setUp'), $modules);
    }

    // If not explicitly disabled by a test, setup and validate testing keys.
    if (empty($this->disableDefaultSetup)) {
      $this->setKeys();
      $this->assertValidKeys();
    }
  }

  function tearDown() {
    // 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
   * considered as "severe".
   *
   * @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.
   *
   * @todo Add this to Drupal core.
   */
  protected function assertMollomWatchdogMessages($max_severity = WATCHDOG_NOTICE) {
    module_load_include('inc', 'dblog', 'dblog.admin');

    $this->messages = array();
    $result = db_query("SELECT * FROM {watchdog} WHERE type = 'mollom' ORDER BY timestamp ASC");

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

    $fail_expected = ($max_severity < WATCHDOG_NOTICE);
    $had_severe_message = FALSE;
    while ($row = db_fetch_object($result)) {
      $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.
      if ($row->severity >= $max_severity) {
        $this->pass(_dblog_format_message($row), t('Watchdog'));
      }
      else {
        $this->fail(_dblog_format_message($row), t('Watchdog'));
      }
      // 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 not found.'), t('Watchdog'));
    }
    // Delete processed watchdog messages.
    if (!empty($this->messages)) {
      $seen_ids = array_keys($this->messages);
      db_query("DELETE FROM {watchdog} WHERE wid IN (" . db_placeholders($seen_ids) . ")", $seen_ids);
    }
  }

  /**
   * 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 $session_id
   *   A Mollom session_id of the last request, as contained in the XML-RPC
   *   response.
   */
  protected function assertSessionID($session_id) {
    // Check whether watchdog messages indicate a refresh or redirect.
    foreach ($this->messages as $message) {
      if ($message->message == 'Refreshed servers: %servers' || $message->message == 'Server %server redirected to: %next.') {
        $this->resetSessionID();
      }
    }

    if (!isset($this->session_id)) {
      // Use assertTrue() instead of pass(), to test !empty().
      $this->assertTrue($session_id, t('New session_id: %session_id', array('%session_id' => $session_id)));
      $this->session_id = $session_id;
    }
    else {
      $this->assertSame('session_id', $session_id, $this->session_id);
    }
    return $this->session_id;
  }

  /**
   * Reset the statically cached Mollom session id.
   */
  protected function resetSessionID() {
    $this->session_id = NULL;
  }

  /**
   * Assert a Mollom session id in a form.
   *
   * This is a wrapper around assertSessionID() 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, assertSessionIDInForm(), 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.
   */
  protected function assertSessionIDInForm() {
    // The session id found in the form element value is prefixed with the UNIX
    // timestamp denoting the time it was generated/output. The form element
    // #process callback mollom_process_mollom_session_id() uses this timestamp
    // to additionally validate its age.
    list($timestamp, $session_id) = explode('-', $this->getFieldValueByName('mollom[session_id]'));
    return $this->assertSessionID($session_id);
  }

  /**
   * Assign the Mollom API keys to internal variables and reset the server list.
   *
   * @param $public
   *   The public Mollom API key.
   * @param $private
   *   The private Mollom API key.
   * @param $reseller
   *   A boolean that is TRUE if the keys are for a reseller account, or FALSE
   *   otherwise.
   */
  protected function setKeys($public = MOLLOM_TEST_PUBLIC_KEY, $private = MOLLOM_TEST_PRIVATE_KEY, $reseller = MOLLOM_TEST_RESELLER_KEY) {
    // Save internal properties.
    $this->public_key = $public;
    $this->private_key = $private;
    $this->is_reseller = $reseller;

    // Set the module key settings.
    variable_set('mollom_public_key', $public);
    variable_set('mollom_private_key', $private);

    // Enable testing mode.
    variable_set('mollom_testing_mode', 1);

    // Delete any previously set Mollom servers to make sure we are using
    // the default ones.
    variable_del('mollom_servers');
  }

  /**
   * Call the mollom.verifyKey function directly and check that the current
   * keys are valid.
   */
  protected function assertValidKeys() {
    $status = _mollom_status(TRUE);
    $this->assertMollomWatchdogMessages();
    $this->assertIdentical($status, TRUE, t('Mollom servers can be contacted and testing API keys are valid.'));
  }

  /**
   * Configure Mollom protection for a given form.
   *
   * @param $form_id
   *   The form id to configure.
   * @param $mode
   *   The Mollom protection mode for the form.
   * @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.
   */
  protected function setProtection($form_id, $mode = MOLLOM_MODE_ANALYSIS, $fields = NULL, $edit = array()) {
    // Always start from overview page, also to make debugging easier.
    $this->drupalGet('admin/settings/mollom');
    // Determine whether the form is already protected.
    $exists = db_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $form_id, 0, 1));
    // Add a new form.
    if (!$exists) {
      $this->clickLink(t('Add form'));
      $add_form_edit = array(
        'mollom[form_id]' => $form_id,
      );
      $this->drupalPost(NULL, $add_form_edit, t('Next'));
    }
    // Edit an existing form.
    else {
      $this->drupalGet('admin/settings/mollom/manage/' . $form_id);
    }

    $edit += array(
      'mollom[mode]' => $mode,
    );

    // Process the enabled fields.
    $form_list = mollom_form_list();
    $form_info = mollom_form_info($form_id, $form_list[$form_id]['module']);
    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;
      }
      else {
        // Otherwise set the field's checkbox value to FALSE.
        $edit['mollom[enabled_fields][' . rawurlencode($field) . ']'] = FALSE;
      }
    }
    $this->drupalPost(NULL, $edit, t('Save'));
    if (!$exists) {
      $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_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $form_id, 0, 1));
    if ($exists) {
      $this->drupalGet('admin/settings/mollom/unprotect/' . $form_id);
      $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 $session_id
   *   (optional) The Mollom session id to assert additionally.
   */
  protected function assertMollomData($entity, $id, $session_id = NULL) {
    $data = mollom_data_load($entity, $id);
    $this->assertTrue($data->session_id, t('Mollom session data for %entity @id exists: <pre>@data</pre>', array('%entity' => $entity, '@id' => $id, '@data' => var_export($data, TRUE))));
    if (isset($session_id)) {
      $this->assertSame(t('Stored session id'), $data->session_id, $session_id);
    }
    return $data;
  }

  /**
   * 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)));
  }

  /**
   * Assert that the CAPTCHA field is found on the current page.
   */
  protected function assertCaptchaField() {
    $this->assertFieldByXPath('//input[@type="text"][@name="mollom[captcha]"]', '', 'CAPTCHA field found.');
    $image = $this->xpath('//img[@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.
   */
  protected function assertNoCaptchaField() {
    $this->assertNoFieldByXPath('//input[@type="text"][@name="mollom[captcha]"]', '', 'CAPTCHA field not found.');
    $image = $this->xpath('//img[@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.'));
  }

  /**
   * 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 = '') {
    $edit['mollom[captcha]'] = 'correct';
    $this->drupalPost($url, $edit, $button);
    $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 = '') {
    $edit['mollom[captcha]'] = 'incorrect';
    $before_url = $this->getUrl();
    $this->drupalPost($url, $edit, $button);
    if ($this->getUrl() == $before_url) {
      $this->assertCaptchaField();
    }
    $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);
    $this->assertNoCaptchaField($url);
    $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');
    $before_url = $this->getUrl();
    $this->drupalPost($url, $edit, $button);
    if ($this->getUrl() == $before_url) {
      $this->assertCaptchaField();
    }
    $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);
  }

  /**
   * 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 submitted XML-RPC values from testing server implementation.
   *
   * @param $method
   *   (optional) The XML-RPC method name to retrieve submitted values from.
   *   Defaults to 'mollom.checkContent'.
   *
   * @see MollomWebTestCase::resetServerRecords()
   * @see mollom_test_xmlrpc()
   */
  protected function getServerRecord($method = 'mollom.checkContent') {
    // Map the XML-RPC method name to the corresponding function callback name.
    drupal_load('module', 'mollom_test');
    $method_function_map = mollom_test_xmlrpc();
    $function = $method_function_map[$method];

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

    return $return;
  }

  /**
   * Resets recorded XML-RPC values.
   *
   * @param $method
   *   (optional) The XML-RPC method name to reset records of. Defaults to
   *   'mollom.checkContent'.
   *
   * @see MollomWebTestCase::getServerRecord()
   * @see mollom_test_xmlrpc()
   */
  protected function resetServerRecords($method = 'mollom.checkContent') {
    // Map the XML-RPC method name to the corresponding function callback name.
    drupal_load('module', 'mollom_test');
    $method_function_map = mollom_test_xmlrpc();
    $function = $method_function_map[$method];

    // Delete the variable.
    variable_del($function);
  }

  /**
   * 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->assertSessionID()
   */
  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->assertSessionID()
   * @see DrupalWebTestCase->drupalPost()
   */
  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array()) {
    $output = parent::drupalPost($path, $edit, $submit, $options, $headers);
    $options += array('watchdog' => WATCHDOG_NOTICE);
    $this->assertMollomWatchdogMessages($options['watchdog']);
    return $output;
  }

  /**
   * Asserts that two values belonging to the same variable are equal.
   *
   * Checks to see whether two values, which belong to the same variable name or
   * identifier, are 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.
   *
   * @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.
   */
  protected function assertSame($name, $first, $second) {
    $message = t("@name: @first is equal to @second.", array(
      '@name' => $name,
      '@first' => var_export($first, TRUE),
      '@second' => var_export($second, TRUE),
    ));
    $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.
   *
   * @see MollomWebTestCase::assertSame()
   */
  protected function assertNotSame($name, $first, $second) {
    $message = t("@name: '@first' is not equal to '@second'.", array(
      '@name' => $name,
      '@first' => var_export($first, TRUE),
      '@second' => var_export($second, TRUE),
    ));
    $this->assertNotEqual($first, $second, $message);
  }
}

/**
 * Tests module installation and global status handling.
 */
class MollomInstallationTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Installation and key handling',
      'description' => 'Tests module installation and key error handling.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    // Re-initialize stored session_id and watchdog messages.
    $this->resetSessionID();
    $this->messages = array();

    $this->disableDefaultSetup = TRUE;
    parent::setUp('comment');

    $this->admin_user = $this->drupalCreateUser(array(
      'access administration pages',
      'administer site configuration',
      'administer permissions',
    ));
    $this->web_user = $this->drupalCreateUser();
  }

  /**
   * Tests status handling after installation.
   *
   * We walk through a regular installation of the Mollom module instead of using
   * setUp() to ensure that everything works as expected.
   *
   * Note: Partial error messages tested here; hence, no t().
   */
  function testInstallationProcess() {
    $admin_message = t('Visit the <a href="@settings-url">Mollom settings page</a> to configure your keys.', array(
      '@settings-url' => url('admin/settings/mollom/settings'),
    ));
    $this->drupalLogin($this->admin_user);

    // Ensure there is no requirements error by default.
    $this->drupalGet('admin/reports/status');
    $this->clickLink('run cron manually');

    // Install the Mollom module.
    $this->drupalPost('admin/build/modules', array('status[mollom]' => TRUE), t('Save configuration'));
    $this->assertRaw(t('The Mollom API keys are not configured yet. !admin-message', array(
      '!admin-message' => $admin_message,
    )), t('Post installation warning found.'));

    // Verify that forms can be submitted without valid Mollom module configuration.
    $node = $this->drupalCreateNode(array('type' => 'story', 'promoted' => TRUE));
    $this->drupalLogin($this->web_user);
    $this->drupalGet('comment/reply/' . $node->nid);
    $edit = array(
      'comment' => 'spam',
    );
    $this->drupalPost(NULL, $edit, t('Preview'));
    $this->drupalPost(NULL, array(), t('Save'));
    $this->assertRaw('<p>' . $edit['comment'] . '</p>', t('Comment found.'));

    // Assign the 'administer mollom' permission and log in a user.
    $this->drupalLogin($this->admin_user);
    $edit = array(
      DRUPAL_AUTHENTICATED_RID . '[administer mollom]' => TRUE,
    );
    $this->drupalPost('admin/user/permissions', $edit, t('Save permissions'));

    // Verify presence of 'empty keys' error message.
    $this->drupalGet('admin/settings/mollom');
    $this->assertText('The Mollom API keys are not configured yet.');
    $this->assertNoText('The configured Mollom API keys are invalid.');

    // Verify requirements error about missing API keys.
    $this->drupalGet('admin/reports/status');
    $this->assertRaw(t('The Mollom API keys are not configured yet. !admin-message', array(
      '!admin-message' => $admin_message,
    )), t('Requirements error found.'));

    // Configure invalid keys.
    $edit = array(
      'mollom_public_key' => 'foo',
      'mollom_private_key' => 'bar',
    );
    $this->drupalGet('admin/settings/mollom/settings');
    $this->drupalPost(NULL, $edit, t('Save configuration'), array('watchdog' => WATCHDOG_EMERG));
    $this->assertText(t('The configuration options have been saved.'));
    $this->assertNoText($this->fallback_message, t('Fallback message not found.'));

    // Verify presence of 'incorrect keys' error message.
    $this->assertText('The configured Mollom API keys are invalid.');
    $this->assertNoText('The Mollom API keys are not configured yet.');
    $this->assertNoText(t('The Mollom servers could not be contacted. Please make sure that your web server can make outgoing HTTP requests.'));

    // Verify requirements error about invalid API keys.
    $this->drupalGet('admin/reports/status', array('watchdog' => WATCHDOG_EMERG));
    $this->assertText('The configured Mollom API keys are invalid.');

    // Ensure unreachable servers.
    variable_set('mollom_servers', array('http://fake-host'));

    // Verify presence of 'network error' message.
    $this->drupalGet('admin/settings/mollom/settings', array('watchdog' => WATCHDOG_EMERG));
    $this->assertText(t('The Mollom servers could not be contacted. Please make sure that your web server can make outgoing HTTP requests.'));

    // Ensure unreachable servers.
    variable_set('mollom_servers', array('http://fake-host'));

    // Verify requirements error about network error.
    $this->drupalGet('admin/reports/status', array('watchdog' => WATCHDOG_EMERG));
    $this->assertText(t('The Mollom servers could not be contacted. Please make sure that your web server can make outgoing HTTP requests.'));
    $this->assertNoText($this->fallback_message, t('Fallback message not found.'));

    // Verify that valid keys work.
    $this->drupalGet('admin/settings/mollom/settings', array('watchdog' => WATCHDOG_EMERG));
    $edit = array(
      'mollom_public_key' => MOLLOM_TEST_PUBLIC_KEY,
      'mollom_private_key' => MOLLOM_TEST_PRIVATE_KEY,
      'mollom_testing_mode' => 1,
    );
    $this->drupalPost(NULL, $edit, t('Save configuration'));
    $this->assertText(t('The configuration options have been saved.'));
    $this->assertText('The services are operating correctly.');
    $this->assertNoText('The Mollom API keys are not configured yet.');
    $this->assertNoText('The configured Mollom API keys are invalid.');

    // Verify presence of testing mode warning.
    $this->drupalGet('admin/settings/mollom');
    $this->assertText('Mollom testing mode is still enabled.');
  }
}

/**
 * Tests low-level XML-RPC communication with Mollom servers.
 */
class MollomResponseTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Server responses',
      'description' => 'Tests that Mollom server responses match expectations.',
      'group' => 'Mollom',
    );
  }

  /**
   * Tests mollom.checkContent().
   */
  function testCheckContent() {
    $data = array(
      'author_name' => $this->admin_user->name,
      'author_mail' => $this->admin_user->mail,
      'author_id' => $this->admin_user->uid,
      'author_ip' => ip_address(),
    );

    // Ensure proper response for 'ham' submissions.
    // By default (i.e., omitting 'checks') we expect spam and quality checking
    // only.
    $data['post_body'] = 'ham';
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM);
    $this->assertSame('quality', $result['quality'], 1);
    $this->assertTrue(!isset($result['profanity']), 'profanity not returned.');
    $session_id = $this->assertSessionID($result['session_id']);

    // Ensure proper response for 'spam' submissions, re-using session_id.
    $data['post_body'] = 'spam';
    $data['session_id'] = $session_id;
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_SPAM);
    $this->assertSame('quality', $result['quality'], 0);
    $this->assertTrue(!isset($result['profanity']), 'profanity not returned.');
    $session_id = $this->assertSessionID($result['session_id']);

    // Ensure proper response for 'unsure' submissions, re-using session_id.
    $data['post_body'] = 'unsure';
    $data['session_id'] = $session_id;
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
    $this->assertSame('quality', $result['quality'], 0.5);
    $this->assertTrue(!isset($result['profanity']), 'profanity not returned.');
    $session_id = $this->assertSessionID($result['session_id']);

    // Additionally enable profanity checking.
    $data['post_body'] = 'spam profanity';
    $data['checks'] = 'spam,quality,profanity';
    $data['session_id'] = $session_id;
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_SPAM);
    $this->assertSame('quality', $result['quality'], 0);
    $this->assertSame('profanity', $result['profanity'], 1);
    $session_id = $this->assertSessionID($result['session_id']);

    // Change the string to contain profanity only.
    $data['post_body'] = 'profanity';
    $data['checks'] = 'spam,quality,profanity';
    $data['session_id'] = $session_id;
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
    $this->assertSame('quality', $result['quality'], 0);
    $this->assertSame('profanity', $result['profanity'], 1);
    $session_id = $this->assertSessionID($result['session_id']);

    // Disable spam checking, only do profanity checking.
    $data['post_body'] = 'spam profanity';
    $data['checks'] = 'profanity';
    $data['session_id'] = $session_id;
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertTrue(!isset($result['spam']), 'spam not returned.');
    $this->assertTrue(!isset($result['quality']), 'quality not returned.');
    $this->assertSame('profanity', $result['profanity'], 1);
    $session_id = $this->assertSessionID($result['session_id']);

    // Pass arbitrary string to profanity checking.
    $data['post_body'] = $this->randomString(12);
    $data['session_id'] = $session_id;
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertTrue(!isset($result['spam']), 'spam not returned.');
    $this->assertTrue(!isset($result['quality']), 'quality not returned.');
    $this->assertSame('profanity', $result['profanity'], 0);
    $session_id = $this->assertSessionID($result['session_id']);
  }

  /**
   * Tests results of mollom.checkContent() across requests for a single session.
   */
  function testCheckContentSession() {
    $data = array(
      'author_name' => $this->admin_user->name,
      'author_mail' => $this->admin_user->mail,
      'author_id' => $this->admin_user->uid,
      'author_ip' => ip_address(),
    );

    // Sequence: Post unsure spam, correct CAPTCHA, change post into spam,
    // expect it to be ham (due to correct CAPTCHA).
    $data['post_body'] = 'unsure';
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
    $data['session_id'] = $this->assertSessionID($result['session_id']);

    $captcha_data = array(
      'session_id' => $data['session_id'],
      'author_ip' => $data['author_ip'],
    );
    $result = mollom('mollom.getImageCaptcha', $captcha_data);
    $this->assertMollomWatchdogMessages();
    $data['session_id'] = $this->assertSessionID($result['session_id']);

    $captcha_data = array(
      'session_id' => $data['session_id'],
      'author_ip' => $data['author_ip'],
      'author_id' => $data['author_id'],
      'captcha_result' => 'correct',
    );
    $result = mollom('mollom.checkCaptcha', $captcha_data);
    $this->assertMollomWatchdogMessages();
    $this->assertIdentical($result, TRUE, t('CAPTCHA response was correct.'));

    $data['post_body'] = 'spam';
    $result = mollom('mollom.checkContent', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM);
    $data['session_id'] = $this->assertSessionID($result['session_id']);
  }

  /**
   * Tests mollom.getImageCaptcha().
   */
  function testGetImageCaptcha() {
    // Ensure we get no SSL URL by default.
    $data = array(
      'author_ip' => ip_address(),
    );
    $result = mollom('mollom.getImageCaptcha', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertTrue(strpos($result['url'], 'http://') === 0, t('CAPTCHA URL uses HTTP protocol.'));

    // Ensure we get a SSL URL when passing the 'ssl' parameter.
    $data = array(
      'author_ip' => ip_address(),
      'ssl' => TRUE,
    );
    $result = mollom('mollom.getImageCaptcha', $data);
    $this->assertMollomWatchdogMessages();
    $this->assertTrue(strpos($result['url'], 'https://') === 0, t('CAPTCHA URL uses HTTPS protocol.'));
  }

  /**
   * Tests mollom.checkCaptcha().
   */
  function testCheckCaptcha() {
    // Ensure we can send an 'author_id'.
    // Verifying no severe watchdog messages is sufficient, as unsupported
    // parameters would trigger a XML-RPC error.
    $uid = rand();
    $data = array(
      'author_ip' => ip_address(),
      'author_id' => $uid,
    );
    $result = mollom('mollom.getImageCaptcha', $data);
    $this->assertMollomWatchdogMessages();

    $data += array(
      'session_id' => $result['session_id'],
      'captcha_result' => 'correct',
    );
    $result = mollom('mollom.checkCaptcha', $data);
    $this->assertMollomWatchdogMessages();
  }
}

class MollomAccessTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Access checking',
      'description' => 'Confirm that there is a working key pair and that this status is correctly indicated on the module settings page for appropriate users.',
      'group' => 'Mollom',
    );
  }

  /**
   * Configure an invalid key pair and ensure error message.
   */
  function testKeyPairs() {
    // No error message or watchdog messages should be thrown with default
    // testing keys.
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('admin/settings/mollom/settings');

    // Set up invalid test keys and check that an error message is shown.
    $edit = array(
      'mollom_public_key' => 'invalid-public-key',
      'mollom_private_key' => 'invalid-private-key',
    );
    $this->drupalPost(NULL, $edit, t('Save configuration'), array('watchdog' => WATCHDOG_EMERG));
    $this->assertText(t('The configuration options have been saved.'));
    $this->assertText('The configured Mollom API keys are invalid.');
  }

  /**
   * Make sure that the Mollom settings page works for users with the
   * 'administer mollom' permission but not those without
   * it.
   */
  function testAdminAccessRights() {
    // Check access for a user that only has access to the 'administer
    // site configuration' permission. This user should have access to
    // the Mollom settings page.
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('admin/settings/mollom');
    $this->assertResponse(200);

    // Check access for a user that has everything except the 'administer
    // mollom' permission. This user should not have access to the Mollom
    // settings page.
    $this->web_user = $this->drupalCreateUser(array_diff(module_invoke_all('perm'), array('administer mollom')));
    $this->drupalLogin($this->web_user);
    $this->drupalGet('admin/settings/mollom');
    $this->assertResponse(403);
  }

  /**
   * Tests 'bypass access' property of registered forms.
   */
  function testBypassAccess() {
    $this->drupalLogin($this->admin_user);
    $this->setProtection('comment_form');
    $this->drupalLogout();

    $node = $this->drupalCreateNode(array('body' => 'node body', 'type' => 'story'));

    // Create a regular user and post a comment.
    $this->web_user = $this->drupalCreateUser();
    $this->drupalLogin($this->web_user);
    $edit = array(
      'comment' => 'ham',
    );
    $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));

    $this->drupalPost(NULL, array(), t('Save'));
    $this->assertText('node body');
    $this->assertText($edit['comment']);

    // Ensure a user having one of the permissions to bypass access can post
    // spam without triggering the spam protection.
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('node/' . $node->nid);
    $this->clickLink('edit');

    $edit = array(
      'subject' => '',
      'comment' => 'spam',
    );
    $this->drupalPost(NULL, $edit, t('Preview'));
    $this->assertNoText($this->spam_message);

    $this->drupalPost(NULL, array(), t('Save'));
    $this->assertNoText($this->spam_message);
    $this->assertText('node body');

    // Log in back the regular user and try to edit the comment containing spam.
    $this->drupalLogin($this->web_user);
    $this->drupalGet('node/' . $node->nid);
    $this->clickLink('edit');

    $this->drupalPost(NULL, array(), t('Preview'));
    $this->assertText($this->spam_message);

    $this->drupalPost(NULL, array(), t('Save'));
    $this->assertText($this->spam_message);
    $this->assertNoText('node body');
  }
}

class MollomFallbackTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Fallback behavior',
      'description' => 'Check that the module uses the correct fallback mechanism when one or more of the specified Mollom servers are not available.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    // Enable testing server implementation.
    parent::setUp('mollom_test');
  }

  /**
   * Make sure that "request new password" submissions can be blocked when
   * the Mollom servers are unreachable.
   */
  function testFallbackMechanismBlock() {
    // Enable Mollom for the request password form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
    // Set the fallback strategy to 'blocking mode'.
    $this->drupalPost('admin/settings/mollom/settings', array('mollom_fallback' => MOLLOM_FALLBACK_BLOCK), t('Save configuration'));
    $this->assertText('The configuration options have been saved.');
    $this->drupalLogout();

    // Configure Mollom to use a non-existent server as that should trigger
    // the fallback mechanism.
    variable_set('mollom_servers', array('http://fake-host'));

    // Check the password request form.
    // @todo Test mail sending with assertMail() now that it is available.
    $this->drupalGet('user/password', array('watchdog' => WATCHDOG_EMERG));
    $this->assertNoCaptchaField();
    $this->assertText($this->fallback_message);
  }

  /**
   * Make sure that "request new password" submissions can be allowed when
   * the Mollom servers are unreachable.
   */
  function testFallbackMechanismAccept() {
    // Enable Mollom for the request password form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
    // Set the fallback strategy to 'accept mode'.
    $this->drupalPost('admin/settings/mollom/settings', array('mollom_fallback' => MOLLOM_FALLBACK_ACCEPT), t('Save configuration'));
    $this->assertText('The configuration options have been saved.');
    $this->drupalLogout();

    // Configure Mollom to use a non-existent server as that should trigger
    // the fallback mechanism.
    variable_set('mollom_servers', array('http://fake-host'));

    // Check the password request form.
    $this->drupalGet('user/password', array('watchdog' => WATCHDOG_EMERG));
    $this->assertNoCaptchaField();
    $this->assertNoText($this->fallback_message);
  }

  /**
   * Make sure that spam protection is still active even when some of the
   * Mollom servers are unavailable.
   *
   * @todo Test mail sending with assertMail() now that it is available.
   */
  function testFailoverMechanism() {
    $this->drupalLogin($this->admin_user);
    $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
    $this->drupalLogout();

    // Set the fallback strategy to 'blocking mode', so that if the failover
    // mechanism does not work, we would expect to get a warning.
    variable_set('mollom_fallback', MOLLOM_FALLBACK_BLOCK);

    // Configure Mollom to use a list of servers that have a number of
    // unknown servers, but one real server.
    variable_set('mollom_servers', array(
      'http://fake-host-1',
      'http://fake-host-2',
      $GLOBALS['base_url'] . '/xmlrpc.php?version=',
      'http://xmlrpc1.mollom.com', // The real server.
      'http://fake-host-3',
    ));

    // Validate that the request password form has a CAPTCHA text field and
    // that a user is not blocked from submitting it.
    $this->drupalGet('user/password');
    $this->assertCaptchaField();
    $this->assertNoText($this->fallback_message);

    $this->postCorrectCaptcha('user/password', array('name' => $this->admin_user->name), t('E-mail new password'));
    $this->assertText(t('Further instructions have been sent to your e-mail address.'));
  }
}

class MollomServerListRecoveryTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Server list recovery',
      'description' => 'Check that the module can recover from an invalid server list.',
      'group' => 'Mollom',
    );
  }

  /**
   * Make sure the server list is reset when the Mollom servers are unavailable or incorrect.
   */
  function testServerListRecovery() {
    $list = array(
      array(
        'http://not-a-valid-server-1',
        'http://not-a-valid-server-2',
      ),
      // The lack of the http://-schema results in different error codes
      array(
        'not-a-valid-server-url-1',
        'not-a-valid-server-url-2',
      ),
    );

    foreach ($list as $servers) {
      // Call mollom.verifyKey with an invalid server list.  The expected behavior
      // is that the first call fails, but that the second call succeeds because
      // the server list is automatically reset or recovered by the Mollom module.
      variable_set('mollom_servers', $servers);

      $key_is_valid = mollom('mollom.verifyKey');
      $this->assertIdentical($key_is_valid, NETWORK_ERROR, t('The Mollom servers could not be contacted.'));
      $this->assertMollomWatchdogMessages(WATCHDOG_EMERG);

      $key_is_valid = mollom('mollom.verifyKey');
      $this->assertIdentical($key_is_valid, TRUE, t('The Mollom servers could be contacted.'));
      $this->assertMollomWatchdogMessages();
    }
  }
}

class MollomLanguageDetectionTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Language detection',
      'description' => 'Tests language detection functionality.',
      'group' => 'Mollom',
    );
  }

  /**
   * Test the language detection functionality at the API level without using a web interface.
   */
  function testLanguageDetectionAPI() {
    // Note that Mollom supports more languages than those tested.
    $strings = array(
      'en' => "Hi, this is a test of the language detection code to see if it works well.",
      'nl' => "Hallo, dit is een test van de taaldetectiecode om te controleren of het werkt.",
      'fr' => "Bonjour, ceci est un test du detecteur langue automatique pour voir ci ça marche bien.",
      'de' => "Bedecke deinen Himmel, Zeus, Mit Wolkendunst Und übe, dem Knaben gleich, der Disteln köpft, An Eichen dich und Bergeshöhn.",
      'ko' => "'엄마야 누나야 강변살자. 뜰에는 반짝이는 금모래 빛. 뒷문 밖에는 갈잎의 노래",
      'ru' => "Холуй трясется. Раб хохочет. Палач свою секиру точит. Тиран кромсает каплуна. Сверкает зимняя луна.",
      'hu' => "Földszintiek mászófámról pillantva fejjel lefelé ti lógtok bele nézőim az űrbe ki tudja így kölcsönös kíváncsiak a helyes felelet kié",
      'el' => "Σαν να 'χουνε την όψη της αιώνες οργωμένη. Κάτι άναρχο κι ατέλειωτο στο πρόσωπό της μένει.",
      'ja' => "吹くからに秋の草木のしをるれば",
      'th' => "ทั่วประเทศ ประมาณ ๔๐,๐๐๐ แห่ง ชาวไทยนับตั้งแต่ครั้งอดีตมีวิถี ชีวิตผูกพันกับพุทธศาสนาอย่างใกล้ชิด แสดงออกมาเป็น ขนบธรรมเนียมประเพณี",
      'zh' => "螽斯羽，诜诜兮。宜尔子孙，振振兮",
    );

    foreach ($strings as $language => $text) {
      $result = mollom('mollom.detectLanguage', array('text' => $text));
      $this->assertEqual($result[0]['language'], $language, t('A language code was specified and they match.'));
      $this->assertTrue($result[0]['confidence'] > 0, t('A confidence value was specified and it is greater than 0.'));
    }
  }
}

/**
 * Tests blacklist functionality.
 *
 * The blacklists are stored on the server. These tests can fail when
 * different people run the tests at the same time because all tests share
 * the same blacklist. You can configure a custom key to avoid this.
 */
class MollomBlacklistTestCase extends MollomWebTestCase {
  protected $profile = 'testing';

  public static function getInfo() {
    return array(
      'name' => 'Blacklist',
      'description' => 'Tests URL and text blacklist functionality.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    $this->disableDefaultSetup = TRUE;
    parent::setUp('mollom');
    $this->setKeys();
  }

  /**
   * Test the URL blacklist functionality at the API level without using a web interface.
   */
  function testUrlBlacklistAPI() {
    // Remove any stale blacklist entries from test runs that did not finish.
    $blacklist = mollom('mollom.listBlacklistURL');
    foreach ($blacklist as $entry) {
      if (time() - strtotime($entry['created']) > 86400) {
        mollom('mollom.removeBlacklistURL', array('url' => $entry['url']));
      }
    }

    // Blacklist a URL.
    $domain = $this->randomName() . '.com';
    $result = mollom('mollom.addBlacklistURL', array('url' => 'http://' . $domain));
    $this->assertTrue($result, t('The URL was blacklisted.'));

    // Check whether posts containing the blacklisted URL are properly blocked.
    $result = mollom('mollom.checkContent', array(
      'post_body' => "When the exact URL is present, the post should get blocked: http://{$domain}",
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact URL match was blocked.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When the URL is expanded in the back, the post should get blocked: http://{$domain}/oh-my",
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial URL match was blocked.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When the URL is expanded in the front, the post should get blocked: http://www.{$domain}",
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('URL with www-prefix was blocked.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When the URL has a different schema, the post should get blocked: ftp://www.{$domain}",
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('URL with different schema was blocked.'));

    // @todo Not implemented yet.
    /*
    $result = mollom('mollom.checkContent', array(
      'post_body' => "When the domain appears on its own, the post should get blocked: www.{$domain}",
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Plain domain name with www-prefix was blocked.'));
    */

    $result = mollom('mollom.removeBlacklistURL', array('url' => 'http://' . $domain));
    $this->assertTrue($result, t('The blacklisted URL was removed.'));
  }

  /**
   * Test the text blacklist functionality at the API level without using a web interface.
   */
  function testTextBlacklistAPI() {
    // Remove any stale blacklist entries from test runs that did not finish.
    $blacklist = mollom('mollom.listBlacklistText');
    foreach ($blacklist as $entry) {
      if (time() - strtotime($entry['created']) > 86400) {
        mollom('mollom.removeBlacklistText', array(
          'text' => $entry['text'],
          'context' => $entry['context'],
          'reason' => $entry['reason'],
        ));
      }
    }

    // Blacklist a word.
    // @todo As of now, only non-numeric, lower-case text seems to be supported.
    $term = drupal_strtolower(preg_replace('/[^a-zA-Z]/', '', $this->randomName()));
    $result = mollom('mollom.addBlacklistText', array(
      'text' => $term,
      'context' => 'everything',
      'reason' => 'spam',
      'match' => 'contains',
    ));
    $this->assertIdentical($result, TRUE, t('The text was blacklisted.'));

    // Check whether posts containing the blacklisted word are properly blocked.
    $result = mollom('mollom.checkContent', array(
      'post_body' => $term,
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Identical match was blocked.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When the term is present, the post should get blocked: " . $term,
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact match was blocked.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When match is 'contains', the word can be surrounded by other text: abc" . $term . "def",
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial match was blocked.'));

    // Update the blacklist entry to match the term only exactly.
    $result = mollom('mollom.addBlacklistText', array(
      'text' => $term,
      'context' => 'everything',
      'reason' => 'spam',
      'match' => 'exact',
    ));
    $this->assertTrue($result, t('The text was blacklisted.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When match is 'exact', it has to be exact: " . $term,
    ));
    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact match was blocked.'));

    $result = mollom('mollom.checkContent', array(
      'post_body' => "When match is 'exact', it has to be exact: abc{$term}def",
    ));
    $this->assertNotEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial match was not blocked.'));

    $result = mollom('mollom.removeBlacklistText', array(
      'text' => $term,
      'context' => 'everything',
      'reason' => 'spam',
    ));
    $this->assertTrue($result, t('The blacklisted text was removed.'));

    // Try to remove a non-existing entry.
    $result = mollom('mollom.removeBlacklistText', array(
      'text' => $term,
      'context' => 'everything',
      'reason' => 'spam',
    ));
    $this->assertMollomWatchdogMessages(WATCHDOG_EMERG);
    $this->assertNotIdentical($result, TRUE, t('Error response for a non-existing blacklist text found.'));
  }

  /**
   * Test the blacklist administration interface.
   *
   * We don't need to check whether the blacklisting actually works
   * (i.e. blocks posts) because that is tested in testTextBlacklistAPI() and
   * testURLBlacklistAPI().
   */
  function testBlacklistUI() {
    // Log in as an administrator and access the blacklist administration page.
    $this->admin_user = $this->drupalCreateUser(array(
      'administer mollom',
      'access administration pages',
    ));
    $this->drupalLogin($this->admin_user);

    // Add a word to the spam blacklist.
    $this->drupalGet('admin/settings/mollom/blacklist');
    $text = $this->randomName();
    $edit = array(
      'entry[text]' => $text,
      'entry[context]' => 'everything',
      'entry[match]' => 'contains',
    );
    $this->drupalPost(NULL, $edit, t('Add'));
    $text = drupal_strtolower($text);
    $this->assertText(t('The entry was added to the blacklist.'));
    $this->assertText($text);

    // Remove the word from the spam blacklist.
    $links = $this->xpath('//td[contains(., "' . $text . '")]/following-sibling::td/a');
    $delete_url = $GLOBALS['base_root'] . (string) $links[0]['href'];
    $this->drupalGet($delete_url);
    $this->drupalPost(NULL, array(), t('Delete'));
    $this->assertEqual($this->getUrl(), url('admin/settings/mollom/blacklist', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertNoText($text, 'Text blacklist removed.');

    // Add a word to the profanity blacklist.
    $this->drupalGet('admin/settings/mollom/blacklist/profanity');
    $text = $this->randomName();
    $edit = array(
      'entry[text]' => $text,
      'entry[context]' => 'everything',
      'entry[match]' => 'contains',
    );
    $this->drupalPost(NULL, $edit, t('Add'));
    $this->assertText(t('The entry was added to the blacklist.'));
    $text = drupal_strtolower($text);
    $this->assertText($text);

    // Remove the word from the profanity blacklist.
    $links = $this->xpath('//td[contains(., "' . $text . '")]/following-sibling::td/a');
    $delete_url = $GLOBALS['base_root'] . (string) $links[0]['href'];
    $this->drupalGet($delete_url);
    $this->drupalPost(NULL, array(), t('Delete'));
    $this->assertEqual($this->getUrl(), url('admin/settings/mollom/blacklist/profanity', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertNoText($text, 'Text blacklist removed.');
  }
}

/**
 * Tests Mollom form configuration functionality.
 */
class MollomFormConfigurationTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Form administration',
      'description' => 'Verify that forms can be properly protected and unprotected.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    parent::setUp('mollom_test');
    // Re-route Mollom communication to this testing site.
    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));

    $this->drupalLogin($this->admin_user);
  }

  /**
   * Tests configuration of form fields for textual analysis.
   */
  function testFormFieldsConfiguration() {
    // Protect Mollom test form.
    $this->drupalGet('admin/settings/mollom/add');
    $edit = array(
      'mollom[form_id]' => 'mollom_test_form',
    );
    $this->drupalPost(NULL, $edit, t('Next'));
    $this->assertText('Mollom test form');
    $edit = array(
      'mollom[mode]' => MOLLOM_MODE_ANALYSIS,
      'mollom[enabled_fields][title]' => TRUE,
      'mollom[enabled_fields][body]' => TRUE,
      'mollom[enabled_fields][exclude]' => FALSE,
      'mollom[enabled_fields][' . rawurlencode('parent][child') . ']' => TRUE,
      'mollom[enabled_fields][field]' => TRUE,
    );
    $this->drupalPost(NULL, $edit, t('Save'));

    // Verify that mollom_test_form form was protected.
    $this->assertText(t('The form protection has been added.'));
    $this->assertText('Mollom test form');
    $mollom_form = mollom_form_load('mollom_test_form');
    $this->assertTrue($mollom_form, t('Form configuration exists.'));

    // Verify that field configuration was properly stored.
    $this->drupalGet('admin/settings/mollom/manage/mollom_test_form');
    foreach ($edit as $name => $value) {
      // Skip any inputs that are not the fields for analysis checkboxes.
      if (strpos($name, '[enabled_fields]') === FALSE) {
        continue;
      }
      // assertFieldByName() does not work for checkboxes.
      // @see assertFieldChecked()
      $elements = $this->xpath('//input[@name="' . $name . '"]');
      if (isset($elements[0])) {
        if ($value) {
          $this->assertTrue(!empty($elements[0]['checked']), t('Field @name is checked', array('@name' => $name)));
        }
        else {
          $this->assertTrue(empty($elements[0]['checked']), t('Field @name is not checked', array('@name' => $name)));
        }
      }
      else {
        $this->fail(t('Field @name not found.', array('@name' => $name)));
      }
    }

    // Add a field to the stored configuration that existed previously.
    $mollom_form['enabled_fields'][] = 'orphan_field';
    mollom_form_save($mollom_form);

    // Verify that field configuration contains only available elements.
    $this->drupalGet('admin/settings/mollom/manage/mollom_test_form');
    $form_info = mollom_form_info('mollom_test_form', 'mollom_test');
    $fields = $this->xpath('//input[starts-with(@name, "mollom[enabled_fields]")]');
    $elements = array();
    foreach ($fields as $field) {
      $elements[] = substr(substr(rawurldecode($field['name']), 0, -1), 23);
    }
    $this->assertEqual($elements, array_keys($form_info['elements']), t('Field list only contains available form elements.'));

    // Try a simple submit of the form.
    $this->drupalLogout();
    $edit = array(
      'title' => 'unsure',
    );
    $this->drupalPost('mollom-test/form', $edit, 'Submit');
    $this->assertNoText('Successful form submission.');
    $this->assertText($this->unsure_message);
    $this->postCorrectCaptcha(NULL, array(), 'Submit', 'Successful form submission.');

    // Try to submit values for top-level fields.
    $edit = array(
      'title' => 'spam',
      'body' => 'spam',
    );
    $this->drupalPost('mollom-test/form', $edit, 'Submit');
    $this->assertNoText('Successful form submission.');
    $this->assertNoText($this->unsure_message);
    $this->assertText($this->spam_message);

    // Try to submit values for nested field.
    $edit = array(
      'title' => $this->randomString(),
      'parent[child]' => 'spam',
    );
    $this->drupalPost('mollom-test/form', $edit, 'Submit');
    $this->assertNoText('Successful form submission.');
    $this->assertNoText($this->unsure_message);
    $this->assertText($this->spam_message);

    // Try to submit values for nested field and multiple value field.
    // Start with ham values for simple, nested, and first multiple field.
    $edit = array(
      'title' => 'ham',
      'parent[child]' => 'ham',
      'field[new]' => 'ham',
    );
    $this->drupalPost('mollom-test/form', $edit, 'Add');

    // Verify that the form was rebuilt.
    $this->assertNoText('Successful form submission.');
    $this->assertNoText($this->unsure_message);
    $this->assertNoText($this->spam_message);

    // Add another value for multiple field.
    $edit = array(
      'field[new]' => 'ham',
    );
    $this->drupalPost(NULL, $edit, 'Add');

    // Verify that the form was rebuilt.
    $this->assertNoText('Successful form submission.');
    $this->assertNoText($this->unsure_message);
    $this->assertNoText($this->spam_message);

    // Now replace all ham values with random values, add a spam value to the
    // multiple field and submit the form.
    $edit = array(
      'title' => $this->randomString(),
      'parent[child]' => $this->randomString(),
      'field[0]' => $this->randomString(),
      'field[1]' => $this->randomString(),
      'field[new]' => 'spam',
    );
    $this->drupalPost(NULL, $edit, 'Submit');

    // Verify that the form was not submitted and cannot be submitted.
    $this->assertNoText('Successful form submission.');
    $this->assertText($this->spam_message);

    // Verify that we can remove the form protection.
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('admin/settings/mollom');
    $this->assertText('Mollom test form');

    $this->drupalPost('admin/settings/mollom/unprotect/mollom_test_form', array(), t('Confirm'));
    $this->assertEqual($this->getUrl(), url('admin/settings/mollom', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertNoText('Mollom test form');
    $this->assertText(t('The form protection has been removed.'));
    $mollom_form = mollom_form_load('mollom_test_form');
    $this->assertFalse($mollom_form, t('Form protection not found.'));

    // Verify that the form is no longer protected.
    $this->drupalLogout();
    $edit = array(
      'title' => 'unsure',
    );
    $this->drupalPost('mollom-test/form', $edit, 'Submit');
    $this->assertText('Successful form submission.');
    $this->assertNoText($this->unsure_message);
    $this->assertNoCaptchaField();
  }

  /**
   * Tests default configuration, protecting, and unprotecting forms.
   */
  function testFormAdministration() {
    $form_info = mollom_form_list();
    foreach ($form_info as $form_id => $info) {
      $form_info[$form_id] += mollom_form_info($form_id, $info['module']);
    }

    // Verify that user registration form is not protected.
    $this->drupalGet('admin/settings/mollom');
    $this->assertNoText($form_info['user_register']['title']);
    $this->assertFalse(mollom_form_load('user_register'), t('Form configuration does not exist.'));

    // Re-protect user registration form.
    $this->drupalGet('admin/settings/mollom/add');
    $this->assertNoText(t('All available forms are protected already.'));
    $edit = array(
      'mollom[form_id]' => 'user_register',
    );
    $this->drupalPost(NULL, $edit, t('Next'));
    $this->assertText($form_info['user_register']['title']);
    $this->assertNoText(t('Text fields to analyze'));
    $this->drupalPost(NULL, array(), t('Save'));

    // Verify that user registration form was protected.
    $this->assertText(t('The form protection has been added.'));
    $this->assertText($form_info['user_register']['title']);
    $this->assertTrue(mollom_form_load('user_register'), t('Form configuration exists.'));

    // Retrieve a list of all permissions to verify them below.
    $all_permissions = array();
    foreach (module_implements('perm') as $module) {
      // Transpose permissions into D7 format.
      $module_permissions = array_flip(module_invoke($module, 'perm'));
      foreach ($module_permissions as $permission => $key) {
        $module_permissions[$permission] = array(
          'title' => $permission,
          'module' => $module,
        );
      }
      $all_permissions += $module_permissions;
    }

    // Iterate over all unconfigured forms and protect them.
    foreach ($form_info as $form_id => $info) {
      if (!mollom_form_load($form_id)) {
        $edit = array(
          'mollom[form_id]' => $form_id,
        );
        $this->drupalPost('admin/settings/mollom/add', $edit, t('Next'));
        $this->assertText($info['title']);
        // Verify that forms specifying elements have all possible elements
        // preselected for textual analysis.
        $edit = array();
        if (!empty($info['elements'])) {
          foreach ($info['elements'] as $field => $label) {
            $field = rawurlencode($field);
            $this->assertFieldByName("mollom[enabled_fields][$field]", TRUE);
          }
        }
        // Verify that CAPTCHA-only forms contain no configurable fields.
        else {
          $this->assertNoText(t('Analyze text for'));
          $this->assertNoText(t('Text fields to analyze'));
        }
        // Verify that bypass permissions are output.
        $this->assertText($all_permissions['bypass mollom protection']['title']);
        foreach ($info['bypass access'] as $permission) {
          $this->assertText($all_permissions[$permission]['title']);
        }
        $this->drupalPost(NULL, $edit, t('Save'));
        $this->assertText(t('The form protection has been added.'));
      }
    }

    // Verify that trying to add a form redirects to the overview.
    $this->drupalGet('admin/settings/mollom/add');
    $this->assertText(t('All available forms are protected already.'));
    $this->assertText(t('Operations'));
  }

  /**
   * Tests programmatically, conditionally disabling Mollom.
   */
  function testFormAlter() {
    // Enable CAPTCHA-only protection for request user password form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
    $this->drupalLogout();

    // Verify regular form protection.
    $this->drupalGet('user/password');
    $this->assertCaptchaField();

    // Conditionally disable protection and verify again.
    variable_set('mollom_test_disable_mollom', TRUE);
    $this->drupalGet('user/password');
    $this->assertNoCaptchaField();
  }
}

class MollomUserFormsTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'User registration and password protection',
      'description' => 'Check that the user registration and password request forms can be protected.',
      'group' => 'Mollom',
    );
  }

  /**
   * Make sure that the request password form is protected correctly.
   *
   * @todo Test mail sending with assertMail() now that it is available.
   */
  function testProtectRequestPassword() {
    // We first enable Mollom for the request password form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
    $this->drupalLogout();

    // Create a new user.
    $this->web_user = $this->drupalCreateUser();

    $this->drupalGet('user/password');

    // Try to reset the user's password by specifying an invalid CAPTCHA.
    $edit = array('name' => $this->web_user->name);
    $this->postIncorrectCaptcha('user/password', $edit, t('E-mail new password'));
    $this->postCorrectCaptcha(NULL, array(), t('E-mail new password'));

    // Try to reset the user's password by specifying a valid CAPTCHA.
    $this->postCorrectCaptcha('user/password', $edit, t('E-mail new password'));
    $this->assertText(t('Further instructions have been sent to your e-mail address.'));
  }

  /**
   * Make sure that the user registration form is protected correctly.
   */
  function testProtectRegisterUser() {
    // We first enable Mollom for the user registration form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('user_register', MOLLOM_MODE_CAPTCHA);
    $this->drupalLogout();

    // Validate that the user registration form has a CAPTCHA text field.
    $this->drupalGet('user/register');
    $this->assertCaptchaField();

    // Try to register with an invalid CAPTCHA. Make sure the user did not
    // successfully register.
    $name = $this->randomName();
    $edit = array(
      'name' => $name,
      'mail' => $name . '@example.com',
    );
    $this->postIncorrectCaptcha('user/register', $edit, t('Create new account'));
    $this->assertFalse(user_load(array('name' => $name)), t('The user who attempted to register cannot be found in the database when the CAPTCHA is invalid.'));

    // Try to register with a valid CAPTCHA. Make sure the user was able
    // to successfully register.
    $this->postCorrectCaptcha('user/register', $edit, t('Create new account'));
    $this->assertText(t('Your password and further instructions have been sent to your e-mail address.'));
    $this->assertTrue(user_load(array('name' => $name)), t('The user who attempted to register appears in the database when the CAPTCHA is valid.'));
  }
}

/**
 * Tests Profile module integration.
 */
class MollomProfileFormsTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Profile integration',
      'description' => 'Tests Profile module integration.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    parent::setUp(array('profile'));
  }

  /**
   * Tests Profile module integration with user registration form.
   */
  function testProfileRegistration() {
    $this->drupalLogin($this->admin_user);

    // Add the three supported profile field types.
    $fields = array();
    foreach (array('textfield', 'textarea', 'url', 'list') as $type) {
      $name = 'profile_' . drupal_strtolower($this->randomName());
      $title = $this->randomString();
      $edit = array(
        'category' => 'Registration',
        'title' => $title,
        'name' => $name,
        'register' => 1,
      );
      $fields[$name] = $edit + array(
        'type' => $type,
      );
      $this->drupalPost('admin/user/profile/add/' . $type, $edit, t('Save field'));
    }

    // Enable text analysis protection for user registration form.
    $this->setProtection('user_register', MOLLOM_MODE_ANALYSIS);
    $this->drupalLogout();

    // Test each supported field separately.
    foreach ($fields as $key => $field) {
      $this->drupalGet('user/register');
      $this->assertNoCaptchaField();

      $name = $this->randomName();
      $edit = array(
        'name' => $name,
        'mail' => $name . '@example.com',
        $key => $field['type'] != 'url' ? 'unsure' : 'http://example.com/unsure',
      );
      $this->drupalPost(NULL, $edit, t('Create new account'));
      $this->assertCaptchaField();
    }

    $this->postCorrectCaptcha(NULL, array(), t('Create new account'));
    $this->assertText(t('Your password and further instructions have been sent to your e-mail address.'));
    $this->assertTrue(user_load(array('name' => $name)), t('New user was found in database.'));
  }
}

class MollomCommentFormTestCase extends MollomWebTestCase {
  private $node;

  public static function getInfo() {
    return array(
      'name' => 'Comment form protection',
      'description' => 'Check that the comment submission form can be protected.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    parent::setUp('comment');

    $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'post comments without approval', 'create story content'));
    $this->node = $this->drupalCreateNode(array('type' => 'story', 'uid' => $this->web_user->uid));
    variable_set('comment_preview_story', COMMENT_PREVIEW_OPTIONAL);
  }

  /**
   * Make sure that the comment submission form can be unprotected.
   */
  function testUnprotectedCommentForm() {
    // Request the comment reply form. There should be no CAPTCHA.
    $this->drupalLogin($this->web_user);
    $this->drupalGet('comment/reply/'. $this->node->nid);
    $this->assertNoCaptchaField();
    $this->assertNoPrivacyLink();

    // Preview a comment that is 'spam' and make sure there is still no CAPTCHA.
    $this->drupalPost(NULL, array('comment' => 'spam'), t('Preview'));
    $this->assertNoCaptchaField();
    $this->assertNoPrivacyLink();

    // Save the comment and make sure it appears.
    $this->drupalPost(NULL, array(), t('Save'));
    $this->assertRaw('<p>spam</p>', t('A comment that is known to be spam appears on the screen after it is submitted.'));
  }

  /**
   * Make sure that the comment submission form can be protected by captcha only.
   */
  function testCaptchaProtectedCommentForm() {
    // Enable Mollom CAPTCHA protection for comments.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('comment_form', MOLLOM_MODE_CAPTCHA);
    $this->drupalLogout();

    // Request the comment reply form. There should be a CAPTCHA form.
    $this->drupalLogin($this->web_user);
    $this->drupalGet('comment/reply/' . $this->node->nid);
    $this->assertCaptchaField();
    $this->assertSessionIDInForm();
    $this->assertNoPrivacyLink();

    // Try to submit an incorrect answer for the CAPTCHA, without value for
    // required field.
    $this->postIncorrectCaptcha(NULL, array(), t('Preview'));
    $this->assertText(t('Comment field is required.'));
    $this->assertSessionIDInForm();
    $this->assertNoPrivacyLink();

    // Try to submit a correct answer for the CAPTCHA, still without required
    // field value.
    $this->postCorrectCaptcha(NULL, array(), t('Preview'));
    $this->assertText(t('Comment field is required.'));
    $session_id = $this->assertSessionIDInForm();
    $this->assertNoPrivacyLink();

    // Finally, we should be able to submit a comment.
    $this->drupalPost(NULL, array('comment' => 'spam'), t('Save'));
    $this->assertRaw('<p>spam</p>', t('Spam comment could be posted with correct CAPTCHA.'));
    $cid = db_result(db_query("SELECT cid FROM {comments} WHERE comment = '%s' ORDER BY timestamp DESC", array('spam')));
    $this->assertMollomData('comment', $cid, $session_id);
  }

  /**
   * Make sure that the comment submission form can be fully protected.
   */
  function testTextAnalysisProtectedCommentForm() {
    // Enable Mollom text-classification for comments.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('comment_form');
    $this->drupalLogout();

    // Request the comment reply form.  Initially, there should be no CAPTCHA.
    $this->drupalLogin($this->web_user);
    $this->drupalGet('comment/reply/'. $this->node->nid);
    $this->assertNoCaptchaField();
    $this->assertPrivacyLink();

    // Try to save a comment that is 'unsure' and make sure there is a CAPTCHA.
    $edit = array(
      'comment' => 'unsure',
    );
    $this->drupalPost(NULL, $edit, t('Save'));
    $this->assertCaptchaField();
    $session_id = $this->assertSessionIDInForm();
    $this->assertPrivacyLink();

    // Try to submit the form by solving the CAPTCHA incorrectly. At this point,
    // the submission should be blocked and a new CAPTCHA generated, but only if
    // the comment is still neither ham or spam.
    $this->postIncorrectCaptcha(NULL, array(), t('Save'));
    $this->assertCaptchaField();
    $session_id = $this->assertSessionIDInForm();
    $this->assertPrivacyLink();

    // Correctly solving the CAPTCHA should accept the form submission.
    $this->postCorrectCaptcha(NULL, array(), t('Save'));
    $this->assertRaw('<p>' . $edit['comment'] . '</p>', t('A comment that may contain spam was found.'));
    $cid = db_result(db_query("SELECT cid FROM {comments} WHERE comment = '%s' ORDER BY timestamp DESC", array($edit['comment'])));
    $this->assertMollomData('comment', $cid, $session_id);

    // Try to save a new 'spam' comment; it should be discarded, with no CAPTCHA
    // appearing on the page.
    $this->resetSessionID();
    $this->drupalGet('comment/reply/' . $this->node->nid);
    $this->assertPrivacyLink();
    $original_number_of_comments = $this->getCommentCount($this->node->nid);
    $this->assertSpamSubmit(NULL, array('comment'), array(), t('Save'));
    $session_id = $this->assertSessionIDInForm();
    $this->assertCommentCount($this->node->nid, $original_number_of_comments);
    $this->assertPrivacyLink();

    // Try to save again; it should be discarded, with no CAPTCHA.
    $this->assertSpamSubmit(NULL, array('comment'), array(), t('Save'));
    $session_id = $this->assertSessionIDInForm();
    $this->assertCommentCount($this->node->nid, $original_number_of_comments);
    $this->assertPrivacyLink();

    // Save a new 'ham' comment.
    $this->resetSessionID();
    $this->drupalGet('comment/reply/' . $this->node->nid);
    $this->assertPrivacyLink();
    $original_number_of_comments = $this->getCommentCount($this->node->nid);
    $this->assertHamSubmit(NULL, array('comment'), array(), t('Save'));
    $this->assertRaw('<p>ham</p>', t('A comment that is known to be ham appears on the screen after it is submitted.'));
    $this->assertCommentCount($this->node->nid, $original_number_of_comments + 1);
    $cid = db_result(db_query("SELECT cid FROM {comments} WHERE comment = '%s' ORDER BY timestamp DESC", array('ham')));
    $this->assertMollomData('comment', $cid);
  }

  /**
   * Return the number of comments for a node of the given node ID.  We
   * can't use comment_num_all() here, because that is statically cached
   * and therefore will not work correctly with the SimpleTest browser.
   */
  private function getCommentCount($nid) {
    return db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $nid));
  }

  /**
   * Test that the number of comments for a node matches an expected value.
   *
   * @param $nid
   *   A node ID
   * @param $expected
   *   An integer with the expected number of comments for the node.
   * @param $message
   *   An optional string with the message to be used in the assertion.
   */
  protected function assertCommentCount($nid, $expected, $message = '') {
    $actual = $this->getCommentCount($nid);
    if (!$message) {
      $message = t('Node @nid has @actual comment(s), expected @expected.', array('@nid' => $nid, '@actual' => $actual, '@expected' => $expected));
    }
    $this->assertEqual($actual, $expected, $message);
  }
}

class MollomContactFormTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Contact form protection',
      'description' => 'Check that the contact form can be protected.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    parent::setUp('contact');

    $this->web_user = $this->drupalCreateUser(array('access site-wide contact form', 'access user profiles'));
  }

  /**
   * Make sure that the user contact form is protected correctly.
   *
   * @todo Test mail sending with assertMail() now that it is available.
   */
  function testProtectContactUserForm() {
    // Enable Mollom for the contact form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('contact_mail_user');
    $this->drupalLogout();

    $this->drupalLogin($this->web_user);
    $url = 'user/' . $this->admin_user->uid . '/contact';
    $button = t('Send e-mail');
    $success = t('The message has been sent.');

    // Submit a 'spam' message.  This should be blocked.
    $this->assertSpamSubmit($url, array('subject', 'message'), array(), $button);
    $this->assertNoText($success);

    // Submit a 'ham' message.  This should be accepted.
    $this->assertHamSubmit($url, array('subject', 'message'), array(), $button);
    $this->assertText($success);

    // Submit an 'unsure' message.  This should be accepted only after the
    // CAPTCHA has been solved.
    $this->assertUnsureSubmit($url, array('subject', 'message'), array(), $button, $success);
  }

  /**
   * Make sure that the site-wide contact form is protected correctly.
   *
   * @todo Test mail sending with assertMail() now that it is available.
   */
  function testProtectContactSiteForm() {
    // Enable Mollom for the contact form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('contact_mail_page');
    $this->drupalLogout();

    // Add some fields to the contact form so that it is active.
    // Empty 'reply' so as to not have to fiddle with auto-reply messages.
    $this->drupalLogin($this->web_user);
    db_query("INSERT INTO {contact} (category, recipients, reply) VALUES ('%s', '%s', '%s')", 'test category', $this->web_user->mail, '');

    $url = 'contact';
    $button = t('Send e-mail');
    $success = t('Your message has been sent.');

    // Submit a 'spam' message.  This should be blocked.
    $this->assertSpamSubmit($url, array('subject', 'message'), array(), $button);
    $this->assertNoText($success);

    // Submit a 'ham' message.  This should be accepted.
    $this->assertHamSubmit($url, array('subject', 'message'), array(), $button);
    $this->assertText($success);
    $report_link = $this->parseMollomMailReportLink();
    $this->assertTrue($report_link, t('Report to Mollom link found in e-mail.'));
    $this->assertEqual($report_link['entity'], 'session', t('Report link in e-mail uses entity type "session".'));
    $this->assertMollomData($report_link['entity'], $report_link['session_id']);

    // Submit an 'unsure' message.  This should be accepted only after the
    // CAPTCHA has been solved.
    $this->assertUnsureSubmit($url, array('subject', 'message'), array(), $button, $success);
    $report_link = $this->parseMollomMailReportLink();
    $this->assertTrue($report_link, t('Report to Mollom link found in e-mail.'));
    $this->assertEqual($report_link['entity'], 'session', t('Report link in e-mail uses entity type "session".'));
    $this->assertMollomData($report_link['entity'], $report_link['session_id']);

    // Report the mail to Mollom.
    $this->drupalGet($report_link['url']);
    $edit = array(
      'feedback' => 'spam',
    );
    $this->drupalPost(NULL, $edit, t('Delete'));
    $this->assertText(t('The content was successfully reported as inappropriate.'));
  }

  /**
   * Returns data about the report to Mollom link in the last sent mail.
   *
   * Contrary to DrupalWebTestCase::assertMail(), this function removes the last
   * sent mail from the internally recorded stack.
   */
  function parseMollomMailReportLink() {
    // Grab the last sent mail.
    // @see DrupalWebTestCase::assertMail()
    $captured_emails = variable_get('drupal_test_email_collector', array());
    $email = array_pop($captured_emails);
    variable_set('drupal_test_email_collector', $captured_emails);

    $found = FALSE;
    if (preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $email['body'], $matches)) {
      $found = array(
        'url' => $matches[0],
        'entity' => $matches[1],
        'session_id' => $matches[2],
        'mail' => $email,
      );
    }
    return $found;
  }
}

class MollomResellerTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Reseller functionality',
      'description' => 'Check that the reseller APIs are working properly.',
      'group' => 'Mollom',
    );
  }

  /**
   * Make sure that resellers can create a new site.
   */
  function testKeyManagement() {
    if (!$this->is_reseller) {
      // If the current test keys are not reseller keys, skip this test.
      return;
    }

    // Create 3 test sites:
    for ($i = 1; $i <= 3; $i++) {
      $keys[] = mollom('mollom.createSite', array(
        'url' => 'http://example.com/site-'. $i,
        'mail' => 'mail@example.com',
        'status' => 0,
        'testing' => 1,
      ));
    }

    // Assert that there were no XML-RPC errors or watchdog messages.
    $this->assertMollomWatchdogMessages();

    $sites = mollom('mollom.listSites');
    foreach ($sites as $site) {
      // Retrieve the site information:
      $details = mollom('mollom.getSite', array('client_key' => $site));

      $this->assertEqual($details['mail'], 'mail@example.com', t('The original information is correctly retrieved from Mollom.'));
      $this->assertEqual($details['status'], 0, t('The original information is correctly retrieved from Mollom.'));
      $this->assertEqual($details['testing'], 1, t('The original information is correctly retrieved from Mollom.'));

      // Perform a safety check to avoid that the tests would delete
      // valid sites in case someone messed up their Mollom settings!
      if ($details['mail'] == 'mail@example.com' || $details['mail'] == 'root@example.com') {
        // Update the information on the site and verify that it was updated.
        mollom('mollom.updateSite', array('client_key' => $site, 'mail' => 'root@example.com'));
        $details = mollom('mollom.getSite', array('client_key' => $site));
        $this->assertEqual($details['mail'], 'root@example.com', t('The updated information is correctly retrieved from Mollom.'));

        // Verify that the existing information did not change (partial updates).
        $this->assertEqual($details['status'], 0, t('The original information is correctly retrieved from Mollom.'));
        $this->assertEqual($details['testing'], 1, t('The original information is correctly retrieved from Mollom.'));

        // Delete the test site:
        mollom('mollom.deleteSite', array('client_key' => $site));
      }
      else {
        $this->fail(t('We tried to delete a non-test site.'));
      }
    }

    // Assert that there were no XML-RPC errors or watchdog messages.
    $this->assertMollomWatchdogMessages();

    // Retrieve information about a non-existing site:
    $details = mollom('mollom.getSite', array('client_key' => 'bogus'));
    $this->assertEqual(xmlrpc_errno(), TRUE, t('Retrieving information from a non-existing site returned an XML-RPC error.'));
    $this->assertMollomWatchdogMessages(WATCHDOG_EMERG);

    // Verify that all sites have been deleted:
    $sites = mollom('mollom.listSites');
    $this->assertEqual(count($sites), 0, t('All Mollom sites have been deleted.'));
  }
}

/**
 * Tests form value processing.
 */
class MollomDataTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Data processing',
      'description' => 'Verify that form registry information is properly transformed into data that is sent to Mollom servers.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    // Enable testing server implementation.
    parent::setUp('mollom_test');
    // Re-route Mollom communication to this testing site.
    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));
  }

  /**
   * Test mollom_form_get_values().
   */
  function testFormGetValues() {
    global $user;

    // Form registry information.
    $form_info = array(
      'elements' => array(
        'subject' => 'Subject',
        'message' => 'Message',
        'parent][child' => 'Some nested element',
      ),
      'mapping' => array(
        'post_title' => 'subject',
        'author_name' => 'name',
        'author_mail' => 'mail',
      ),
    );
    // Fields configured via Mollom admin UI based on $form_info['elements'].
    $fields = array(
      'subject',
      'message',
      'parent][child',
    );

    // Verify submitted form values for an anonymous/arbitrary user.
    $values = array(
      'subject' => 'Foo',
      'message' => 'Bar',
      'parent' => array(
        'child' => 'Beer',
      ),
      'name' => 'Drupaler',
      'mail' => 'drupaler@example.com',
    );
    $data = mollom_form_get_values($values, $fields, $form_info['mapping']);

    $this->assertSame('post_title', $data['post_title'], $values['subject']);
    $this->assertSame('post_body', $data['post_body'], $values['message'] . "\n" . $values['parent']['child']);
    $this->assertSame('author_name', $data['author_name'], $values['name']);
    $this->assertSame('author_mail', $data['author_mail'], $values['mail']);
    $this->assertFalse(isset($data['author_url']), t('author_url: Undefined.'));
    $this->assertFalse(isset($data['author_openid']), t('author_openid: Undefined.'));
    $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.'));
    $this->assertSame('author_ip', $data['author_ip'], ip_address());

    // Verify submitted form values for an registered user.
    $values = array(
      'subject' => 'Foo',
      'message' => 'Bar',
      'name' => $this->admin_user->name,
    );
    $data = mollom_form_get_values($values, $fields, $form_info['mapping']);

    $this->assertSame('post_title', $data['post_title'], $values['subject']);
    $this->assertSame('post_body', $data['post_body'], $values['message']);
    $this->assertSame('author_name', $data['author_name'], $this->admin_user->name);
    $this->assertSame('author_mail', $data['author_mail'], $this->admin_user->mail);
    $this->assertFalse(isset($data['author_url']), t('author_url: Undefined.'));
    // @todo Test this.
    $this->assertFalse(isset($data['author_openid']), t('author_openid: Undefined.'));
    $this->assertSame('author_id', $data['author_id'], $this->admin_user->uid);
    $this->assertSame('author_ip', $data['author_ip'], ip_address());
  }

  /**
   * Test submitted post and author information for textual analysis.
   */
  function testAnalysis() {
    $this->drupalLogin($this->admin_user);
    $this->setProtection('comment_form');

    // Make comment preview optional.
    $edit = array(
      'comment_preview' => 0,
    );
    $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type'));

    // Create a node we can comment on.
    $node = $this->drupalCreateNode(array('type' => 'story', 'promote' => 1));
    $this->drupalGet('');
    $this->assertText($node->title);

    // Log in regular user and post a comment.
    $this->drupalLogout();
    $this->web_user = $this->drupalCreateUser();
    $this->drupalLogin($this->web_user);
    $this->drupalGet('');
    $this->clickLink(t('Add new comment'));

    $edit = array(
      'subject' => $this->randomString(),
      'comment' => 'unsure',
    );
    $this->drupalPost(NULL, $edit, t('Save'));
    $this->assertText($this->unsure_message);

    // Verify that submitted data equals post data.
    $data = $this->getServerRecord();
    $this->assertSame('post_title', $data['post_title'], $edit['subject']);
    $this->assertSame('post_body', $data['post_body'], $edit['comment']);
    $this->assertSame('author_name', $data['author_name'], $this->web_user->name);
    $this->assertSame('author_mail', $data['author_mail'], $this->web_user->mail);
    $this->assertSame('author_id', $data['author_id'], $this->web_user->uid);

    $this->PostCorrectCaptcha(NULL, array(), t('Save'));
    $comment = db_fetch_object(db_query("SELECT * FROM {comments} WHERE subject = '%s'", $edit['subject']));
    $this->assertTrue($comment, t('Comment exists in database.'));

    // Verify that submitted data equals post data.
    $data = $this->getServerRecord('mollom.checkCaptcha');
    $this->assertSame('author_id', $data['author_id'], $this->web_user->uid);

    // Allow anonymous users to post comments without approval.
    $this->drupalLogin($this->admin_user);
    $edit = array(
      DRUPAL_ANONYMOUS_RID . '[access comments]' => TRUE,
      DRUPAL_ANONYMOUS_RID . '[post comments]' => TRUE,
      DRUPAL_ANONYMOUS_RID . '[post comments without approval]' => TRUE,
    );
    $this->drupalPost('admin/user/permissions', $edit, t('Save permissions'));

    // Allow anonymous users to post contact information.
    $edit = array(
      'comment_anonymous' => COMMENT_ANONYMOUS_MAY_CONTACT,
    );
    $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type'));

    // Log out and post a comment as anonymous user.
    $this->resetServerRecords();
    $this->drupalLogout();
    $this->drupalGet('node/' . $node->nid);
    $this->clickLink(t('Add new comment'));
    // Ensure we have some potentially escaped characters in the values.
    $edit = array(
      'name' => $this->randomString(6) . ' & ' . $this->randomString(8),
      'mail' => 'mollom@example.com',
      'homepage' => 'http://mollom.com',
      'subject' => '"' . $this->randomString() . '"',
      'comment' => 'unsure',
    );
    $this->drupalPost(NULL, $edit, t('Save'));
    $this->assertText($this->unsure_message);

    // Verify that submitted data equals post data.
    $data = $this->getServerRecord();
    $this->assertSame('post_title', $data['post_title'], $edit['subject']);
    $this->assertSame('post_body', $data['post_body'], $edit['comment']);
    $this->assertSame('author_name', $data['author_name'], $edit['name']);
    $this->assertSame('author_mail', $data['author_mail'], $edit['mail']);
    $this->assertSame('author_url', $data['author_url'], $edit['homepage']);
    $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.'));

    $this->PostCorrectCaptcha(NULL, array(), t('Save'));
    $comment = db_fetch_object(db_query("SELECT * FROM {comments} WHERE subject = '%s'", $edit['subject']));
    $this->assertTrue($comment, t('Comment exists in database.'));

    // Verify that submitted data equals post data.
    $data = $this->getServerRecord('mollom.checkCaptcha');
    $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.'));

    // Log in admin user and edit comment containing spam.
    $this->resetServerRecords();
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('comment/edit/' . $comment->cid);
    // Post without modification.
    $this->drupalPost(NULL, array(), t('Save'));

    // Verify that no data was submitted to Mollom.
    $data = $this->getServerRecord();
    $this->assertFalse($data, t('Administrative form submission was not validated by Mollom.'));
  }

  /**
   * Tests that protected forms contain a hidden honeypot field and its value is recorded.
   */
  function testHoneypot() {
    // Enable protection for mollom_test_form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('mollom_test_form');
    $this->drupalLogout();

    // Verify that the hidden honeypot field is output.
    $this->drupalGet('mollom-test/form');
    $elements = $this->xpath(strtr('//div[contains(@style, :style)]/descendant::input[@name = :name]', array(
      ':style' => '"display: none"',
      ':name' => '"mollom[homepage]"',
    )));
    $this->assertEqual(count($elements), 1, t('Hidden honeypot field found.'));

    // Verify that a honeypot value is sent to mollom.checkContent.
    $edit = array(
      'title' => 'unsure',
      'body' => 'unsure',
      'mollom[homepage]' => 'HONEYPOT-VALUE',
    );
    $this->drupalPost(NULL, $edit, 'Submit');
    $this->assertCaptchaField();
    $data = $this->getServerRecord();
    $this->assertSame('honeypot', $data['honeypot'], $edit['mollom[homepage]']);

    $this->postCorrectCaptcha(NULL, array(), 'Submit', 'Successful form submission.');
    $data = $this->getServerRecord();
    $this->assertSame('honeypot', $data['honeypot'], $edit['mollom[homepage]']);
    $data = $this->getServerRecord('mollom.checkCaptcha');
    $this->assertSame('honeypot', $data['honeypot'], $edit['mollom[homepage]']);

    // Change form protection to CAPTCHA only.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('mollom_test_form', MOLLOM_MODE_CAPTCHA);
    $this->drupalLogout();
    $this->resetServerRecords();

    // Verify that the hidden honeypot field is output.
    $this->drupalGet('mollom-test/form');
    $elements = $this->xpath(strtr('//div[contains(@style, :style)]/descendant::input[@name = :name]', array(
      ':style' => '"display: none"',
      ':name' => '"mollom[homepage]"',
    )));
    $this->assertEqual(count($elements), 1, t('Hidden honeypot field found.'));

    // Verify that a honeypot value is sent to mollom.checkContent.
    // postCorrectCaptcha() cannot be used for mollom_test_form, since the form
    // is re-displayed again after a successful form submission.
    $edit = array(
      'title' => $this->randomString(),
      'mollom[captcha]' => 'correct',
      'mollom[homepage]' => 'HONEYPOT-VALUE',
    );
    $this->drupalPost(NULL, $edit, 'Submit');
    $this->assertText('Successful form submission.');
    $data = $this->getServerRecord('mollom.checkCaptcha');
    $this->assertSame('honeypot', $data['honeypot'], $edit['mollom[homepage]']);
  }

  /**
   * Tests automated 'post_id' mapping and session data storage.
   *
   * This is an atomic test to verify that a simple 'post_id' mapping defined
   * via hook_mollom_form_info() is sufficient for basic integration with
   * Mollom (without reporting).
   */
  function testPostIdMapping() {
    // Enable protection for mollom_test_form.
    $this->drupalLogin($this->admin_user);
    $this->setProtection('mollom_test_form');
    $this->drupalLogout();

    // Submit a mollom_test thingy.
    $edit = array(
      'title' => 'ham',
      'body' => $this->randomString(),
    );
    $this->drupalPost('mollom-test/form', $edit, 'Submit');
    $this->assertText('Successful form submission.');
    $mid = $this->getFieldValueByName('mid');
    $this->assertTrue($mid > 0, t('Submission was stored.'));
    $data = $this->assertMollomData('mollom_test', $mid);

    // Ensure we were redirected to the form for the stored entry.
    $this->assertFieldByName('body', $edit['body'], t('Existing body value found.'));
    $new_mid = $this->getFieldValueByName('mid');
    $this->assertEqual($new_mid, $mid, t('Existing entity id found.'));

    // Verify that session data was stored.
    $this->assertSame('entity', $data->entity, 'mollom_test');
    $this->assertSame('id', $data->id, $mid);
    $this->assertSame('form_id', $data->form_id, 'mollom_test_form');
    $count = db_result(db_query("SELECT COUNT(1) FROM {mollom}"));
    $this->assertEqual($count, 1, t('Data was stored in {mollom}.'));

    // Update the stored entry.
    $edit['title'] = 'unsure';
    $this->drupalPost(NULL, $edit, 'Submit');
    $this->assertCaptchaField();
    $this->postCorrectCaptcha(NULL, array(), 'Submit', 'Successful form submission.');
    $new_data = $this->assertMollomData('mollom_test', $mid);

    // Verify that only session data was updated.
    $this->assertSame('entity', $data->entity, $new_data->entity);
    $this->assertSame('id', $data->id, $new_data->id);
    $this->assertNotSame('session_id', $data->session_id, $new_data->session_id);
    $this->assertSame('form_id', $data->form_id, $new_data->form_id);
    $this->assertSame('quality', $data->quality, $new_data->quality);
    $count = db_result(db_query("SELECT COUNT(1) FROM {mollom}"));
    $this->assertEqual($count, 1, t('Stored data in {mollom} was updated.'));
  }

  /**
   * Tests data sent for mollom.verifyKey.
   */
  function testVerifyKey() {
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('admin/settings/mollom/settings');

    // Verify that we additionally sent version data.
    $data = $this->getServerRecord('mollom.verifyKey');
    $info = _mollom_get_version();
    $this->assertTrue(!empty($info['platform_name']), t('Version information found.'));
    $this->assertSame('platform_name', $data['platform_name'], $info['platform_name']);
    $this->assertSame('platform_version', $data['platform_version'], $info['platform_version']);
    $this->assertSame('client_name', $data['client_name'], $info['client_name']);
    $this->assertSame('client_version', $data['client_version'], $info['client_version']);
  }
}

/**
 * Tests text analysis functionality.
 *
 * @todo Verify that no button captions appear in the data that is sent for
 *   analyis; i.e., no "Add" string for mollom_test_form.
 */
class MollomAnalysisTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Text analysis',
      'description' => 'Tests text analysis functionality.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    // @todo This is the new default setUp() procedure for all new tests, which
    //   should be moved into MollomWebTestCase::setUp() after cleaning up the
    //   tests.
    $this->disableDefaultSetup = TRUE;
    parent::setUp(array('mollom', 'mollom_test'));
    $this->setKeys();
    $this->assertValidKeys();

    $this->admin_user = $this->drupalCreateUser(array(
      'access administration pages',
      'administer mollom',
    ));
    $this->web_user = $this->drupalCreateUser(array('access content'));
  }

  /**
   * Tests retaining bad posts and moderating them.
   */
  function testRetain() {
    $this->drupalLogin($this->admin_user);
    // Verify that mollom_basic_test_form cannot be configured to put posts into
    // moderation queue.
    $this->drupalGet('admin/config/content/mollom/manage/mollom_basic_elements_test_form');
    $this->assertNoFieldByName('mollom[discard]');
    // Configure mollom_test_form to accept bad posts.
    $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array(
      'mollom[discard]' => 0,
    ));
    $this->drupalLogout();

    // Verify that we are able to post spam and the post is unpublished.
    $edit = array(
      'title' => $this->randomString(),
      'body' => 'spam profanity',
    );
    $this->drupalPost('mollom-test/form', $edit, 'Submit');
    $mid = $this->assertTestSubmitData();
    $data = $this->assertMollomData('mollom_test', $mid);
    $record = mollom_test_load($mid);
    $this->assertEqual($record['status'], 0, t('Unpublished test post found.'));
    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);

    // Verify that editing the post does neither change the session data, nor
    // the publishing status.
    $edit = array(
      'title' => $this->randomString(),
      'body' => 'spam profanity spam profanity',
    );
    $this->drupalPost(NULL, $edit, 'Submit');
    $mid = $this->assertTestSubmitData($mid);
    $data = $this->assertMollomData('mollom_test', $mid);
    $record = mollom_test_load($mid);
    $this->assertEqual($record['status'], 0, t('Unpublished test post found.'));
    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);

    // Verify that publishing the post changes the session data accordingly.
    $this->drupalLogin($this->admin_user);
    $edit = array(
      'status' => TRUE,
    );
    $this->drupalPost('mollom-test/form/' . $mid, $edit, 'Submit');
    $mid = $this->assertTestSubmitData($mid);
    $data = $this->assertMollomData('mollom_test', $mid);
    $record = mollom_test_load($mid);
    $this->assertEqual($record['status'], 1, t('Published test post found.'));
    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);

    // Verify that neither ham or unsure spam posts, nor non-profane posts are
    // marked for moderation.
    $this->drupalLogout();
    $expectations = array(
      'ham' => array('spam' => MOLLOM_ANALYSIS_HAM, 'profanity' => 0),
      'unsure' => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0),
      $this->randomString() => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0),
    );
    foreach ($expectations as $body => $expected) {
      $edit = array(
        'title' => $this->randomString(),
        'body' => $body,
      );
      $this->drupalPost('mollom-test/form', $edit, 'Submit');
      if ($expected['spam'] == MOLLOM_ANALYSIS_UNSURE) {
        $this->postCorrectCaptcha(NULL, array(), 'Submit');
      }
      $mid = $this->assertTestSubmitData();
      $data = $this->assertMollomData('mollom_test', $mid);
      $record = mollom_test_load($mid);
      $this->assertEqual($record['status'], 1, t('Published test post %body found.', array('%body' => $body)));
      $this->assertSame('spam', $data->spam, $expected['spam']);
    }
  }

  /**
   * 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 {
      $this->assertTrue($mid > 0, t('Test record id @id found.', array('@id' => $mid)));
    }
    return $mid;
  }
}

/**
 * Tests report to Mollom functionality.
 */
class MollomReportTestCase extends MollomWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Reporting functionality',
      'description' => 'Verify that session data is properly stored and content can be reported to Mollom.',
      'group' => 'Mollom',
    );
  }

  function setUp() {
    parent::setUp('comment');

    $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'post comments without approval', 'create story content'));
  }

  /**
   * Tests reporting comments.
   */
  function testReportComment() {
    $this->drupalLogin($this->admin_user);
    $this->setProtection('comment_form');
    $this->drupalLogout();

    $this->node = $this->drupalCreateNode(array('type' => 'story'));
    variable_set('comment_preview_story', COMMENT_PREVIEW_OPTIONAL);

    // Post a comment.
    $this->drupalLogin($this->web_user);
    $edit = array(
      'comment' => 'ham',
    );
    $this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save'));
    $this->comment = db_fetch_object(db_query("SELECT * FROM {comments} WHERE comment = '%s' AND nid = %d", array($edit['comment'], $this->node->nid)));
    $this->assertTrue($this->comment, t('Comment was found in the database.'));
    $this->assertMollomData('comment', $this->comment->cid);

    // Log in comment administrator and verify that we can report to Mollom.
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('node/' . $this->node->nid);
    $this->assertText($edit['comment'], t('Comment found.'));
    $this->clickLink('report to Mollom');
    $edit = array(
      'feedback' => 'spam',
    );
    $this->drupalPost(NULL, $edit, t('Delete'));
    $this->assertText(t('The comment has been deleted.'));
    $this->assertText(t('The content was successfully reported as inappropriate.'));

    // Verify that the comment and Mollom session data has been deleted.
    $this->assertFalse(_comment_load($this->comment->cid), t('Comment was deleted.'));
    $this->assertNoMollomData('comment', $this->comment->cid);
  }

  /**
   * Tests mass-reporting comments.
   */
  function testMassReportComments() {
    $this->drupalLogin($this->admin_user);
    $this->setProtection('comment_form');
    $this->drupalLogout();

    $this->node = $this->drupalCreateNode(array('type' => 'story'));
    variable_set('comment_preview_story', COMMENT_PREVIEW_OPTIONAL);

    // Post 3 comments.
    $this->drupalLogin($this->web_user);
    $this->comments = array();
    foreach (range(1, 3) as $num) {
      $edit = array(
        'subject' => $this->randomName(),
        'comment' => 'ham',
      );
      $this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save'));
      $this->comments[$num] = db_fetch_object(db_query("SELECT * FROM {comments} WHERE subject = '%s' AND nid = %d", array($edit['subject'], $this->node->nid)));
      $this->assertTrue($this->comments[$num], t('Comment was found in the database.'));
      $this->assertMollomData('comment', $this->comments[$num]->cid);
    }

    // Log in comment administrator and verify that we can mass-report all
    // comments to Mollom.
    $this->drupalLogin($this->admin_user);
    $this->drupalGet('admin/content/comment');
    $edit = array(
      'operation' => 'mollom-unpublish',
    );
    foreach ($this->comments as $comment) {
      $this->assertText($comment->subject, t('Comment found.'));
      $edit["comments[{$comment->cid}]"] = TRUE;
    }
    $this->drupalPost(NULL, $edit, t('Update'));
    $this->assertText(t('The selected comments have been reported as inappropriate and are unpublished.'));

    // Verify that unpublished comments are found in approval queue and
    // mass-report all comments again to delete them.
    $this->drupalGet('admin/content/comment/approval');
    $edit['operation'] = 'mollom-delete';
    foreach ($this->comments as $comment) {
      $this->assertText($comment->subject, t('Comment found.'));
    }
    $this->drupalPost(NULL, $edit, t('Update'));
    $this->assertText(t('The selected comments have been reported as inappropriate and are deleted.'));

    // Verify that the comments and Mollom session data has been deleted.
    foreach ($this->comments as $comment) {
      $this->assertFalse(_comment_load($comment->cid), t('Comment was deleted.'));
      $this->assertNoMollomData('comment', $comment->cid);
    }
  }
}

