botcha.test in BOTCHA Spam Prevention 7.2
Same filename and directory in other branches
Tests for BOTCHA module.
File
botcha.testView 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());
Constants
Classes
Name | Description |
---|---|
BotchaAdminTestCase | |
BotchaBaseWebTestCase | Base class for BOTCHA tests. |
BotchaHoneypot2TestCase | |
BotchaHoneypotTestCase | |
BotchaNoResubmitTestCase | |
BotchaObscureUrlTestCase | |
BotchaTestCase | Testing general BOTCHA functionality. |
BotchaTimegateTestCase | |
BotchaUsingJsTestCase |