You are here

botcha.test in BOTCHA Spam Prevention 7

Same filename and directory in other branches
  1. 6 botcha.test
  2. 6.2 botcha.test
  3. 7.2 botcha.test

Tests for BOTCHA module.

File

botcha.test
View source
<?php

/**
 * @file
 * Tests for BOTCHA module.
 */

// TODO: write test for BOTCHAs on admin pages
// TODO: test for JS recipes (how??)
// TODO: test about placement (comment form, node forms, log in form, etc)
// TODO: test custom BOTCHA validation stuff
// TODO: test if entry on status report (Already X blocked form submissions) works
// TODO: Add tests for integration with CAPTCHA
// TODO: Test secret key auto-generation
// TODO: Test behavior around "back" button for legit users - behave nicely, what message, how form values are preserved, etc.
// Some constants for better reuse.
define('BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE', 'You must be a human, not a spam bot, to submit forms on this website.');
define('BOTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE', 'Form session reuse detected.');
define('BOTCHA_UNKNOWN_CSID_ERROR_MESSAGE', 'You must be a human, not a spam bot, to submit forms on this website.');
define('BOTCHA_NO_JS_ERROR_MESSAGE', 'Please enable Javascript to use this form.');

/**
 * Base class for BOTCHA tests.
 *
 * Provides common setup stuff and various helper functions
 */
abstract class BotchaBaseWebTestCase extends DrupalWebTestCase {

  /**
   * User with various administrative permissions.
   * @var Drupal user
   */
  protected $admin_user;

  /**
   * Normal visitor with limited permissions
   * @var Drupal user;
   */
  protected $normal_user;

  /**
   * Form ID of comment form on standard (page) node
   * @var string
   */
  const COMMENT_FORM_ID = 'comment_node_page_form';

  /**
   * Drupal path of the (general) BOTCHA admin page
   */
  const BOTCHA_ADMIN_PATH = 'admin/config/people/botcha';
  function setUp() {

    // Load two modules: the botcha module itself and the comment module for testing anonymous comments.
    parent::setUp('botcha', 'comment');
    module_load_include('inc', 'botcha');

    // Create a normal user.
    $permissions = array(
      'access comments',
      'post comments',
      'skip comment approval',
      'access content',
      'create page content',
      'edit own page content',
    );
    $this->normal_user = $this
      ->drupalCreateUser($permissions);

    // Create an admin user.
    $permissions[] = 'administer BOTCHA settings';
    $permissions[] = 'skip BOTCHA';
    $permissions[] = 'administer permissions';
    $permissions[] = 'administer content types';
    $this->admin_user = $this
      ->drupalCreateUser($permissions);

    // Put comments on page nodes on a separate page (default in D7: below post).
    variable_set('comment_form_location_page', COMMENT_FORM_SEPARATE_PAGE);

    // Tweak BOTCHA configuration
    db_update('botcha_points')
      ->fields(array(
      'botcha_type' => 'none',
    ))
      ->condition('form_id', 'user_login')
      ->execute();
    db_update('botcha_points')
      ->fields(array(
      'botcha_type' => 'none',
    ))
      ->condition('form_id', 'user_login_block')
      ->execute();
  }

  /**
   * Assert that the response is accepted:
   * no "unknown CSID" message, no "CSID reuse attack detection" message,
   * no "wrong answer" message.
   */
  protected function assertBotchaResponseAccepted() {

    // There should be no error message about unknown BOTCHA session ID.
    $this
      ->assertNoText(t(BOTCHA_UNKNOWN_CSID_ERROR_MESSAGE), 'BOTCHA response should be accepted (known CSID).', 'BOTCHA');

    // There should be no error message about CSID reuse attack.
    $this
      ->assertNoText(t(BOTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE), 'BOTCHA response should be accepted (no BOTCHA session reuse attack detection).', 'BOTCHA');

    // There should be no error message about wrong response.
    $this
      ->assertNoText(t(BOTCHA_NO_JS_ERROR_MESSAGE), 'BOTCHA response should be accepted (JS enabled).', 'BOTCHA');
  }

  /**
   * Assert that there is a BOTCHA on the form or not.
   * @param bool $presence whether there should be a BOTCHA or not.
   */
  protected function assertBotchaPresence($presence) {
    if ($presence) {
      $this
        ->assertText('If you\'re a human, don\'t change the following field', 'There should be a BOTCHA on the form.', 'BOTCHA');
    }
    else {
      $this
        ->assertNoText('If you\'re a human, don\'t change the following field', 'There should be no BOTCHA on the form.', 'BOTCHA');
    }
  }

  /**
   * Helper function to create a node with comments enabled.
   *
   * @return
   *   Created node object.
   */
  protected function createNodeWithCommentsEnabled($type = 'page') {
    $node_settings = array(
      'type' => $type,
      'comment' => COMMENT_NODE_OPEN,
    );
    $node = $this
      ->drupalCreateNode($node_settings);
    return $node;
  }

  /**
   * Helper function to get form values array from comment form
   */
  protected function getCommentFormValuesFromForm() {

    // Submit the form using the displayed values.
    $langcode = LANGUAGE_NONE;
    $displayed = array();
    foreach (array(
      'subject' => "//input[@id='edit-subject']/@value",
      "comment_body[{$langcode}][0][value]" => "//textarea[@id='edit-comment-body-{$langcode}-0-value']",
      'botcha_response' => "//input[@id='edit-botcha-response']/@value",
    ) as $field => $path) {
      $value = current($this
        ->xpath($path));
      if (!empty($value)) {
        $displayed[$field] = (string) $value;
      }
    }
    return $displayed;
  }

  /**
   * Helper function to generate a default form values array for comment forms
   */
  protected function setCommentFormValues() {
    $langcode = LANGUAGE_NONE;
    $edit = array(
      'subject' => 'comment_subject ' . $this
        ->randomName(32),
      "comment_body[{$langcode}][0][value]" => 'comment_body ' . $this
        ->randomName(256),
    );
    return $edit;
  }

  /**
   * Helper function to generate a default form values array for node forms
   */
  protected function setNodeFormValues() {
    $langcode = LANGUAGE_NONE;
    $edit = array(
      'title' => 'node_title ' . $this
        ->randomName(32),
      "body[{$langcode}][0][value]" => 'node_body ' . $this
        ->randomName(256),
    );
    return $edit;
  }

  /**
   * Get the form_build_id from the current form in the browser.
   */
  protected function getFormBuildIdFromForm() {
    $form_build_id = current($this
      ->xpath('//input[@name="form_build_id"]/@value'));
    return $form_build_id;
  }

  /**
   * Helper function to allow comment posting for anonymous users.
   */
  protected function allowCommentPostingForAnonymousVisitors() {

    // Log in as admin.
    $this
      ->drupalLogin($this->admin_user);

    // Post user permissions form
    $edit = array(
      '1[access comments]' => true,
      '1[post comments]' => true,
      '1[skip comment approval]' => true,
    );
    $this
      ->drupalPost('admin/people/permissions', $edit, 'Save permissions');
    $this
      ->assertText('The changes have been saved.');

    // Log admin out
    $this
      ->drupalLogout();
  }

}
class BotchaTestCase extends BotchaBaseWebTestCase {
  public static function getInfo() {
    return array(
      'name' => t('General BOTCHA functionality'),
      'description' => t('Testing of the basic BOTCHA functionality.'),
      'group' => t('BOTCHA'),
    );
  }

  /**
   * Testing the protection of the user log in form.
   */
  function testBotchaOnLoginForm() {

    // Create user and test log in without BOTCHA.
    $user = $this
      ->drupalCreateUser();
    $this
      ->drupalLogin($user);

    // Log out again.
    $this
      ->drupalLogout();

    // Set a BOTCHA on login form
    botcha_set_form_id_setting('user_login', 'default');

    // Check if there is a BOTCHA on the login form (look for the title).
    $this
      ->drupalGet('user');
    $this
      ->assertBotchaPresence(TRUE);

    // Try to log in, which should fail (due to JS required for 'default' BOTCHA recipe is not present in simpletest browser).
    $edit = array(
      'name' => $user->name,
      'pass' => $user->pass_raw,
    );
    $this
      ->drupalPost('user', $edit, t('Log in'));

    // Check for error message.
    $this
      ->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), 'BOTCHA should block user login form', 'BOTCHA');

    // And make sure that user is not logged in: check for name and password fields on ?q=user
    $this
      ->drupalGet('user');
    $this
      ->assertField('name', t('Username field found.'), 'BOTCHA');
    $this
      ->assertField('pass', t('Password field found.'), 'BOTCHA');
  }

  /**
   * Assert function for testing if comment posting works as it should.
   *
   * Creates node with comment writing enabled, tries to post comment
   * with given BOTCHA response (caller should enable the desired
   * challenge on page node comment forms) and checks if the result is as expected.
   *
   * @param $node existing node to post comments to (if NULL, will be created)
   * @param $should_pass boolean describing if the posting should pass or should be blocked
   * @param $message message to prefix to nested asserts
   * @param $button name of button to click (t('Save') by default)
   */
  protected function assertCommentPosting($node, $should_pass, $message, $button = '') {
    $langcode = LANGUAGE_NONE;

    // Make sure comments on pages can be saved directely without preview.
    variable_set('comment_preview_page', DRUPAL_OPTIONAL);
    if (empty($node)) {

      // Create a node with comments enabled.
      $node = $this
        ->createNodeWithCommentsEnabled();
    }
    if (empty($button)) {
      $button = t('Save');
    }

    // Check if there is a BOTCHA on the comment form.
    $this
      ->drupalGet('comment/reply/' . $node->nid);
    $this
      ->assertBotchaPresence(TRUE);

    // Post comment on node.
    $edit = $this
      ->setCommentFormValues();
    if (!$should_pass) {

      // Screw up fields (like a bot would do)
      $edit['botcha_response'] = 'xx';
    }
    $comment_subject = $edit['subject'];
    $comment_body = $edit["comment_body[{$langcode}][0][value]"];
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, $button);
    if ($should_pass) {

      // There should be no error message.
      $this
        ->assertBotchaResponseAccepted();

      // Get node page and check that comment shows up.
      $this
        ->drupalGet('node/' . $node->nid);
      $this
        ->assertText($comment_subject, $message . ' Comment should show up on node page.', 'BOTCHA');
      $this
        ->assertText($comment_body, $message . ' Comment should show up on node page.', 'BOTCHA');
    }
    else {

      // Check for error message.
      $this
        ->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), $message . ' Comment submission should be blocked.', 'BOTCHA');

      // Check that there is still BOTCHA after failed submit.
      $this
        ->assertBotchaPresence(TRUE);

      // Get node page and check that comment is not present.
      $this
        ->drupalGet('node/' . $node->nid);
      $this
        ->assertNoText($comment_subject, $message . ' Comment should not show up on node page.', 'BOTCHA');
      $this
        ->assertNoText($comment_body, $message . ' Comment should not show up on node page.', 'BOTCHA');
    }
    return $node;
  }

  /**
   * Testing the case sensistive/insensitive validation.
   */
  function testBotchaValidation() {

    // Set Test BOTCHA on comment form
    botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');

    // Log in as normal user.
    $this
      ->drupalLogin($this->normal_user);
    $this
      ->assertCommentPosting(NULL, TRUE, 'Validation of right fields touched.');
    $this
      ->assertCommentPosting(NULL, FALSE, 'Validation of wrong fields touched.');
  }

  /**
   * Test if BOTCHA is applied when previewing comments:
   * comment preview should have BOTCHA again.
   *
   * \see testBotchaAfterNodePreview()
   */
  function testBotchaAfterCommentPreview() {

    // Set Test BOTCHA on comment form.
    botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');

    // Log in as normal user.
    $this
      ->drupalLogin($this->normal_user);

    // Create a node with comments enabled.
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Check if there is a BOTCHA on the comment form (look for the title).
    $this
      ->drupalGet('comment/reply/' . $node->nid);
    $this
      ->assertBotchaPresence(TRUE);

    // Preview comment with correct BOTCHA.
    $edit = $this
      ->setCommentFormValues();
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));

    // Check that there is still BOTCHA after preview.
    $this
      ->assertBotchaPresence(TRUE);
  }

  /**
   * Test if BOTCHA is applied when previewing nodes:
   * node preview should have BOTCHA again.
   * The preview functionality of comments and nodes works slightly different under the hood.
   * BOTCHA module should be able to handle both.
   *
   * \see testBotchaAfterCommentPreview()
   */
  function testBotchaAfterNodePreview() {

    // Set Test BOTCHA on page form.
    botcha_set_form_id_setting('page_node_form', 'test');

    // Log in as normal user.
    $this
      ->drupalLogin($this->normal_user);

    // Check if there is a BOTCHA on the node form (look for the title).
    $this
      ->drupalGet('node/add/page');
    $this
      ->assertBotchaPresence(TRUE);

    // Page settings to post, with correct BOTCHA answer.
    $edit = $this
      ->setNodeFormValues();

    // Preview the node
    $this
      ->drupalPost('node/add/page', $edit, t('Preview'));

    // Check that there is still BOTCHA after preview.
    $this
      ->assertBotchaPresence(TRUE);
  }

  /**
   * BOTCHA should also be put on admin pages if visitor
   * has no access
   */
  function testBotchaOnLoginBlockOnAdminPages() {

    // Set a BOTCHA on login block form
    botcha_set_form_id_setting('user_login_block', 'default');

    // Check if there is a BOTCHA on home page.
    $this
      ->drupalGet('node');
    $this
      ->assertBotchaPresence(TRUE);

    // Check there is a BOTCHA on "forbidden" admin pages
    $this
      ->drupalGet('admin');
    $this
      ->assertBotchaPresence(TRUE);
  }

  /**
   * Test if BOTCHA is applied correctly when failing first and then resubmitting comments:
   * comment form should have BOTCHA again and pass correct submission.
   * (We use to fail BOTCHA as it is impossible to fail comment form on its own)
   *
   * \see testBotchaAfterNodePreview()
   */
  function testBotchaResubmit() {
    $langcode = LANGUAGE_NONE;

    // Set Test BOTCHA on comment form.
    botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');

    // Create a node with comments enabled.
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Log in as normal user.
    $this
      ->drupalLogin($this->normal_user);

    // Make sure comments on pages can be saved directely without preview.
    variable_set('comment_preview_page', DRUPAL_OPTIONAL);

    // Check if there is a BOTCHA on the comment form.
    $this
      ->drupalGet('comment/reply/' . $node->nid);
    $this
      ->assertBotchaPresence(TRUE);

    // Post comment on node.
    $edit = $this
      ->setCommentFormValues();

    // Screw up fields (like a bot would do)
    $edit['botcha_response'] = 'xx';
    $comment_subject = $edit['subject'];
    $comment_body = $edit["comment_body[{$langcode}][0][value]"];
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, t('Save'));

    // Check for error message.
    $this
      ->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), 'Comment submission should be blocked.', 'BOTCHA');

    // Check that there is still BOTCHA after failed submit.
    $this
      ->assertBotchaPresence(TRUE);

    // Copy all values from the form into new one.
    $edit = $this
      ->getCommentFormValuesFromForm();

    // Get node page and check that comment is not present.
    $this
      ->drupalGet('node/' . $node->nid);
    $this
      ->assertNoText($comment_subject, 'Comment should not show up on node page.', 'BOTCHA');
    $this
      ->assertNoText($comment_body, 'Comment should not show up on node page.', 'BOTCHA');
    $comment_subject = $edit['subject'];
    $comment_body = $edit["comment_body[{$langcode}][0][value]"];

    // Save comment again with correct BOTCHA.
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, t('Save'));

    // There should be no error message.
    $this
      ->assertBotchaResponseAccepted();

    // Get node page and check that comment shows up.
    $this
      ->drupalGet('node/' . $node->nid);
    $this
      ->assertText($comment_subject, ' Comment should show up on node page.', 'BOTCHA');
    $this
      ->assertText($comment_body, ' Comment should show up on node page.', 'BOTCHA');
  }

}
class BotchaAdminTestCase extends BotchaBaseWebTestCase {
  public static function getInfo() {
    return array(
      'name' => t('BOTCHA administration functionality'),
      'description' => t('Testing of the BOTCHA administration interface and functionality.'),
      'group' => t('BOTCHA'),
    );
  }

  /**
   * Test access to the admin pages.
   */
  function testAdminAccess() {
    $this
      ->drupalLogin($this->normal_user);
    $this
      ->drupalGet(self::BOTCHA_ADMIN_PATH);
    file_put_contents('tmp.simpletest.html', $this
      ->drupalGetContent());
    $this
      ->assertText(t('Access denied'), 'Normal users should not be able to access the BOTCHA admin pages', 'BOTCHA');
    $this
      ->drupalLogin($this->admin_user);
    $this
      ->drupalGet(self::BOTCHA_ADMIN_PATH);
    $this
      ->assertNoText(t('Access denied'), 'Admin users should be able to access the BOTCHA admin pages', 'BOTCHA');
  }

  /**
   * Test the BOTCHA point setting getter/setter.
   */
  function testBotchaPointSettingGetterAndSetter() {
    $comment_form_id = self::COMMENT_FORM_ID;

    // Set to 'none'.
    botcha_set_form_id_setting($comment_form_id, 'none');
    $result = botcha_get_form_id_setting($comment_form_id);
    $this
      ->assertNotNull($result, 'Setting and getting BOTCHA point: none', 'BOTCHA');
    $this
      ->assertNull($result->botcha_type, 'Setting and getting BOTCHA point: none', 'BOTCHA');
    $result = botcha_get_form_id_setting($comment_form_id, TRUE);
    $this
      ->assertEqual($result, 'none', 'Setting and symbolic getting BOTCHA point: "none"', 'BOTCHA');

    // Set to 'default'
    botcha_set_form_id_setting($comment_form_id, 'default');
    $result = botcha_get_form_id_setting($comment_form_id);
    $this
      ->assertNotNull($result, 'Setting and getting BOTCHA point: default', 'BOTCHA');
    $this
      ->assertEqual($result->botcha_type, 'default', 'Setting and getting BOTCHA point: default', 'BOTCHA');
    $result = botcha_get_form_id_setting($comment_form_id, TRUE);
    $this
      ->assertEqual($result, 'default', 'Setting and symbolic getting BOTCHA point: "default"', 'BOTCHA');

    // Set to 'boo'.
    botcha_set_form_id_setting($comment_form_id, 'boo');
    $result = botcha_get_form_id_setting($comment_form_id);
    $this
      ->assertNotNull($result, 'Setting and getting BOTCHA point: boo', 'BOTCHA');
    $this
      ->assertEqual($result->botcha_type, 'boo', 'Setting and getting BOTCHA point: boo', 'BOTCHA');
    $result = botcha_get_form_id_setting($comment_form_id, TRUE);
    $this
      ->assertEqual($result, 'boo', 'Setting and symbolic getting BOTCHA point: "boo"', 'BOTCHA');

    // Set to NULL (which should delete the BOTCHA point setting entry).
    botcha_set_form_id_setting($comment_form_id, NULL);
    $result = botcha_get_form_id_setting($comment_form_id);
    $this
      ->assertNull($result, 'Setting and getting BOTCHA point: NULL', 'BOTCHA');
    $result = botcha_get_form_id_setting($comment_form_id, TRUE);
    $this
      ->assertNull($result, 'Setting and symbolic getting BOTCHA point: NULL', 'BOTCHA');

    //    // Set with object.
    //    $botcha_type = new stdClass;
    //    $botcha_type->botcha_type = 'fofo';
    //    botcha_set_form_id_setting($comment_form_id, $botcha_type);
    //    $result = botcha_get_form_id_setting($comment_form_id);
    //    $this->assertNotNull($result, 'Setting and getting BOTCHA point: fofo', 'BOTCHA');
    //    $this->assertEqual($result->botcha_type, 'fofo', 'Setting and getting BOTCHA point: fofo', 'BOTCHA');
    //    $result = botcha_get_form_id_setting($comment_form_id, TRUE);
    //    $this->assertEqual($result, 'fofo', 'Setting and symbolic getting BOTCHA point: "fofo"', 'BOTCHA');
  }

  /**
   * Helper function for checking BOTCHA setting of a form.
   *
   * @param $form_id the form_id of the form to investigate.
   * @param $challenge_type what the challenge type should be:
   *   NULL, 'none', 'default' or something like 'default'
   */
  protected function assertBotchaSetting($form_id, $challenge_type) {
    $result = botcha_get_form_id_setting(self::COMMENT_FORM_ID, TRUE);
    $this
      ->assertEqual($result, $challenge_type, t('Check BOTCHA setting for form: expected: @expected, received: @received.', array(
      '@expected' => var_export($challenge_type, TRUE),
      '@received' => var_export($result, TRUE),
    )), 'BOTCHA');
  }

  /**
   * Testing of the BOTCHA administration links.
   */
  function testBotchaAdminLinks() {

    // Log in as admin
    $this
      ->drupalLogin($this->admin_user);

    // Test for [#1861016]
    $this
      ->drupalGet('user/' . $this->admin_user->uid . '/edit');
    $this
      ->assertText($this->admin_user->name, t('User name found.'), 'BOTCHA');

    // Enable BOTCHA administration links.
    $edit = array(
      'botcha_administration_mode' => TRUE,
    );
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH, $edit, 'Save configuration');

    // Create a node with comments enabled.
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Go to node page
    $this
      ->drupalGet('node/' . $node->nid);

    // Click the add new comment link
    $this
      ->clickLink(t('Add new comment'));
    $add_comment_url = $this
      ->getUrl();

    // Remove fragment part from comment URL to avoid problems with later asserts
    $add_comment_url = strtok($add_comment_url, "#");

    ////////////////////////////////////////////////////////////

    // Click the BOTCHA admin link to enable a challenge.
    $this
      ->clickLink(t('Add BOTCHA protection on form'));

    // Enable 'default' BOTCHA.
    $edit = array(
      'botcha_type' => 'default',
    );
    $this
      ->drupalPost($this
      ->getUrl(), $edit, t('Save'));

    // Check if returned to original comment form.
    $this
      ->assertUrl($add_comment_url, array(), 'After setting BOTCHA with BOTCHA admin links: should return to original form.', 'BOTCHA');

    // Check if BOTCHA was successfully enabled (on BOTCHA admin links fieldset).
    $this
      ->assertText(t('Saved BOTCHA point settings.', array(
      '@type' => 'default',
    )), 'Enable a challenge through the BOTCHA admin links', 'BOTCHA');

    // Check if BOTCHA was successfully enabled (through API).
    $this
      ->assertBotchaSetting(self::COMMENT_FORM_ID, 'default');

    //////////////////////////////////////////////////////

    // Edit challenge type through BOTCHA admin links.
    $this
      ->clickLink(t('change'));

    // Enable 'default' BOTCHA.
    $edit = array(
      'botcha_type' => 'default',
    );
    $this
      ->drupalPost($this
      ->getUrl(), $edit, t('Save'));

    // Check if returned to original comment form.
    $this
      ->assertEqual($add_comment_url, $this
      ->getUrl(), 'After editing challenge type BOTCHA admin links: should return to original form.', 'BOTCHA');

    // Check if BOTCHA was successfully changed (on BOTCHA admin links fieldset).
    // This is actually the same as the previous setting because the botcha/Math is the
    // default for the default challenge. TODO Make sure the edit is a real change.
    $this
      ->assertText(t('Saved BOTCHA point settings.', array(
      '@type' => 'default',
    )), 'Enable a challenge through the BOTCHA admin links', 'BOTCHA');

    // Check if BOTCHA was successfully edited (through API).
    $this
      ->assertBotchaSetting(self::COMMENT_FORM_ID, 'default');

    //////////////////////////////////////////////////////

    // Disable challenge through BOTCHA admin links.
    $this
      ->clickLink(t('disable'));

    // And confirm.
    $this
      ->drupalPost($this
      ->getUrl(), array(), 'Disable');

    // Check if returned to original comment form.
    $this
      ->assertEqual($add_comment_url, $this
      ->getUrl(), 'After disablin challenge with BOTCHA admin links: should return to original form.', 'BOTCHA');

    // Check if BOTCHA was successfully disabled (on BOTCHA admin links fieldset).
    $this
      ->assertText(t('Disabled BOTCHA for form'), 'Disable challenge through the BOTCHA admin links', 'BOTCHA');

    // Check if BOTCHA was successfully disabled (through API).
    $this
      ->assertBotchaSetting(self::COMMENT_FORM_ID, 'none');
  }
  function testUntrustedUserPosting() {

    // Set BOTCHA on comment form.
    botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');

    // Create a node with comments enabled.
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Log in as normal (untrusted) user.
    $this
      ->drupalLogin($this->normal_user);

    // Go to node page and click the "add comment" link.
    $this
      ->drupalGet('node/' . $node->nid);
    $this
      ->clickLink(t('Add new comment'));
    $add_comment_url = $this
      ->getUrl();

    // Check if BOTCHA is visible on form.
    $this
      ->assertBotchaPresence(TRUE);

    // Try to post a comment with wrong answer.
    $edit = $this
      ->setCommentFormValues();

    // Screw up fields (like a bot would do)
    $edit['botcha_response'] = 'xx';
    $this
      ->drupalPost($add_comment_url, $edit, t('Preview'));
    $this
      ->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), 'wrong BOTCHA should block form submission.', 'BOTCHA');

    //TODO: more testing for untrusted posts.
  }

  /**
   * Test the BOTCHA placement flushing.
   */
  function testBotchaPlacementCacheFlushing() {
    $comment_form_id = self::COMMENT_FORM_ID;
    botcha_set_form_id_setting($comment_form_id, 'default');
    variable_set('botcha_administration_mode', TRUE);

    //    $placement_map = variable_get('botcha_placement_map_cache', NULL);
    //    $this->assertNull($placement_map, 'BOTCHA placement cache should be unset.');
    // Create a node with comments enabled.
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Visit comment form of commentable node to fill the BOTCHA placement cache.
    $this
      ->drupalLogin($this->admin_user);
    $this
      ->drupalGet('comment/reply/' . $node->nid);

    // Check if there is BOTCHA placement cache.
    $placement_map = variable_get('botcha_placement_map_cache', NULL);
    $this
      ->assertNotNull($placement_map, 'BOTCHA placement cache should be set.');

    /* UNUSED, as BOTCHA does not invoke botcha_placement_map_cache on protected forms
        // Set BOTCHA on user register form.
        botcha_set_form_id_setting('user_register_form', 'default');
        // Visit user register form to fill the BOTCHA placement cache.
        $this->drupalGet('user/register');
        // Check if there is BOTCHA placement cache.
        $placement_map = variable_get('botcha_placement_map_cache', NULL);
        $this->assertNotNull($placement_map, 'BOTCHA placement cache should be set.');
    */

    /* UNUSED, as BOTCHA does not have unset placement cache control
        // Flush the cache
        $this->drupalLogin($this->admin_user);
        $this->drupalPost(self::BOTCHA_ADMIN_PATH, array(), t('Flush the BOTCHA placement cache'));
        // Check that the placement cache is unset
        $placement_map = variable_get('botcha_placement_map_cache', NULL);
        $this->assertNull($placement_map, 'BOTCHA placement cache should be unset after flush.');
    */
  }

  /**
   * Helper function to get the BOTCHA point setting straight from the database.
   * @param string $form_id
   * @return stdClass object
   */
  private function getBotchaPointSettingFromDatabase($form_id) {
    $result = db_query("SELECT * FROM {botcha_points} WHERE form_id = :form_id", array(
      ':form_id' => $form_id,
    ))
      ->fetchObject();
    return $result;
  }

  /**
   * Method for testing the BOTCHA point administration
   */
  function testBotchaPointAdministration() {

    // Generate BOTCHA point data:
    // Drupal form ID should consist of lowercase alphanumerics and underscore)
    $botcha_point_form_id = 'form_' . strtolower($this
      ->randomName(32));

    // the 'default' BOTCHA is always available, so let's use it
    $botcha_point_type = 'default';

    // Log in as admin
    $this
      ->drupalLogin($this->admin_user);

    // Set BOTCHA point through admin/user/botcha/botcha_point
    $form_values = array(
      'botcha_point_form_id' => $botcha_point_form_id,
      'botcha_type' => $botcha_point_type,
    );
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point', $form_values, t('Save'));
    $this
      ->assertText(t('Saved BOTCHA point settings.'), 'Saving of BOTCHA point settings');

    // Check in database
    $result = $this
      ->getBotchaPointSettingFromDatabase($botcha_point_form_id);
    $this
      ->assertEqual($result->botcha_type, $botcha_point_type, 'Enabled BOTCHA point should have type set');

    // Disable BOTCHA point again
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/disable', array(), t('Disable'));
    $this
      ->assertRaw(t('Disabled BOTCHA for form %form_id.', array(
      '%form_id' => $botcha_point_form_id,
    )), 'Disabling of BOTCHA point');

    // Check in database
    $result = $this
      ->getBotchaPointSettingFromDatabase($botcha_point_form_id);
    $this
      ->assertNull($result->botcha_type, 'Disabled BOTCHA point should have NULL as type');

    // Set BOTCHA point through admin/user/botcha/botcha_point/$form_id
    $form_values = array(
      'botcha_type' => $botcha_point_type,
    );
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id, $form_values, t('Save'));
    $this
      ->assertText(t('Saved BOTCHA point settings.'), 'Saving of BOTCHA point settings');

    // Check in database
    $result = $this
      ->getBotchaPointSettingFromDatabase($botcha_point_form_id);
    $this
      ->assertEqual($result->botcha_type, $botcha_point_type, 'Enabled BOTCHA point should have type set');

    // Delete BOTCHA point
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/delete', array(), t('Delete'));
    $this
      ->assertRaw(t('Deleted BOTCHA for form %form_id.', array(
      '%form_id' => $botcha_point_form_id,
    )), 'Deleting of BOTCHA point');

    // Check in database
    $result = $this
      ->getBotchaPointSettingFromDatabase($botcha_point_form_id);
    $this
      ->assertFalse($result, 'Deleted BOTCHA point should be in database');
  }

  /**
   * Method for testing the BOTCHA point administration
   */
  function testBotchaPointAdministrationByNonAdmin() {

    // First add a BOTCHA point (as admin)
    $this
      ->drupalLogin($this->admin_user);
    $botcha_point_form_id = 'form_' . strtolower($this
      ->randomName(32));
    $botcha_point_type = 'default';
    $form_values = array(
      'botcha_point_form_id' => $botcha_point_form_id,
      'botcha_type' => $botcha_point_type,
    );
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/', $form_values, t('Save'));
    $this
      ->assertText(t('Saved BOTCHA point settings.'), 'Saving of BOTCHA point settings');

    // Switch from admin to nonadmin
    $this
      ->drupalGet(url('logout', array(
      'absolute' => TRUE,
    )));
    $this
      ->drupalLogin($this->normal_user);

    // Try to set BOTCHA point through admin/user/botcha/botcha_point
    $this
      ->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point');
    $this
      ->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to set a BOTCHA point');

    // Try to set BOTCHA point through admin/user/botcha/botcha_point/$form_id
    $this
      ->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . 'form_' . strtolower($this
      ->randomName(32)));
    $this
      ->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to set a BOTCHA point');

    // Try to disable the BOTCHA point
    $this
      ->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/disable');
    $this
      ->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to disable a BOTCHA point');

    // Try to delete the BOTCHA point
    $this
      ->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/delete');
    $this
      ->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to delete a BOTCHA point');

    // Switch from nonadmin to admin again
    $this
      ->drupalGet(url('logout', array(
      'absolute' => TRUE,
    )));
    $this
      ->drupalLogin($this->admin_user);

    // Check if original BOTCHA point still exists in database
    $result = $this
      ->getBotchaPointSettingFromDatabase($botcha_point_form_id);
    $this
      ->assertEqual($result->botcha_type, $botcha_point_type, 'Enabled BOTCHA point should still have type set');

    // Delete BOTCHA point
    $this
      ->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/delete', array(), t('Delete'));
    $this
      ->assertRaw(t('Deleted BOTCHA for form %form_id.', array(
      '%form_id' => $botcha_point_form_id,
    )), 'Deleting of BOTCHA point');
  }

}
class BotchaSessionReuseAttackTestCase extends BotchaBaseWebTestCase {
  public static function getInfo() {
    return array(
      'name' => t('BOTCHA session reuse attack tests'),
      'description' => t('Testing of the protection against BOTCHA session reuse attacks.'),
      'group' => t('BOTCHA'),
    );
  }

  /**
   * Assert that the BOTCHA session ID reuse attack was detected.
   */
  protected function assertBotchaSessionIdReuseAttackDetection() {
    $this
      ->assertText(t(BOTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE), 'BOTCHA session ID reuse attack should be detected.', 'BOTCHA');

    //    // There should be an error message about bad post.
    //    $this->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
    //      'BOTCHA should be flagged as wrong.',
    //      'BOTCHA');
  }
  function testBotchaSessionReuseAttackDetectionOnCommentPreview() {
    $langcode = LANGUAGE_NONE;

    // Create commentable node
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Set Test BOTCHA on comment form.
    botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');

    // Log in as normal user.
    $this
      ->drupalLogin($this->normal_user);

    // Go to comment form of commentable node.
    $this
      ->drupalGet('comment/reply/' . $node->nid);
    $this
      ->assertBotchaPresence(TRUE);

    // Get form_build_id.
    $form_build_id = $this
      ->getFormBuildIdFromForm();

    // Post the form with the solution.
    $edit = $this
      ->setCommentFormValues();
    $this
      ->drupalPost(NULL, $edit, t('Preview'));

    // Answer should be accepted and further BOTCHA ommitted.
    $this
      ->assertBotchaResponseAccepted();
    $this
      ->assertBotchaPresence(TRUE);

    // Post a new comment, reusing the previous BOTCHA session.
    $edit = $this
      ->setCommentFormValues();
    $edit['form_build_id'] = $form_build_id;
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));

    // BOTCHA session reuse attack should be detected.
    $this
      ->assertBotchaSessionIdReuseAttackDetection();

    // There should be a BOTCHA.
    $this
      ->assertBotchaPresence(TRUE);

    // Verify that values that user posted are preserved in the new form
    $values = $this
      ->getCommentFormValuesFromForm();
    $this
      ->assertEqual($values['subject'], $edit['subject'], 'Subject should be preserved');
    $this
      ->assertEqual($values["comment_body[{$langcode}][0][value]"], $edit["comment_body[{$langcode}][0][value]"], 'Comment body should be preserved');

    // And verify new form can be re-submitted
    unset($edit['form_build_id']);
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
    $this
      ->assertBotchaResponseAccepted();
    $this
      ->assertBotchaPresence(TRUE);
  }
  function testBotchaSessionReuseAttackDetectionOnNodeForm() {

    // Set BOTCHA on page form.
    botcha_set_form_id_setting('page_node_form', 'test');

    // Log in as normal user.
    $this
      ->drupalLogin($this->normal_user);

    // Go to node add form.
    $this
      ->drupalGet('node/add/page');
    $this
      ->assertBotchaPresence(TRUE);

    // Get form_build_id.
    $form_build_id = $this
      ->getFormBuildIdFromForm();

    // Page settings to post, with correct BOTCHA answer.
    $edit = $this
      ->setNodeFormValues();

    // Preview the node
    $this
      ->drupalPost(NULL, $edit, t('Preview'));

    // Answer should be accepted.
    $this
      ->assertBotchaResponseAccepted();
    $this
      ->assertBotchaPresence(TRUE);

    // Post a new node, reusing the previous BOTCHA session.
    $edit = $this
      ->setNodeFormValues();
    $edit['form_build_id'] = $form_build_id;
    $this
      ->drupalPost('node/add/page', $edit, t('Preview'));

    // BOTCHA session reuse attack should be detected.
    $this
      ->assertBotchaSessionIdReuseAttackDetection();

    // There should be a BOTCHA.
    $this
      ->assertBotchaPresence(TRUE);
  }
  function testBotchaSessionReuseAttackDetectionOnLoginForm() {

    // Set BOTCHA on login form.
    botcha_set_form_id_setting('user_login', 'test');

    // Go to log in form.
    $this
      ->drupalGet('user');
    $this
      ->assertBotchaPresence(TRUE);

    // Get form_build_id.
    $form_build_id = $this
      ->getFormBuildIdFromForm();

    // Log in through form.
    $edit = array(
      'name' => $this->normal_user->name,
      'pass' => $this->normal_user->pass_raw,
    );
    $this
      ->drupalPost(NULL, $edit, t('Log in'));
    $this
      ->assertBotchaResponseAccepted();
    $this
      ->assertBotchaPresence(FALSE);

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

    // Log out again.
    $this
      ->drupalLogout();

    // Try to log in again, reusing the previous BOTCHA session.
    $edit += array(
      'form_build_id' => $form_build_id,
    );
    $this
      ->drupalPost('user', $edit, t('Log in'));

    // BOTCHA session reuse attack should be detected.
    $this
      ->assertBotchaSessionIdReuseAttackDetection();

    // There should be a BOTCHA.
    $this
      ->assertBotchaPresence(TRUE);
  }
  public function testMultipleBotchaProtectedFormsOnOnePage() {

    // Set Test BOTCHA on comment form and login block
    botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
    botcha_set_form_id_setting('user_login_block', 'test');
    $this
      ->allowCommentPostingForAnonymousVisitors();

    // Create a node with comments enabled.
    $node = $this
      ->createNodeWithCommentsEnabled();

    // Preview comment with correct BOTCHA answer.
    $edit = $this
      ->setCommentFormValues();
    $comment_subject = $edit['subject'];
    $this
      ->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));

    // Post should be accepted: no warnings,
    // no BOTCHA reuse detection (which could be used by user log in block).
    $this
      ->assertBotchaResponseAccepted();
    $this
      ->assertText($comment_subject);
  }

}

// Some tricks to debug:
// drupal_debug($data) // from devel module
// file_put_contents('tmp.simpletest.html', $this->drupalGetContent());