You are here

botcha.test in BOTCHA Spam Prevention 7.2

Same filename and directory in other branches
  1. 6 botcha.test
  2. 6.2 botcha.test
  3. 7 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_TIMEGATE_TIMEOUT_ERROR_MESSAGE', 'Form is submitted too fast.');
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';
  function setUp() {

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

    // Clean the environment.
    Botcha::clean();

    /* @todo Remove it.
       $recipebook = Botcha::getRecipebook();
       $recipes = $recipebook->getRecipes();
       foreach ($recipes as $recipe) {
         $recipebook = $recipebook->unsetRecipe($recipe->id);
       }
       $forms = $recipebook->getForms();
       foreach ($forms as $form) {
         $recipebook = $recipebook->unsetForm($form->id);
       }
       $recipebook->save();
        *
        */

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

  /**
   * 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
   */

  // "= NULL" is for backward compatibility.
  protected function setCommentFormValues($should_pass = NULL) {
    $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
   */

  // "= NULL" is for backward compatibility.
  protected function setNodeFormValues($should_pass = NULL) {
    $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 = NULL) {

    // Form a xpath query string.
    $xpath = '//';
    if (!empty($form)) {

      // Specially to transform user_login to user-login.
      // @todo getFormBuildIdFromForm ?Is there a better way to do it?
      $form_filtered = str_replace('_', '-', $form);

      // If form parameter is set we are looking for forms like it. It is useful
      // when we have multiple forms on one page.
      $xpath .= "form[contains(@id, '{$form_filtered}')]/div/";
    }
    $xpath .= "input[@name='form_build_id']/@value";

    // Force conversion to string.

    //$form_build_id = (string) current($this->xpath("//input[@name='form_build_id']/@value"));
    $form_build_id = (string) current($this
      ->xpath($xpath));
    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();
  }

  //-----------------------------------

  /**
   * Get an array of our expectations to cycle through: should we test that this
   * form fails or successfully submitted or both?
   */
  function getExpectations() {
    return array(
      TRUE,
    );
  }

  /**
   * Get the list of names of the buttons that are available for this concrete
   * form. Used as "modes" for behavior testing. Later we check that the real
   * behavior after clicking this button matches our suspections.
   */
  function getButtonsByForm($form) {
    $buttons = array();
    switch ($form) {
      case 'user_login':
        $buttons[] = t('Log in');
        break;
      case 'node':
      case 'comment':
      default:
        $buttons[] = t('Preview');
        $buttons[] = t('Save');
        break;
    }
    return $buttons;
  }

  /**
   * Helper function to generate a default form values array for any form.
   */
  protected function setFormValues($form, $should_pass = TRUE, &$parameters = array()) {
    $edit = array();
    switch ($form) {

      // These ones for testing FormUI.
      case 'addForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $edit['botcha_form_id'] = drupal_strtolower($this
          ->randomName(32));
        $parameters['botcha_form_id'] = $edit['botcha_form_id'];
        $edit["botcha_form_recipebook"] = $parameters['rbid'];
        break;
      case 'editForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // Fill in enabled.
        $edit['botcha_enabled'] = $parameters['botcha_enabled'];

        // Fill in recipe book.
        $edit["botcha_form_recipebook"] = $parameters['rbid'];
        break;
      case 'deleteForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // Nothing to do.
        break;

      // These ones for testing RecipebookUI.
      case 'addRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $edit['id'] = drupal_strtolower($this
          ->randomName(32));
        $edit['title'] = $this
          ->randomName(32);
        $edit['description'] = $this
          ->randomName(255);
        $edit['recipes[timegate]'] = 'timegate';
        break;
      case 'editRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $edit['title'] = $this
          ->randomName(32);
        $edit['description'] = $this
          ->randomName(255);
        $edit['recipes[timegate]'] = 'timegate';
        break;
      case 'deleteRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // @todo BotchaBaseWebTestCase setFormValues Case deleteRecipebook real logic.
        break;

      // And these ones for testing form submission.
      case 'node':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $edit = $this
          ->setNodeFormValues($should_pass);
        break;
      case 'user_login':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $edit = $this
          ->setUserLoginFormValues($should_pass);
        break;
      case 'comment':
      default:
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $edit = $this
          ->setCommentFormValues($should_pass);
        break;
    }
    return $edit;
  }

  /**
   * Helper function to generate a default form values array for comment forms
   */
  protected function setUserLoginFormValues($should_pass) {
    $edit = array(
      'name' => $this->normal_user->name,
      'pass' => $this->normal_user->pass_raw,
    );
    return $edit;
  }
  function checkPreConditions($form, $should_pass, $button) {
    switch ($form) {
      case 'comment':

        // Check that comment form is enabled.
        $this
          ->assertTrue(variable_get("botcha_enabled_comment_node_page_form", 0), "BOTCHA protection for comment_node_page_form must be enabled", 'BOTCHA');
        break;
    }
  }

  /**
   * Get one of predefined forms.
   * Used to unify the process of testing.
   */
  function getForm($form, &$parameters = array()) {
    switch ($form) {

      // These ones for testing FormUI.
      case 'addForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->drupalGet(Botcha::BOTCHA_ADMIN_PATH . '/form/add');
        foreach (array(
          'botcha_form_id',
          'botcha_enabled',
          'botcha_form_recipebook',
        ) as $field) {
          $this
            ->assertField($field, "There should be a {$field} field on the form", 'BOTCHA');
        }
        break;
      case 'editForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $form_id = drupal_strtolower($this
          ->randomName(12));

        // Pass this newly created form to other methods.
        $parameters['botcha_form_id'] = $form_id;

        // Random enabled state generation.
        // Converting to boolean is a workaround for drupalPost which interprets 0 as TRUE else.
        $enabled = (bool) rand(0, 1);
        $parameters['botcha_enabled'] = $enabled;

        // The form should be already binded to some (let's say default) recipe book.
        $rbid = 'default';
        $botcha_form = Botcha::getForm($form_id, TRUE)
          ->setEnabled($enabled)
          ->setRecipebook($rbid)
          ->save();

        // Assert form existence.
        $this
          ->assertTrue(!Botcha::getForm($parameters['botcha_form_id'], FALSE) instanceof BotchaFormNone, "Form {$parameters['botcha_form_id']} exists", 'BOTCHA');

        // Assert recipe book of the form.
        $recipebook = Botcha::getForm($parameters['botcha_form_id'], FALSE)
          ->getRecipebook();
        $this
          ->assertEqual($rbid, $recipebook->id, "BOTCHA form has recipe book {$recipebook->id} (should have {$rbid})", 'BOTCHA');

        // Assert enabled or not.
        $this
          ->assertEqual($enabled, $botcha_form
          ->isEnabled(), 'BOTCHA form has correct state', 'BOTCHA');
        $this
          ->drupalGet(Botcha::BOTCHA_ADMIN_PATH . "/form/{$form_id}");
        foreach (array(
          'botcha_form_id',
          'botcha_enabled',
          'botcha_form_recipebook',
        ) as $field) {
          $this
            ->assertField($field, "There should be a {$field} field on the form", 'BOTCHA');
        }

        // @todo getForm Check that id field is disabled.
        break;
      case 'deleteForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $form_id = drupal_strtolower($this
          ->randomName(12));

        // Pass this newly created form to other methods.
        $parameters['botcha_form_id'] = $form_id;

        // The form should be already binded to some (let's say default) recipe book.
        Botcha::getForm($form_id, TRUE)
          ->setRecipebook('default')
          ->save();
        $this
          ->drupalGet(Botcha::BOTCHA_ADMIN_PATH . "/form/{$form_id}/delete");
        break;

      // These ones for testing RecipebookUI.
      case 'addRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->drupalGet(Botcha::BOTCHA_ADMIN_PATH . '/recipebook/add');

        // @todo BotchaBaseWebTestCase getForm Implement recipes checking.
        foreach (array(
          'id',
          'title',
          'description',
        ) as $field) {
          $this
            ->assertField($field, "There should be a {$field} field on the form", 'BOTCHA');
        }
        break;
      case 'editRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $id = drupal_strtolower($this
          ->randomName(12));

        // Save this id to the parameters.
        $parameters['id'] = $id;
        $title = $this
          ->randomName(12);
        $description = $this
          ->randomString(255);

        // We need some recipes already set to test unsetting.
        $recipe_id = 'honeypot';
        Botcha::getRecipebook($id, TRUE)
          ->setTitle($title)
          ->setDescription($description)
          ->setRecipe($recipe_id)
          ->save();
        $this
          ->drupalGet(Botcha::BOTCHA_ADMIN_PATH . "/recipebook/{$id}");

        // @todo BotchaBaseWebTestCase getForm Implement recipes appearance checking.
        foreach (array(
          'id',
          'title',
          'description',
        ) as $field) {
          $this
            ->assertField($field, "There should be a {$field} field on the form", 'BOTCHA');
        }
        break;
      case 'deleteRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // @todo BotchaBaseWebTestCase getForm Case deleteRecipebook real logic.
        break;

      // And these ones are for testing form submissions.
      case 'node':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->drupalGet('node/add/page');
        $this
          ->assertBotchaPresence(TRUE);
        break;
      case 'user_login':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->drupalGet('user');
        $this
          ->assertBotchaPresence(TRUE);
        break;
      case 'comment':
      default:
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // Create node to post comment to.
        $node = $this
          ->createNodeWithCommentsEnabled();
        $this
          ->drupalGet("comment/reply/{$node->nid}");
        $this
          ->assertBotchaPresence(TRUE);

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

  /**
   * Post one of predefined forms.
   * Used to unify the process of testing.
   */
  function postForm($form, $edit, $button = NULL, &$parameters = array()) {
    switch ($form) {

      // These ones for testing Form UI.
      case 'addForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Add');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
      case 'editForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Save');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
      case 'deleteForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Delete');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;

      // These ones for testing Recipebook UI.
      case 'addRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Add');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
      case 'editRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Save');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
      case 'deleteRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // @todo BotchaBaseWebTestCase postForm Case deleteRecipebook.
        break;

      // And these ones are for testing form submissions.
      case 'node':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Save');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
      case 'user_login':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $button = $button ? $button : t('Log in');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
      case 'comment':
      default:
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // Make sure comments on pages can be saved directly without preview.
        variable_set('comment_preview_page', DRUPAL_OPTIONAL);
        $button = $button ? $button : t('Save');
        $this
          ->drupalPost(NULL, $edit, $button);
        break;
    }
  }

  /**
   * Check whether our suspections are real.
   */
  function assertFormSubmission($form, $edit, $should_pass = TRUE, $button = NULL, &$parameters = array()) {
    switch ($form) {

      // These ones for testing Form UI.
      case 'addForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // Make sure that the message appeared.

        //$this->assertText(t('Added BOTCHA form %form_id.', array('%form_id' => $parameters['botcha_form_id'])), 'BOTCHA form successfully added : Message displayed', 'BOTCHA');
        $this
          ->assertText("Added BOTCHA form {$parameters['botcha_form_id']}.", 'BOTCHA form successfully added : Message displayed', 'BOTCHA');

        // Ensure that the form was created.
        $this
          ->assertTrue(!Botcha::getForm($parameters['botcha_form_id'], FALSE) instanceof BotchaFormNone, 'BOTCHA form successfully added : Form saved to DB', 'BOTCHA');

        // Assert recipe book of the form.
        $recipebook = Botcha::getForm($parameters['botcha_form_id'], FALSE)
          ->getRecipebook();
        $this
          ->assertEqual($parameters['rbid'], $recipebook->id, "BOTCHA form successfully added : Recipe book {$parameters['rbid']} saved correctly as {$recipebook->id}", 'BOTCHA');
        break;
      case 'editForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // Make sure that the message appeared.
        $this
          ->assertText("Saved BOTCHA form settings for {$parameters['botcha_form_id']}.", 'BOTCHA form successfully saved : Message displayed', 'BOTCHA');

        // @todo ?Why we need to call getForm with $create = TRUE? What's wrong
        $botcha_form = Botcha::getForm($parameters['botcha_form_id'], TRUE);

        // Assert recipe book of the form.
        $recipebook = $botcha_form
          ->getRecipebook();

        // Ensure recipe book.
        $this
          ->assertEqual($parameters['rbid'], $recipebook->id, "BOTCHA form successfully saved : Recipe book {$parameters['rbid']} saved correctly as {$recipebook->id}", 'BOTCHA');

        // Ensure enabled.
        $this
          ->assertEqual($parameters['botcha_enabled'], $botcha_form
          ->isEnabled(), 'BOTCHA form was successfully turned on/off', 'BOTCHA');
        break;
      case 'deleteForm':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $form_id = $parameters['botcha_form_id'];

        // Make sure that the message appeared.
        $this
          ->assertText("Deleted BOTCHA protection for form {$form_id}.", 'BOTCHA form successfully deleted : Message displayed', 'BOTCHA');

        // Ensure that the form was deleted.
        $this
          ->assertTrue(Botcha::getForm($form_id, FALSE) instanceof BotchaFormNone, 'BOTCHA form successfully deleted', 'BOTCHA');
        break;

      // These ones for testing Recipebook UI.
      case 'addRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->assertTrue($recipebook = Botcha::getRecipebook($edit['id'], FALSE), "Recipe book {$edit['id']} should exist", 'BOTCHA');
        foreach (array(
          'id',
          'title',
          'description',
        ) as $field) {
          $this
            ->assertEqual($recipebook->{$field}, $edit[$field], "Recipe book {$edit['id']} should have {$field} equal {$edit[$field]} (in fact it has {$recipebook->{$field}})", 'BOTCHA');
        }
        break;
      case 'editRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $recipebook = Botcha::getRecipebook($parameters['id'], FALSE);
        foreach (array(
          'title',
          'description',
        ) as $field) {
          $this
            ->assertEqual($recipebook->{$field}, $edit[$field], "Recipe book {$parameters['id']} should have {$field} equal {$edit[$field]} (in fact it has {$recipebook->{$field}})", 'BOTCHA');
        }

        // @todo Add recipes assertion.
        break;
      case 'deleteRecipebook':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));

        // @todo BotchaBaseWebTestCase assertFormSubmission Case deleteRecipebook.
        break;

      // And these ones are for testing form submissions.
      case 'node':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->assertNodeFormSubmission($edit, $should_pass, $button);
        break;
      case 'user_login':
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->assertUserLoginFormSubmission($edit, $should_pass, $button);
        break;
      case 'comment':
      default:
        $this
          ->debug("Entered %method %case", array(
          '%method' => __METHOD__,
          '%case' => $form,
        ));
        $this
          ->assertCommentFormSubmission($edit, $should_pass, $button);
        break;
    }
  }

  /**
   * 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 assertCommentFormSubmission($edit, $should_pass, $button) {

    // Assertion itself.
    switch ($should_pass) {
      case FALSE:

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

        // Check that there is still BOTCHA after failed submit.
        $this
          ->assertBotchaPresence(TRUE);
        $this
          ->assertNoText($edit['subject'], 'Comment should not show up on node page.', 'BOTCHA');

        // !!? Do we need to check message body?

        //$this->assertNoText($comment_body, $message . ' Comment should not show up on node page.', 'BOTCHA');
        break;
      case TRUE:
      default:
        switch ($button) {
          case t('Preview'):

            // Check that there is still BOTCHA after preview.
            $this
              ->assertBotchaPresence(TRUE);
            break;
          case t('Save'):
          default:

            // There should be no error message.
            $this
              ->assertBotchaResponseAccepted();
            $this
              ->assertText($edit['subject'], 'Comment should show up on node page.', 'BOTCHA');

            // !!? Do we need to check message body?

            //$this->assertText($edit['comment_body'], $message . ' Comment should show up on node page.', 'BOTCHA');
            break;
        }
        break;
    }
  }

  /**
   * Assert submission of node form, check whether it works how it should.
   *
   * @param type $edit
   * @param type $should_pass
   */
  protected function assertNodeFormSubmission($edit, $should_pass, $button) {

    // !!~ @todo assertNodeFormSubmission real logic
  }

  /**
   * Assert submission of user login form, check whether it works how it should.
   *
   * @param type $edit
   * @param type $should_pass
   */
  protected function assertUserLoginFormSubmission($edit, $should_pass, $button) {
    switch ($should_pass) {
      case FALSE:

        // Check for error message.
        $this
          ->assertText(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');
        break;
      case TRUE:
      default:

        // If log in was successful, log out to continue testing.
        $this
          ->drupalLogout();
        break;
    }
  }

  /**
   * Used to print debug message on the test result screen.
   * @param string $message_template
   * @param array $substitutions
   */
  protected function debug($message_template, $substitutions = array()) {
    return $this
      ->pass(t($message_template, $substitutions), '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'),
    );
  }
  public function setUp() {
    parent::setUp(array(
      'system',
    ));

    // Disable all recipes and forms for default recipe book not to have any
    // problems with submitting forms.
    $forms = Botcha::getForms(TRUE);
    foreach ($forms as $botcha_form) {
      $form_id = $botcha_form->id;
      Botcha::getForm($form_id, FALSE)
        ->setEnabled(FALSE);
    }
  }

  /**
   * Testing general BOTCHA admin functionality.
   */
  function testAdminGeneral() {

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

    // Clean the environment.
    Botcha::clean();
    variable_set('botcha_form_passed_counter', $pass_cnt = rand(0, 10000));
    variable_set('botcha_form_blocked_counter', $block_cnt = rand(1, 10000));
    $build_cnt = $pass_cnt + $block_cnt;

    // Assert that the statistics is present.
    $this
      ->drupalGet(Botcha::BOTCHA_ADMIN_PATH);
    $this
      ->assertText("Already {$block_cnt} blocked form submissions.", 'BOTCHA blocked count statistics is present', 'BOTCHA');
    $percent = sprintf("%0.3f", 100 * $block_cnt / $build_cnt);
    $this
      ->assertText("({$percent}% of total {$build_cnt} processed)", 'BOTCHA total count statistics is present', 'BOTCHA');

    // Reset BOTCHA statistics.
    $this
      ->drupalPost(NULL, array(), t('Reset BOTCHA statistics'));

    // Assert that the statistics is reset.
    $this
      ->assertText("BOTCHA statistics have been reset.", "Message displayed", 'BOTCHA');
    $this
      ->assertText("Already 0 blocked form submissions.", 'BOTCHA blocked count statistics is reset', 'BOTCHA');
    $this
      ->assertNoText("({$percent}% of total {$build_cnt} processed)", 'BOTCHA total count statistics is reset', 'BOTCHA');
  }

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

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

    // Clean the environment.
    Botcha::clean();

    // 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(Botcha::BOTCHA_ADMIN_PATH . '/form', $edit, t('Save configuration'));

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

    // Allow BOTCHA to protect it.
    Botcha::getForm($form_id)
      ->setEnabled(TRUE);

    // 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'));
    $this
      ->assertFieldByName('botcha_form_id', $form_id, 'Form id has been automatically filled in');

    // Enable 'default' BOTCHA.
    $edit = array(
      'botcha_form_recipebook' => 'default',
    );
    $this
      ->drupalPost(NULL, $edit, t('Add'));

    // 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.
    $this
      ->assertText("Added BOTCHA form {$form_id}.", 'Message displayed', 'BOTCHA');

    // Check the links appearance.
    $botcha_form = Botcha::getForm($form_id, FALSE);
    $recipebook = $botcha_form
      ->getRecipebook();
    $this
      ->assertLink(t('edit'));
    $this
      ->assertLink(t('disable'));

    // Check if BOTCHA was successfully enabled (through API).
    $this
      ->assertFalse($botcha_form instanceof BotchaFormNone, "Botcha protection for {$form_id} form added via admin link", 'BOTCHA');
    $this
      ->assertEqual($recipebook->id, 'default', "Recipe book is chosen for {$form_id} form via admin link", 'BOTCHA');

    /* @todo Delete it since it is already tested in testFormUI.
       // Edit challenge type through BOTCHA admin links.
       $this->clickLink(t('edit'));
       // 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');
       // Delete challenge through BOTCHA admin links.
       $this->clickLink(t('delete'));
       // 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');
        *
        */
  }

  /**
   * Tests for creating, modifying and deleting botcha forms.
   */
  function testFormUI() {

    // It is necessary to rebuild menu after changing the setting.
    menu_rebuild();

    // Create a recipe book.
    $id = 'test';
    $title = 'FormUI recipe book';
    $description = 'Created for testing FormUI';
    Botcha::getRecipebook($id)
      ->setTitle($title)
      ->setDescription($description)
      ->save();

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

    // Clean the environment.
    Botcha::clean();
    $forms = array(
      'addForm',
      'editForm',
      'deleteForm',
    );

    // Parameters that need to be passed through methods.
    $parameters = array();
    $parameters['rbid'] = $id;
    foreach ($forms as $form) {
      $this
        ->getForm($form, $parameters);
      $edit = $this
        ->setFormValues($form, NULL, $parameters);
      $this
        ->postForm($form, $edit, NULL, $parameters);
      $this
        ->assertFormSubmission($form, $edit, NULL, NULL, $parameters);
    }
    $this
      ->drupalLogout();
  }

  /**
   * Tests for creating, modifying and deleting recipe books.
   */
  function testRecipebookUI() {

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

    // Clean the environment.
    Botcha::clean();
    $forms = array(
      'addRecipebook',
      'editRecipebook',
      'deleteRecipebook',
    );

    // Parameters that need to be passed through methods.
    $parameters = array();
    foreach ($forms as $form) {
      $this
        ->getForm($form, $parameters);
      $edit = $this
        ->setFormValues($form, NULL, $parameters);
      $this
        ->postForm($form, $edit, NULL, $parameters);
      $this
        ->assertFormSubmission($form, $edit, NULL, NULL, $parameters);
    }
    $this
      ->drupalLogout();
  }

  /**
   * Testing of placing messages into log.
   */
  public function testAdminLog() {

    // Assert filtering vulnerable data: password. Use case is as follows:
    // 1) Enable BOTCHA protection for user registration form.
    $form_controller = $this->application
      ->getController(Botcha::CONTROLLER_TYPE_FORM);
    $form_id = 'user_register_form';
    $botcha_form = $form_controller
      ->getForm($form_id);
    $botcha_form
      ->setEnabled(TRUE);

    // Set log level to one of the highest.
    variable_set('botcha_loglevel', 6);

    // Disable email verification to allow setting password during registration.
    variable_set('user_email_verification', 0);

    // 2) Register new user via that form.
    $this
      ->drupalLogout();
    $edit = array(
      'name' => $username = $this
        ->randomName(),
      'mail' => $mail = $username . '@example.com',
      'pass[pass1]' => $password = user_password(),
      'pass[pass2]' => $password,
    );
    $this
      ->drupalPost('user/register', $edit, t('Create new account'));

    // 3) Check that password is encrypted.
    $this
      ->drupalLogin($this->admin_user);
    $this
      ->drupalGet('admin/reports/dblog');

    // Always blocked, because Simpletest can't run JavaScript.
    $this
      ->clickLink("{$form_id} post blocked by BOTCHA: submission...");
    $pass_fields = array(
      // Assert password is hidden in POST.
      'pass1',
      'pass2',
      // Assert password is hidden in values.
      'pass',
      // Assert password is hidden in form.
      '#value',
    );
    foreach ($pass_fields as $pass_field) {

      // Filtering is needed because matching is handled on filtered text.
      $this
        ->assertText(filter_xss("[{$pass_field}] => ******", array()), t("Password {$pass_field} is hidden"));
      $this
        ->assertNoText(filter_xss("[{$pass_field}] => {$password}", array()), t("There is no raw {$pass_field} password"));
    }
  }

}

/**
 * Testing general BOTCHA functionality.
 */
class BotchaTestCase extends BotchaBaseWebTestCase {
  function setUp() {
    parent::setUp();

    // Allow comment posting for guests.
    $this
      ->allowCommentPostingForAnonymousVisitors();

    // For some reason we don't find this form after installation - but we should.
    // So fix it manually.
    Botcha::getForm('comment_node_page_form', TRUE)
      ->setEnabled(TRUE)
      ->save();

    // Create recipebook "test" + bind all forms to it.
    $recipebook = Botcha::getRecipebook('test', TRUE)
      ->setTitle('Test recipebook')
      ->setDescription("Created for {$this->testId}");
    foreach (Botcha::getForms() as $form) {
      $recipebook = $recipebook
        ->setForm($form->id);
    }
    $recipebook
      ->save();
  }
  function testFormSubmission() {
    $forms = array(
      'comment',
    );
    foreach ($forms as $form) {

      // Determine what we expect: whether successful submission or blocked or both.
      foreach ($this
        ->getExpectations() as $should_pass) {
        foreach ($this
          ->getButtonsByForm($form) as $button) {

          // Check some assumptions that are  necessary for this test.
          $this
            ->checkPreConditions($form, $should_pass, $button);

          // Get a form.
          $this
            ->getForm($form);

          // Fill in the form.
          $edit = $this
            ->setFormValues($form, $should_pass);

          // Submit the form.
          $this
            ->postForm($form, $edit, $button);

          // Make sure that we get what expected.
          $this
            ->assertFormSubmission($form, $edit, $should_pass, $button);
        }
      }
    }
  }

}
class BotchaUsingJsTestCase extends BotchaTestCase {
  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');
    }
  }

}
class BotchaHoneypotTestCase extends BotchaUsingJsTestCase {
  public static function getInfo() {
    return array(
      'name' => t('Honeypot method testing'),
      'description' => t('Testing of the honeypot protection method.'),
      'group' => t('BOTCHA'),
    );
  }
  function setUp() {
    parent::setUp();

    // Bind only one recipe to test recipe book.
    Botcha::getRecipebook('test')
      ->setRecipe('honeypot')
      ->save();
  }
  function getExpectations() {

    // !!~ @todo BotchaHoneypotTestCase How to test JavaScript?
    return array(
      FALSE,
    );
  }
  protected function setFormValues($form, $should_pass) {
    $edit = parent::setFormValues($form, $should_pass);

    // Since we can't test javascript (is there a way to do it?) and Simpletest
    // can't evaluate javascript we should do nothing to break the submission.
    return $edit;
  }

}
class BotchaHoneypot2TestCase extends BotchaUsingJsTestCase {

  // @todo BotchaRecipeHoneypot2 Refactor as a configuration of BotchaRecipeHoneypot.
  public static function getInfo() {
    return array(
      'name' => t('Honeypot2 method testing'),
      'description' => t('Testing of the honeypot2 protection method.'),
      'group' => t('BOTCHA'),
    );
  }
  function setUp() {
    parent::setUp();

    // Bind only one recipe to test recipe book.
    Botcha::getRecipebook('test')
      ->setRecipe('honeypot2')
      ->save();
  }
  function getExpectations() {

    // !!~ @todo BotchaHoneypot2TestCase How to test JavaScript?
    return array(
      FALSE,
    );
  }
  protected function setFormValues($form, $should_pass) {
    $edit = parent::setFormValues($form, $should_pass);

    // Since we can't test javascript (is there a way to do it?) and Simpletest
    // can't evaluate javascript we should do nothing to break the submission.
    return $edit;
  }

}
class BotchaObscureUrlTestCase extends BotchaUsingJsTestCase {
  public static function getInfo() {
    return array(
      'name' => t('Obscure url method testing'),
      'description' => t('Testing of the obscure url protection method.'),
      'group' => t('BOTCHA'),
    );
  }
  function setUp() {
    parent::setUp();

    // Bind only one recipe to test recipe book.
    Botcha::getRecipebook('test')
      ->setRecipe('obscure_url')
      ->save();
  }
  function getExpectations() {

    // !!~ @todo BotchaObscureUrlTestCase How to test JavaScript?
    return array(
      FALSE,
    );
  }
  protected function setFormValues($form, $should_pass) {
    $edit = parent::setFormValues($form, $should_pass);

    // Since we can't test javascript (is there a way to do it?) and Simpletest
    // can't evaluate javascript we should do nothing to break the submission.
    return $edit;
  }

}
class BotchaNoResubmitTestCase extends BotchaTestCase {
  public static function getInfo() {
    return array(
      'name' => t('NoResubmit method testing'),
      'description' => t('Testing of the session reuse attack protection.'),
      'group' => t('BOTCHA'),
    );
  }
  function setUp() {
    parent::setUp();

    // Bind only one recipe to test recipe book.
    Botcha::getRecipebook('test')
      ->setRecipe('no_resubmit')
      ->save();
  }
  function getExpectations() {
    return array(
      TRUE,
      FALSE,
    );
  }
  protected function assertBotchaPresence($presence) {

    // We couldn't understand whether this form implement no-resubmit protection
    // or not by its appearance - so always pass this check.
    return TRUE;
  }
  protected function setFormValues($form, $should_pass) {
    $edit = parent::setFormValues($form, $should_pass);
    switch ($should_pass) {
      case FALSE:

        // Get form_build_id of the form (to simulate resubmit).
        $form_build_id = $this
          ->getFormBuildIdFromForm($form);

        // Submit a form once.
        $this
          ->postForm($form, $edit);

        // Make sure it passes.
        parent::assertFormSubmission($form, $edit, TRUE);

        // Get new form.
        $this
          ->getForm($form);

        //$edit = parent::setFormValues($form, $should_pass);

        // Set form_build_id of the current form to stored value (to simulate resubmit).
        $edit['form_build_id'] = $form_build_id;
        break;
      case TRUE:
      default:

        // Everything is done already.
        break;
    }
    return $edit;
  }
  function assertFormSubmission($form, $edit, $should_pass = TRUE, $button = NULL) {
    switch ($form) {
      case 'user_login':
      case 'comment':

        // Assertion itself.
        switch ($should_pass) {
          case FALSE:

            // Check for error message.
            $this
              ->assertText(BOTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE, 'Submission should be blocked.', 'BOTCHA');
            break;
        }
        break;
    }

    // We are placing it to the end since we have a redirection there which
    // makes impossible to assert submission by these conditions.
    parent::assertFormSubmission($form, $edit, $should_pass, $button);
  }

}
class BotchaTimegateTestCase extends BotchaTestCase {
  public static function getInfo() {
    return array(
      'name' => t('Timegate method testing'),
      'description' => t('Testing of the timegate protection method.'),
      'group' => t('BOTCHA'),
    );
  }
  function setUp() {
    parent::setUp();

    // Bind only one recipe to test recipe book.
    Botcha::getRecipebook('test')
      ->setRecipe('timegate')
      ->save();
  }
  function getExpectations() {
    return array(
      TRUE,
      FALSE,
    );
  }

  /**
   * Recipe should modify default form to break itself.
   *
   * @param type $form
   * @param type $should_pass
   * @return type
   */
  protected function setFormValues($form, $should_pass) {
    $edit = parent::setFormValues($form, $should_pass);
    switch ($should_pass) {
      case FALSE:
        unset($edit['timegate']);
        break;
      case TRUE:
      default:
        $edit['timegate'] = (int) time() - (int) variable_get('botcha_timegate', 5) - 1;
        break;
    }
    return $edit;
  }
  protected function assertBotchaPresence($presence) {
    if ($presence) {
      $this
        ->assertField('timegate', 'There should be a BOTCHA timegate field on the form.', 'BOTCHA');
    }
    else {
      $this
        ->assertNoField('timegate', 'There should be no BOTCHA timegate field on the form.', 'BOTCHA');
    }
  }
  function assertFormSubmission($form, $edit, $should_pass = TRUE, $button = NULL) {
    switch ($form) {
      case 'user_login':
      case 'comment':

        // Assertion itself.
        switch ($should_pass) {
          case FALSE:

            // Check for error message.
            $this
              ->assertText(BOTCHA_TIMEGATE_TIMEOUT_ERROR_MESSAGE, 'Submission should be blocked.', 'BOTCHA');
            break;
        }
        break;
    }

    // We are placing it to the end since we have a redirection there which
    // makes impossible to assert submission by these conditions.
    parent::assertFormSubmission($form, $edit, $should_pass, $button);
  }

}

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