botcha.test in BOTCHA Spam Prevention 7
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_UNKNOWN_CSID_ERROR_MESSAGE', 'You must be a human, not a spam bot, to submit forms on this website.');
define('BOTCHA_NO_JS_ERROR_MESSAGE', 'Please enable Javascript to use this form.');
/**
* Base class for BOTCHA tests.
*
* Provides common setup stuff and various helper functions
*/
abstract class BotchaBaseWebTestCase extends DrupalWebTestCase {
/**
* User with various administrative permissions.
* @var Drupal user
*/
protected $admin_user;
/**
* Normal visitor with limited permissions
* @var Drupal user;
*/
protected $normal_user;
/**
* Form ID of comment form on standard (page) node
* @var string
*/
const COMMENT_FORM_ID = 'comment_node_page_form';
/**
* Drupal path of the (general) BOTCHA admin page
*/
const BOTCHA_ADMIN_PATH = 'admin/config/people/botcha';
function setUp() {
// Load two modules: the botcha module itself and the comment module for testing anonymous comments.
parent::setUp('botcha', 'comment');
module_load_include('inc', 'botcha');
// Create a normal user.
$permissions = array(
'access comments',
'post comments',
'skip comment approval',
'access content',
'create page content',
'edit own page content',
);
$this->normal_user = $this
->drupalCreateUser($permissions);
// Create an admin user.
$permissions[] = 'administer BOTCHA settings';
$permissions[] = 'skip BOTCHA';
$permissions[] = 'administer permissions';
$permissions[] = 'administer content types';
$this->admin_user = $this
->drupalCreateUser($permissions);
// Put comments on page nodes on a separate page (default in D7: below post).
variable_set('comment_form_location_page', COMMENT_FORM_SEPARATE_PAGE);
// Tweak BOTCHA configuration
db_update('botcha_points')
->fields(array(
'botcha_type' => 'none',
))
->condition('form_id', 'user_login')
->execute();
db_update('botcha_points')
->fields(array(
'botcha_type' => 'none',
))
->condition('form_id', 'user_login_block')
->execute();
}
/**
* Assert that the response is accepted:
* no "unknown CSID" message, no "CSID reuse attack detection" message,
* no "wrong answer" message.
*/
protected function assertBotchaResponseAccepted() {
// There should be no error message about unknown BOTCHA session ID.
$this
->assertNoText(t(BOTCHA_UNKNOWN_CSID_ERROR_MESSAGE), 'BOTCHA response should be accepted (known CSID).', 'BOTCHA');
// There should be no error message about CSID reuse attack.
$this
->assertNoText(t(BOTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE), 'BOTCHA response should be accepted (no BOTCHA session reuse attack detection).', 'BOTCHA');
// There should be no error message about wrong response.
$this
->assertNoText(t(BOTCHA_NO_JS_ERROR_MESSAGE), 'BOTCHA response should be accepted (JS enabled).', 'BOTCHA');
}
/**
* Assert that there is a BOTCHA on the form or not.
* @param bool $presence whether there should be a BOTCHA or not.
*/
protected function assertBotchaPresence($presence) {
if ($presence) {
$this
->assertText('If you\'re a human, don\'t change the following field', 'There should be a BOTCHA on the form.', 'BOTCHA');
}
else {
$this
->assertNoText('If you\'re a human, don\'t change the following field', 'There should be no BOTCHA on the form.', 'BOTCHA');
}
}
/**
* Helper function to create a node with comments enabled.
*
* @return
* Created node object.
*/
protected function createNodeWithCommentsEnabled($type = 'page') {
$node_settings = array(
'type' => $type,
'comment' => COMMENT_NODE_OPEN,
);
$node = $this
->drupalCreateNode($node_settings);
return $node;
}
/**
* Helper function to get form values array from comment form
*/
protected function getCommentFormValuesFromForm() {
// Submit the form using the displayed values.
$langcode = LANGUAGE_NONE;
$displayed = array();
foreach (array(
'subject' => "//input[@id='edit-subject']/@value",
"comment_body[{$langcode}][0][value]" => "//textarea[@id='edit-comment-body-{$langcode}-0-value']",
'botcha_response' => "//input[@id='edit-botcha-response']/@value",
) as $field => $path) {
$value = current($this
->xpath($path));
if (!empty($value)) {
$displayed[$field] = (string) $value;
}
}
return $displayed;
}
/**
* Helper function to generate a default form values array for comment forms
*/
protected function setCommentFormValues() {
$langcode = LANGUAGE_NONE;
$edit = array(
'subject' => 'comment_subject ' . $this
->randomName(32),
"comment_body[{$langcode}][0][value]" => 'comment_body ' . $this
->randomName(256),
);
return $edit;
}
/**
* Helper function to generate a default form values array for node forms
*/
protected function setNodeFormValues() {
$langcode = LANGUAGE_NONE;
$edit = array(
'title' => 'node_title ' . $this
->randomName(32),
"body[{$langcode}][0][value]" => 'node_body ' . $this
->randomName(256),
);
return $edit;
}
/**
* Get the form_build_id from the current form in the browser.
*/
protected function getFormBuildIdFromForm() {
$form_build_id = current($this
->xpath('//input[@name="form_build_id"]/@value'));
return $form_build_id;
}
/**
* Helper function to allow comment posting for anonymous users.
*/
protected function allowCommentPostingForAnonymousVisitors() {
// Log in as admin.
$this
->drupalLogin($this->admin_user);
// Post user permissions form
$edit = array(
'1[access comments]' => true,
'1[post comments]' => true,
'1[skip comment approval]' => true,
);
$this
->drupalPost('admin/people/permissions', $edit, 'Save permissions');
$this
->assertText('The changes have been saved.');
// Log admin out
$this
->drupalLogout();
}
}
class BotchaTestCase extends BotchaBaseWebTestCase {
public static function getInfo() {
return array(
'name' => t('General BOTCHA functionality'),
'description' => t('Testing of the basic BOTCHA functionality.'),
'group' => t('BOTCHA'),
);
}
/**
* Testing the protection of the user log in form.
*/
function testBotchaOnLoginForm() {
// Create user and test log in without BOTCHA.
$user = $this
->drupalCreateUser();
$this
->drupalLogin($user);
// Log out again.
$this
->drupalLogout();
// Set a BOTCHA on login form
botcha_set_form_id_setting('user_login', 'default');
// Check if there is a BOTCHA on the login form (look for the title).
$this
->drupalGet('user');
$this
->assertBotchaPresence(TRUE);
// Try to log in, which should fail (due to JS required for 'default' BOTCHA recipe is not present in simpletest browser).
$edit = array(
'name' => $user->name,
'pass' => $user->pass_raw,
);
$this
->drupalPost('user', $edit, t('Log in'));
// Check for error message.
$this
->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), 'BOTCHA should block user login form', 'BOTCHA');
// And make sure that user is not logged in: check for name and password fields on ?q=user
$this
->drupalGet('user');
$this
->assertField('name', t('Username field found.'), 'BOTCHA');
$this
->assertField('pass', t('Password field found.'), 'BOTCHA');
}
/**
* Assert function for testing if comment posting works as it should.
*
* Creates node with comment writing enabled, tries to post comment
* with given BOTCHA response (caller should enable the desired
* challenge on page node comment forms) and checks if the result is as expected.
*
* @param $node existing node to post comments to (if NULL, will be created)
* @param $should_pass boolean describing if the posting should pass or should be blocked
* @param $message message to prefix to nested asserts
* @param $button name of button to click (t('Save') by default)
*/
protected function assertCommentPosting($node, $should_pass, $message, $button = '') {
$langcode = LANGUAGE_NONE;
// Make sure comments on pages can be saved directely without preview.
variable_set('comment_preview_page', DRUPAL_OPTIONAL);
if (empty($node)) {
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
}
if (empty($button)) {
$button = t('Save');
}
// Check if there is a BOTCHA on the comment form.
$this
->drupalGet('comment/reply/' . $node->nid);
$this
->assertBotchaPresence(TRUE);
// Post comment on node.
$edit = $this
->setCommentFormValues();
if (!$should_pass) {
// Screw up fields (like a bot would do)
$edit['botcha_response'] = 'xx';
}
$comment_subject = $edit['subject'];
$comment_body = $edit["comment_body[{$langcode}][0][value]"];
$this
->drupalPost('comment/reply/' . $node->nid, $edit, $button);
if ($should_pass) {
// There should be no error message.
$this
->assertBotchaResponseAccepted();
// Get node page and check that comment shows up.
$this
->drupalGet('node/' . $node->nid);
$this
->assertText($comment_subject, $message . ' Comment should show up on node page.', 'BOTCHA');
$this
->assertText($comment_body, $message . ' Comment should show up on node page.', 'BOTCHA');
}
else {
// Check for error message.
$this
->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), $message . ' Comment submission should be blocked.', 'BOTCHA');
// Check that there is still BOTCHA after failed submit.
$this
->assertBotchaPresence(TRUE);
// Get node page and check that comment is not present.
$this
->drupalGet('node/' . $node->nid);
$this
->assertNoText($comment_subject, $message . ' Comment should not show up on node page.', 'BOTCHA');
$this
->assertNoText($comment_body, $message . ' Comment should not show up on node page.', 'BOTCHA');
}
return $node;
}
/**
* Testing the case sensistive/insensitive validation.
*/
function testBotchaValidation() {
// Set Test BOTCHA on comment form
botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
// Log in as normal user.
$this
->drupalLogin($this->normal_user);
$this
->assertCommentPosting(NULL, TRUE, 'Validation of right fields touched.');
$this
->assertCommentPosting(NULL, FALSE, 'Validation of wrong fields touched.');
}
/**
* Test if BOTCHA is applied when previewing comments:
* comment preview should have BOTCHA again.
*
* \see testBotchaAfterNodePreview()
*/
function testBotchaAfterCommentPreview() {
// Set Test BOTCHA on comment form.
botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
// Log in as normal user.
$this
->drupalLogin($this->normal_user);
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
// Check if there is a BOTCHA on the comment form (look for the title).
$this
->drupalGet('comment/reply/' . $node->nid);
$this
->assertBotchaPresence(TRUE);
// Preview comment with correct BOTCHA.
$edit = $this
->setCommentFormValues();
$this
->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
// Check that there is still BOTCHA after preview.
$this
->assertBotchaPresence(TRUE);
}
/**
* Test if BOTCHA is applied when previewing nodes:
* node preview should have BOTCHA again.
* The preview functionality of comments and nodes works slightly different under the hood.
* BOTCHA module should be able to handle both.
*
* \see testBotchaAfterCommentPreview()
*/
function testBotchaAfterNodePreview() {
// Set Test BOTCHA on page form.
botcha_set_form_id_setting('page_node_form', 'test');
// Log in as normal user.
$this
->drupalLogin($this->normal_user);
// Check if there is a BOTCHA on the node form (look for the title).
$this
->drupalGet('node/add/page');
$this
->assertBotchaPresence(TRUE);
// Page settings to post, with correct BOTCHA answer.
$edit = $this
->setNodeFormValues();
// Preview the node
$this
->drupalPost('node/add/page', $edit, t('Preview'));
// Check that there is still BOTCHA after preview.
$this
->assertBotchaPresence(TRUE);
}
/**
* BOTCHA should also be put on admin pages if visitor
* has no access
*/
function testBotchaOnLoginBlockOnAdminPages() {
// Set a BOTCHA on login block form
botcha_set_form_id_setting('user_login_block', 'default');
// Check if there is a BOTCHA on home page.
$this
->drupalGet('node');
$this
->assertBotchaPresence(TRUE);
// Check there is a BOTCHA on "forbidden" admin pages
$this
->drupalGet('admin');
$this
->assertBotchaPresence(TRUE);
}
/**
* Test if BOTCHA is applied correctly when failing first and then resubmitting comments:
* comment form should have BOTCHA again and pass correct submission.
* (We use to fail BOTCHA as it is impossible to fail comment form on its own)
*
* \see testBotchaAfterNodePreview()
*/
function testBotchaResubmit() {
$langcode = LANGUAGE_NONE;
// Set Test BOTCHA on comment form.
botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
// Log in as normal user.
$this
->drupalLogin($this->normal_user);
// Make sure comments on pages can be saved directely without preview.
variable_set('comment_preview_page', DRUPAL_OPTIONAL);
// Check if there is a BOTCHA on the comment form.
$this
->drupalGet('comment/reply/' . $node->nid);
$this
->assertBotchaPresence(TRUE);
// Post comment on node.
$edit = $this
->setCommentFormValues();
// Screw up fields (like a bot would do)
$edit['botcha_response'] = 'xx';
$comment_subject = $edit['subject'];
$comment_body = $edit["comment_body[{$langcode}][0][value]"];
$this
->drupalPost('comment/reply/' . $node->nid, $edit, t('Save'));
// Check for error message.
$this
->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), 'Comment submission should be blocked.', 'BOTCHA');
// Check that there is still BOTCHA after failed submit.
$this
->assertBotchaPresence(TRUE);
// Copy all values from the form into new one.
$edit = $this
->getCommentFormValuesFromForm();
// Get node page and check that comment is not present.
$this
->drupalGet('node/' . $node->nid);
$this
->assertNoText($comment_subject, 'Comment should not show up on node page.', 'BOTCHA');
$this
->assertNoText($comment_body, 'Comment should not show up on node page.', 'BOTCHA');
$comment_subject = $edit['subject'];
$comment_body = $edit["comment_body[{$langcode}][0][value]"];
// Save comment again with correct BOTCHA.
$this
->drupalPost('comment/reply/' . $node->nid, $edit, t('Save'));
// There should be no error message.
$this
->assertBotchaResponseAccepted();
// Get node page and check that comment shows up.
$this
->drupalGet('node/' . $node->nid);
$this
->assertText($comment_subject, ' Comment should show up on node page.', 'BOTCHA');
$this
->assertText($comment_body, ' Comment should show up on node page.', 'BOTCHA');
}
}
class BotchaAdminTestCase extends BotchaBaseWebTestCase {
public static function getInfo() {
return array(
'name' => t('BOTCHA administration functionality'),
'description' => t('Testing of the BOTCHA administration interface and functionality.'),
'group' => t('BOTCHA'),
);
}
/**
* Test access to the admin pages.
*/
function testAdminAccess() {
$this
->drupalLogin($this->normal_user);
$this
->drupalGet(self::BOTCHA_ADMIN_PATH);
file_put_contents('tmp.simpletest.html', $this
->drupalGetContent());
$this
->assertText(t('Access denied'), 'Normal users should not be able to access the BOTCHA admin pages', 'BOTCHA');
$this
->drupalLogin($this->admin_user);
$this
->drupalGet(self::BOTCHA_ADMIN_PATH);
$this
->assertNoText(t('Access denied'), 'Admin users should be able to access the BOTCHA admin pages', 'BOTCHA');
}
/**
* Test the BOTCHA point setting getter/setter.
*/
function testBotchaPointSettingGetterAndSetter() {
$comment_form_id = self::COMMENT_FORM_ID;
// Set to 'none'.
botcha_set_form_id_setting($comment_form_id, 'none');
$result = botcha_get_form_id_setting($comment_form_id);
$this
->assertNotNull($result, 'Setting and getting BOTCHA point: none', 'BOTCHA');
$this
->assertNull($result->botcha_type, 'Setting and getting BOTCHA point: none', 'BOTCHA');
$result = botcha_get_form_id_setting($comment_form_id, TRUE);
$this
->assertEqual($result, 'none', 'Setting and symbolic getting BOTCHA point: "none"', 'BOTCHA');
// Set to 'default'
botcha_set_form_id_setting($comment_form_id, 'default');
$result = botcha_get_form_id_setting($comment_form_id);
$this
->assertNotNull($result, 'Setting and getting BOTCHA point: default', 'BOTCHA');
$this
->assertEqual($result->botcha_type, 'default', 'Setting and getting BOTCHA point: default', 'BOTCHA');
$result = botcha_get_form_id_setting($comment_form_id, TRUE);
$this
->assertEqual($result, 'default', 'Setting and symbolic getting BOTCHA point: "default"', 'BOTCHA');
// Set to 'boo'.
botcha_set_form_id_setting($comment_form_id, 'boo');
$result = botcha_get_form_id_setting($comment_form_id);
$this
->assertNotNull($result, 'Setting and getting BOTCHA point: boo', 'BOTCHA');
$this
->assertEqual($result->botcha_type, 'boo', 'Setting and getting BOTCHA point: boo', 'BOTCHA');
$result = botcha_get_form_id_setting($comment_form_id, TRUE);
$this
->assertEqual($result, 'boo', 'Setting and symbolic getting BOTCHA point: "boo"', 'BOTCHA');
// Set to NULL (which should delete the BOTCHA point setting entry).
botcha_set_form_id_setting($comment_form_id, NULL);
$result = botcha_get_form_id_setting($comment_form_id);
$this
->assertNull($result, 'Setting and getting BOTCHA point: NULL', 'BOTCHA');
$result = botcha_get_form_id_setting($comment_form_id, TRUE);
$this
->assertNull($result, 'Setting and symbolic getting BOTCHA point: NULL', 'BOTCHA');
// // Set with object.
// $botcha_type = new stdClass;
// $botcha_type->botcha_type = 'fofo';
// botcha_set_form_id_setting($comment_form_id, $botcha_type);
// $result = botcha_get_form_id_setting($comment_form_id);
// $this->assertNotNull($result, 'Setting and getting BOTCHA point: fofo', 'BOTCHA');
// $this->assertEqual($result->botcha_type, 'fofo', 'Setting and getting BOTCHA point: fofo', 'BOTCHA');
// $result = botcha_get_form_id_setting($comment_form_id, TRUE);
// $this->assertEqual($result, 'fofo', 'Setting and symbolic getting BOTCHA point: "fofo"', 'BOTCHA');
}
/**
* Helper function for checking BOTCHA setting of a form.
*
* @param $form_id the form_id of the form to investigate.
* @param $challenge_type what the challenge type should be:
* NULL, 'none', 'default' or something like 'default'
*/
protected function assertBotchaSetting($form_id, $challenge_type) {
$result = botcha_get_form_id_setting(self::COMMENT_FORM_ID, TRUE);
$this
->assertEqual($result, $challenge_type, t('Check BOTCHA setting for form: expected: @expected, received: @received.', array(
'@expected' => var_export($challenge_type, TRUE),
'@received' => var_export($result, TRUE),
)), 'BOTCHA');
}
/**
* Testing of the BOTCHA administration links.
*/
function testBotchaAdminLinks() {
// Log in as admin
$this
->drupalLogin($this->admin_user);
// Test for [#1861016]
$this
->drupalGet('user/' . $this->admin_user->uid . '/edit');
$this
->assertText($this->admin_user->name, t('User name found.'), 'BOTCHA');
// Enable BOTCHA administration links.
$edit = array(
'botcha_administration_mode' => TRUE,
);
$this
->drupalPost(self::BOTCHA_ADMIN_PATH, $edit, 'Save configuration');
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
// Go to node page
$this
->drupalGet('node/' . $node->nid);
// Click the add new comment link
$this
->clickLink(t('Add new comment'));
$add_comment_url = $this
->getUrl();
// Remove fragment part from comment URL to avoid problems with later asserts
$add_comment_url = strtok($add_comment_url, "#");
////////////////////////////////////////////////////////////
// Click the BOTCHA admin link to enable a challenge.
$this
->clickLink(t('Add BOTCHA protection on form'));
// Enable 'default' BOTCHA.
$edit = array(
'botcha_type' => 'default',
);
$this
->drupalPost($this
->getUrl(), $edit, t('Save'));
// Check if returned to original comment form.
$this
->assertUrl($add_comment_url, array(), 'After setting BOTCHA with BOTCHA admin links: should return to original form.', 'BOTCHA');
// Check if BOTCHA was successfully enabled (on BOTCHA admin links fieldset).
$this
->assertText(t('Saved BOTCHA point settings.', array(
'@type' => 'default',
)), 'Enable a challenge through the BOTCHA admin links', 'BOTCHA');
// Check if BOTCHA was successfully enabled (through API).
$this
->assertBotchaSetting(self::COMMENT_FORM_ID, 'default');
//////////////////////////////////////////////////////
// Edit challenge type through BOTCHA admin links.
$this
->clickLink(t('change'));
// Enable 'default' BOTCHA.
$edit = array(
'botcha_type' => 'default',
);
$this
->drupalPost($this
->getUrl(), $edit, t('Save'));
// Check if returned to original comment form.
$this
->assertEqual($add_comment_url, $this
->getUrl(), 'After editing challenge type BOTCHA admin links: should return to original form.', 'BOTCHA');
// Check if BOTCHA was successfully changed (on BOTCHA admin links fieldset).
// This is actually the same as the previous setting because the botcha/Math is the
// default for the default challenge. TODO Make sure the edit is a real change.
$this
->assertText(t('Saved BOTCHA point settings.', array(
'@type' => 'default',
)), 'Enable a challenge through the BOTCHA admin links', 'BOTCHA');
// Check if BOTCHA was successfully edited (through API).
$this
->assertBotchaSetting(self::COMMENT_FORM_ID, 'default');
//////////////////////////////////////////////////////
// Disable challenge through BOTCHA admin links.
$this
->clickLink(t('disable'));
// And confirm.
$this
->drupalPost($this
->getUrl(), array(), 'Disable');
// Check if returned to original comment form.
$this
->assertEqual($add_comment_url, $this
->getUrl(), 'After disablin challenge with BOTCHA admin links: should return to original form.', 'BOTCHA');
// Check if BOTCHA was successfully disabled (on BOTCHA admin links fieldset).
$this
->assertText(t('Disabled BOTCHA for form'), 'Disable challenge through the BOTCHA admin links', 'BOTCHA');
// Check if BOTCHA was successfully disabled (through API).
$this
->assertBotchaSetting(self::COMMENT_FORM_ID, 'none');
}
function testUntrustedUserPosting() {
// Set BOTCHA on comment form.
botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
// Log in as normal (untrusted) user.
$this
->drupalLogin($this->normal_user);
// Go to node page and click the "add comment" link.
$this
->drupalGet('node/' . $node->nid);
$this
->clickLink(t('Add new comment'));
$add_comment_url = $this
->getUrl();
// Check if BOTCHA is visible on form.
$this
->assertBotchaPresence(TRUE);
// Try to post a comment with wrong answer.
$edit = $this
->setCommentFormValues();
// Screw up fields (like a bot would do)
$edit['botcha_response'] = 'xx';
$this
->drupalPost($add_comment_url, $edit, t('Preview'));
$this
->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE), 'wrong BOTCHA should block form submission.', 'BOTCHA');
//TODO: more testing for untrusted posts.
}
/**
* Test the BOTCHA placement flushing.
*/
function testBotchaPlacementCacheFlushing() {
$comment_form_id = self::COMMENT_FORM_ID;
botcha_set_form_id_setting($comment_form_id, 'default');
variable_set('botcha_administration_mode', TRUE);
// $placement_map = variable_get('botcha_placement_map_cache', NULL);
// $this->assertNull($placement_map, 'BOTCHA placement cache should be unset.');
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
// Visit comment form of commentable node to fill the BOTCHA placement cache.
$this
->drupalLogin($this->admin_user);
$this
->drupalGet('comment/reply/' . $node->nid);
// Check if there is BOTCHA placement cache.
$placement_map = variable_get('botcha_placement_map_cache', NULL);
$this
->assertNotNull($placement_map, 'BOTCHA placement cache should be set.');
/* UNUSED, as BOTCHA does not invoke botcha_placement_map_cache on protected forms
// Set BOTCHA on user register form.
botcha_set_form_id_setting('user_register_form', 'default');
// Visit user register form to fill the BOTCHA placement cache.
$this->drupalGet('user/register');
// Check if there is BOTCHA placement cache.
$placement_map = variable_get('botcha_placement_map_cache', NULL);
$this->assertNotNull($placement_map, 'BOTCHA placement cache should be set.');
*/
/* UNUSED, as BOTCHA does not have unset placement cache control
// Flush the cache
$this->drupalLogin($this->admin_user);
$this->drupalPost(self::BOTCHA_ADMIN_PATH, array(), t('Flush the BOTCHA placement cache'));
// Check that the placement cache is unset
$placement_map = variable_get('botcha_placement_map_cache', NULL);
$this->assertNull($placement_map, 'BOTCHA placement cache should be unset after flush.');
*/
}
/**
* Helper function to get the BOTCHA point setting straight from the database.
* @param string $form_id
* @return stdClass object
*/
private function getBotchaPointSettingFromDatabase($form_id) {
$result = db_query("SELECT * FROM {botcha_points} WHERE form_id = :form_id", array(
':form_id' => $form_id,
))
->fetchObject();
return $result;
}
/**
* Method for testing the BOTCHA point administration
*/
function testBotchaPointAdministration() {
// Generate BOTCHA point data:
// Drupal form ID should consist of lowercase alphanumerics and underscore)
$botcha_point_form_id = 'form_' . strtolower($this
->randomName(32));
// the 'default' BOTCHA is always available, so let's use it
$botcha_point_type = 'default';
// Log in as admin
$this
->drupalLogin($this->admin_user);
// Set BOTCHA point through admin/user/botcha/botcha_point
$form_values = array(
'botcha_point_form_id' => $botcha_point_form_id,
'botcha_type' => $botcha_point_type,
);
$this
->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point', $form_values, t('Save'));
$this
->assertText(t('Saved BOTCHA point settings.'), 'Saving of BOTCHA point settings');
// Check in database
$result = $this
->getBotchaPointSettingFromDatabase($botcha_point_form_id);
$this
->assertEqual($result->botcha_type, $botcha_point_type, 'Enabled BOTCHA point should have type set');
// Disable BOTCHA point again
$this
->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/disable', array(), t('Disable'));
$this
->assertRaw(t('Disabled BOTCHA for form %form_id.', array(
'%form_id' => $botcha_point_form_id,
)), 'Disabling of BOTCHA point');
// Check in database
$result = $this
->getBotchaPointSettingFromDatabase($botcha_point_form_id);
$this
->assertNull($result->botcha_type, 'Disabled BOTCHA point should have NULL as type');
// Set BOTCHA point through admin/user/botcha/botcha_point/$form_id
$form_values = array(
'botcha_type' => $botcha_point_type,
);
$this
->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id, $form_values, t('Save'));
$this
->assertText(t('Saved BOTCHA point settings.'), 'Saving of BOTCHA point settings');
// Check in database
$result = $this
->getBotchaPointSettingFromDatabase($botcha_point_form_id);
$this
->assertEqual($result->botcha_type, $botcha_point_type, 'Enabled BOTCHA point should have type set');
// Delete BOTCHA point
$this
->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/delete', array(), t('Delete'));
$this
->assertRaw(t('Deleted BOTCHA for form %form_id.', array(
'%form_id' => $botcha_point_form_id,
)), 'Deleting of BOTCHA point');
// Check in database
$result = $this
->getBotchaPointSettingFromDatabase($botcha_point_form_id);
$this
->assertFalse($result, 'Deleted BOTCHA point should be in database');
}
/**
* Method for testing the BOTCHA point administration
*/
function testBotchaPointAdministrationByNonAdmin() {
// First add a BOTCHA point (as admin)
$this
->drupalLogin($this->admin_user);
$botcha_point_form_id = 'form_' . strtolower($this
->randomName(32));
$botcha_point_type = 'default';
$form_values = array(
'botcha_point_form_id' => $botcha_point_form_id,
'botcha_type' => $botcha_point_type,
);
$this
->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/', $form_values, t('Save'));
$this
->assertText(t('Saved BOTCHA point settings.'), 'Saving of BOTCHA point settings');
// Switch from admin to nonadmin
$this
->drupalGet(url('logout', array(
'absolute' => TRUE,
)));
$this
->drupalLogin($this->normal_user);
// Try to set BOTCHA point through admin/user/botcha/botcha_point
$this
->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point');
$this
->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to set a BOTCHA point');
// Try to set BOTCHA point through admin/user/botcha/botcha_point/$form_id
$this
->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . 'form_' . strtolower($this
->randomName(32)));
$this
->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to set a BOTCHA point');
// Try to disable the BOTCHA point
$this
->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/disable');
$this
->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to disable a BOTCHA point');
// Try to delete the BOTCHA point
$this
->drupalGet(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/delete');
$this
->assertText(t('You are not authorized to access this page.'), 'Non admin should not be able to delete a BOTCHA point');
// Switch from nonadmin to admin again
$this
->drupalGet(url('logout', array(
'absolute' => TRUE,
)));
$this
->drupalLogin($this->admin_user);
// Check if original BOTCHA point still exists in database
$result = $this
->getBotchaPointSettingFromDatabase($botcha_point_form_id);
$this
->assertEqual($result->botcha_type, $botcha_point_type, 'Enabled BOTCHA point should still have type set');
// Delete BOTCHA point
$this
->drupalPost(self::BOTCHA_ADMIN_PATH . '/botcha_point/' . $botcha_point_form_id . '/delete', array(), t('Delete'));
$this
->assertRaw(t('Deleted BOTCHA for form %form_id.', array(
'%form_id' => $botcha_point_form_id,
)), 'Deleting of BOTCHA point');
}
}
class BotchaSessionReuseAttackTestCase extends BotchaBaseWebTestCase {
public static function getInfo() {
return array(
'name' => t('BOTCHA session reuse attack tests'),
'description' => t('Testing of the protection against BOTCHA session reuse attacks.'),
'group' => t('BOTCHA'),
);
}
/**
* Assert that the BOTCHA session ID reuse attack was detected.
*/
protected function assertBotchaSessionIdReuseAttackDetection() {
$this
->assertText(t(BOTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE), 'BOTCHA session ID reuse attack should be detected.', 'BOTCHA');
// // There should be an error message about bad post.
// $this->assertText(t(BOTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
// 'BOTCHA should be flagged as wrong.',
// 'BOTCHA');
}
function testBotchaSessionReuseAttackDetectionOnCommentPreview() {
$langcode = LANGUAGE_NONE;
// Create commentable node
$node = $this
->createNodeWithCommentsEnabled();
// Set Test BOTCHA on comment form.
botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
// Log in as normal user.
$this
->drupalLogin($this->normal_user);
// Go to comment form of commentable node.
$this
->drupalGet('comment/reply/' . $node->nid);
$this
->assertBotchaPresence(TRUE);
// Get form_build_id.
$form_build_id = $this
->getFormBuildIdFromForm();
// Post the form with the solution.
$edit = $this
->setCommentFormValues();
$this
->drupalPost(NULL, $edit, t('Preview'));
// Answer should be accepted and further BOTCHA ommitted.
$this
->assertBotchaResponseAccepted();
$this
->assertBotchaPresence(TRUE);
// Post a new comment, reusing the previous BOTCHA session.
$edit = $this
->setCommentFormValues();
$edit['form_build_id'] = $form_build_id;
$this
->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
// BOTCHA session reuse attack should be detected.
$this
->assertBotchaSessionIdReuseAttackDetection();
// There should be a BOTCHA.
$this
->assertBotchaPresence(TRUE);
// Verify that values that user posted are preserved in the new form
$values = $this
->getCommentFormValuesFromForm();
$this
->assertEqual($values['subject'], $edit['subject'], 'Subject should be preserved');
$this
->assertEqual($values["comment_body[{$langcode}][0][value]"], $edit["comment_body[{$langcode}][0][value]"], 'Comment body should be preserved');
// And verify new form can be re-submitted
unset($edit['form_build_id']);
$this
->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
$this
->assertBotchaResponseAccepted();
$this
->assertBotchaPresence(TRUE);
}
function testBotchaSessionReuseAttackDetectionOnNodeForm() {
// Set BOTCHA on page form.
botcha_set_form_id_setting('page_node_form', 'test');
// Log in as normal user.
$this
->drupalLogin($this->normal_user);
// Go to node add form.
$this
->drupalGet('node/add/page');
$this
->assertBotchaPresence(TRUE);
// Get form_build_id.
$form_build_id = $this
->getFormBuildIdFromForm();
// Page settings to post, with correct BOTCHA answer.
$edit = $this
->setNodeFormValues();
// Preview the node
$this
->drupalPost(NULL, $edit, t('Preview'));
// Answer should be accepted.
$this
->assertBotchaResponseAccepted();
$this
->assertBotchaPresence(TRUE);
// Post a new node, reusing the previous BOTCHA session.
$edit = $this
->setNodeFormValues();
$edit['form_build_id'] = $form_build_id;
$this
->drupalPost('node/add/page', $edit, t('Preview'));
// BOTCHA session reuse attack should be detected.
$this
->assertBotchaSessionIdReuseAttackDetection();
// There should be a BOTCHA.
$this
->assertBotchaPresence(TRUE);
}
function testBotchaSessionReuseAttackDetectionOnLoginForm() {
// Set BOTCHA on login form.
botcha_set_form_id_setting('user_login', 'test');
// Go to log in form.
$this
->drupalGet('user');
$this
->assertBotchaPresence(TRUE);
// Get form_build_id.
$form_build_id = $this
->getFormBuildIdFromForm();
// Log in through form.
$edit = array(
'name' => $this->normal_user->name,
'pass' => $this->normal_user->pass_raw,
);
$this
->drupalPost(NULL, $edit, t('Log in'));
$this
->assertBotchaResponseAccepted();
$this
->assertBotchaPresence(FALSE);
// If a "log out" link appears on the page, it is almost certainly because
// the login was successful.
$pass = $this
->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array(
'%name' => $this->normal_user->name,
)), t('User login'));
// Log out again.
$this
->drupalLogout();
// Try to log in again, reusing the previous BOTCHA session.
$edit += array(
'form_build_id' => $form_build_id,
);
$this
->drupalPost('user', $edit, t('Log in'));
// BOTCHA session reuse attack should be detected.
$this
->assertBotchaSessionIdReuseAttackDetection();
// There should be a BOTCHA.
$this
->assertBotchaPresence(TRUE);
}
public function testMultipleBotchaProtectedFormsOnOnePage() {
// Set Test BOTCHA on comment form and login block
botcha_set_form_id_setting(self::COMMENT_FORM_ID, 'test');
botcha_set_form_id_setting('user_login_block', 'test');
$this
->allowCommentPostingForAnonymousVisitors();
// Create a node with comments enabled.
$node = $this
->createNodeWithCommentsEnabled();
// Preview comment with correct BOTCHA answer.
$edit = $this
->setCommentFormValues();
$comment_subject = $edit['subject'];
$this
->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
// Post should be accepted: no warnings,
// no BOTCHA reuse detection (which could be used by user log in block).
$this
->assertBotchaResponseAccepted();
$this
->assertText($comment_subject);
}
}
// Some tricks to debug:
// drupal_debug($data) // from devel module
// file_put_contents('tmp.simpletest.html', $this->drupalGetContent());
Constants
Classes
Name | Description |
---|---|
BotchaAdminTestCase | |
BotchaBaseWebTestCase | Base class for BOTCHA tests. |
BotchaSessionReuseAttackTestCase | |
BotchaTestCase |