drupal/modules/search/search.test

613 lines
21 KiB
Plaintext

<?php
// $Id$
// The search index can contain different types of content. Typically the type is 'node'.
// Here we test with _test_ and _test2_ as the type.
define('SEARCH_TYPE', '_test_');
define('SEARCH_TYPE_2', '_test2_');
define('SEARCH_TYPE_JPN', '_test3_');
class SearchMatchTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Search engine queries',
'description' => 'Indexes content and queries it.',
'group' => 'Search',
);
}
/**
* Implementation setUp().
*/
function setUp() {
parent::setUp('search');
}
/**
* Test search indexing.
*/
function testMatching() {
$this->_setup();
$this->_testQueries();
}
/**
* Set up a small index of items to test against.
*/
function _setup() {
variable_set('minimum_word_size', 3);
for ($i = 1; $i <= 7; ++$i) {
search_index($i, SEARCH_TYPE, $this->getText($i));
}
for ($i = 1; $i <= 5; ++$i) {
search_index($i + 7, SEARCH_TYPE_2, $this->getText2($i));
}
// No getText builder function for Japanese text; just a simple array.
foreach (array(
13 => '以呂波耳・ほへとち。リヌルヲ。',
14 => 'ドルーパルが大好きよ!',
15 => 'コーヒーとケーキ',
) as $i => $jpn) {
search_index($i, SEARCH_TYPE_JPN, $jpn);
}
search_update_totals();
}
/**
* _test_: Helper method for generating snippets of content.
*
* Generated items to test against:
* 1 ipsum
* 2 dolore sit
* 3 sit am ut
* 4 am ut enim am
* 5 ut enim am minim veniam
* 6 enim am minim veniam es cillum
* 7 am minim veniam es cillum dolore eu
*/
function getText($n) {
$words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
return implode(' ', array_slice($words, $n - 1, $n));
}
/**
* _test2_: Helper method for generating snippets of content.
*
* Generated items to test against:
* 8 dear
* 9 king philip
* 10 philip came over
* 11 came over from germany
* 12 over from germany swimming
*/
function getText2($n) {
$words = explode(' ', "Dear King Philip came over from Germany swimming.");
return implode(' ', array_slice($words, $n - 1, $n));
}
/**
* Run predefine queries looking for indexed terms.
*/
function _testQueries() {
/*
Note: OR queries that include short words in OR groups are only accepted
if the ORed terms are ANDed with at least one long word in the rest of the query.
e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good
e.g. dolore OR ut = (dolore) OR (ut) -> bad
This is a design limitation to avoid full table scans.
*/
$queries = array(
// Simple AND queries.
'ipsum' => array(1),
'enim' => array(4, 5, 6),
'xxxxx' => array(),
'enim minim' => array(5, 6),
'enim xxxxx' => array(),
'dolore eu' => array(7),
'dolore xx' => array(),
'ut minim' => array(5),
'xx minim' => array(),
'enim veniam am minim ut' => array(5),
// Simple OR queries.
'dolore OR ipsum' => array(1, 2, 7),
'dolore OR xxxxx' => array(2, 7),
'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7),
'ipsum OR dolore sit OR cillum' => array(2, 7),
'minim dolore OR ipsum' => array(7),
'dolore OR ipsum veniam' => array(7),
'minim dolore OR ipsum OR enim' => array(5, 6, 7),
'dolore xx OR yy' => array(),
'xxxxx dolore OR ipsum' => array(),
// Negative queries.
'dolore -sit' => array(7),
'dolore -eu' => array(2),
'dolore -xxxxx' => array(2, 7),
'dolore -xx' => array(2, 7),
// Phrase queries.
'"dolore sit"' => array(2),
'"sit dolore"' => array(),
'"am minim veniam es"' => array(6, 7),
'"minim am veniam es"' => array(),
// Mixed queries.
'"am minim veniam es" OR dolore' => array(2, 6, 7),
'"minim am veniam es" OR "dolore sit"' => array(2),
'"minim am veniam es" OR "sit dolore"' => array(),
'"am minim veniam es" -eu' => array(6),
'"am minim veniam" -"cillum dolore"' => array(5, 6),
'"am minim veniam" -"dolore cillum"' => array(5, 6, 7),
'xxxxx "minim am veniam es" OR dolore' => array(),
'xx "minim am veniam es" OR dolore' => array()
);
foreach ($queries as $query => $results) {
$result = db_select('search_index', 'i')
->extend('SearchQuery')
->searchExpression($query, SEARCH_TYPE)
->execute();
$set = $result ? $result->fetchAll() : array();
$this->_testQueryMatching($query, $set, $results);
$this->_testQueryScores($query, $set, $results);
}
// These queries are run against the second index type, SEARCH_TYPE_2.
$queries = array(
// Simple AND queries.
'ipsum' => array(),
'enim' => array(),
'enim minim' => array(),
'dear' => array(8),
'germany' => array(11, 12),
);
foreach ($queries as $query => $results) {
$result = db_select('search_index', 'i')
->extend('SearchQuery')
->searchExpression($query, SEARCH_TYPE_2)
->execute();
$set = $result ? $result->fetchAll() : array();
$this->_testQueryMatching($query, $set, $results);
$this->_testQueryScores($query, $set, $results);
}
// These queries are run against the third index type, SEARCH_TYPE_JPN.
$queries = array(
// Simple AND queries.
'呂波耳' => array(13),
'以呂波耳' => array(13),
'ほへと ヌルヲ' => array(13),
'とちリ' => array(),
'ドルーパル' => array(14),
'パルが大' => array(14),
'コーヒー' => array(15),
'ヒーキ' => array(),
);
foreach ($queries as $query => $results) {
$result = db_select('search_index', 'i')
->extend('SearchQuery')
->searchExpression($query, SEARCH_TYPE_JPN)
->execute();
$set = $result ? $result->fetchAll() : array();
$this->_testQueryMatching($query, $set, $results);
$this->_testQueryScores($query, $set, $results);
}
}
/**
* Test the matching abilities of the engine.
*
* Verify if a query produces the correct results.
*/
function _testQueryMatching($query, $set, $results) {
// Get result IDs.
$found = array();
foreach ($set as $item) {
$found[] = $item->sid;
}
// Compare $results and $found.
sort($found);
sort($results);
$this->assertEqual($found, $results, "Query matching '$query'");
}
/**
* Test the scoring abilities of the engine.
*
* Verify if a query produces normalized, monotonous scores.
*/
function _testQueryScores($query, $set, $results) {
// Get result scores.
$scores = array();
foreach ($set as $item) {
$scores[] = $item->calculated_score;
}
// Check order.
$sorted = $scores;
sort($sorted);
$this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
// Check range.
$this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
}
}
class SearchBikeShed extends DrupalWebTestCase {
protected $searching_user;
public static function getInfo() {
return array(
'name' => 'Bike shed',
'description' => 'Tests the bike shed text on the no results page.',
'group' => 'Search'
);
}
function setUp() {
parent::setUp('search');
// Create user.
$this->searching_user = $this->drupalCreateUser(array('search content'));
}
function testFailedSearch() {
$this->drupalLogin($this->searching_user);
$this->drupalGet('search/node');
$this->assertText(t('Enter your keywords'));
$edit = array();
$edit['keys'] = 'bike shed ' . $this->randomName();
$this->drupalPost('search/node', $edit, t('Search'));
$this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), t('Help text is displayed when search returns no results.'));
}
}
class SearchAdvancedSearchForm extends DrupalWebTestCase {
protected $node;
public static function getInfo() {
return array(
'name' => 'Advanced search form',
'description' => 'Indexes content and tests the advanced search form.',
'group' => 'Search',
);
}
function setUp() {
parent::setUp('search');
// Create and login user.
$test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes'));
$this->drupalLogin($test_user);
// Create initial node.
$node = $this->drupalCreateNode();
$this->node = $this->drupalCreateNode();
// First update the index. This does the initial processing.
node_update_index();
// Then, run the shutdown function. Testing is a unique case where indexing
// and searching has to happen in the same request, so running the shutdown
// function manually is needed to finish the indexing process.
search_update_totals();
}
/**
* Test using the search form with GET and POST queries.
* Test using the advanced search form to limit search to nodes of type "Basic page".
*/
function testNodeType() {
$this->assertTrue($this->node->type == 'page', t('Node type is Basic page.'));
// Assert that the dummy title doesn't equal the real title.
$dummy_title = 'Lorem ipsum';
$this->assertNotEqual($dummy_title, $this->node->title, t("Dummy title doens't equal node title"));
// Search for the dummy title with a GET query.
$this->drupalGet('search/node/' . $dummy_title);
$this->assertNoText($this->node->title, t('Basic page node is not found with dummy title.'));
// Search for the title of the node with a GET query.
$this->drupalGet('search/node/' . $this->node->title);
$this->assertText($this->node->title, t('Basic page node is found with GET query.'));
// Search for the title of the node with a POST query.
$edit = array('or' => $this->node->title);
$this->drupalPost('search/node', $edit, t('Advanced search'));
$this->assertText($this->node->title, t('Basic page node is found with POST query.'));
// Advanced search type option.
$this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search'));
$this->assertText($this->node->title, t('Basic page node is found with POST query and type:page.'));
$this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search'));
$this->assertText('bike shed', t('Article node is not found with POST query and type:article.'));
}
}
class SearchRankingTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Search engine ranking',
'description' => 'Indexes content and tests ranking factors.',
'group' => 'Search',
);
}
/**
* Implementation setUp().
*/
function setUp() {
parent::setUp('search', 'statistics', 'comment');
}
function testRankings() {
// Login with sufficient privileges.
$this->drupalLogin($this->drupalCreateUser(array('post comments without approval', 'create page content')));
// Build a list of the rankings to test.
$node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
// Create nodes for testing.
foreach ($node_ranks as $node_rank) {
$settings = array('type' => 'page', 'title' => array(LANGUAGE_NONE => array(array('value' => 'Drupal rocks'))), 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))));
foreach (array(0, 1) as $num) {
if ($num == 1) {
switch ($node_rank) {
case 'sticky':
case 'promote':
$settings[$node_rank] = 1;
break;
case 'relevance':
$settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks";
break;
case 'recent':
$settings['created'] = REQUEST_TIME + 3600;
break;
case 'comments':
$settings['comment'] = 2;
break;
}
}
$nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
}
}
// Update the search index.
module_invoke_all('update_index');
search_update_totals();
// Refresh variables after the treatment.
$this->refreshVariables();
// Add a comment to one of the nodes.
$edit = array();
$edit['subject'] = 'my comment title';
$edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment';
$this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid);
$this->drupalPost(NULL, $edit, t('Preview'));
$this->drupalPost(NULL, $edit, t('Save'));
// Enable counting of statistics.
variable_set('statistics_count_content_views', 1);
// Then View one of the nodes a bunch of times.
for ($i = 0; $i < 5; $i ++) {
$this->drupalGet('node/' . $nodes['views'][1]->nid);
}
// Test each of the possible rankings.
foreach ($node_ranks as $node_rank) {
// Disable all relevancy rankings except the one we are testing.
foreach ($node_ranks as $var) {
variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0);
}
// Do the search and assert the results.
$set = node_search_execute('rocks');
$this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
}
}
}
class SearchBlockTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Block availability',
'description' => 'Check if the search form block is available.',
'group' => 'Search',
);
}
function setUp() {
parent::setUp('search');
// Create and login user
$admin_user = $this->drupalCreateUser(array('administer blocks', 'search content'));
$this->drupalLogin($admin_user);
}
function testSearchFormBlock() {
// Set block title to confirm that the interface is availble.
$this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), t('Save block'));
$this->assertText(t('The block configuration has been saved.'), t('Block configuration set.'));
// Set the block to a region to confirm block is availble.
$edit = array();
$edit['search_form[region]'] = 'footer';
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
$this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.'));
}
/**
* Test that the search block form works correctly.
*/
function testBlock() {
// Enable the block, and place it in the 'content' region so that it isn't
// hidden on 404 pages.
$edit = array('search_form[region]' => 'content');
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
// Test a normal search via the block form, from the front page.
$terms = array('search_block_form' => 'test');
$this->drupalPost('node', $terms, t('Search'));
$this->assertText('Your search yielded no results');
// Test a search from the block on a 404 page.
$this->drupalGet('foo');
$this->assertResponse(404);
$this->drupalPost(NULL, $terms, t('Search'));
$this->assertResponse(200);
$this->assertText('Your search yielded no results');
// Test a search from the block when it doesn't appear on the search page.
$edit = array('pages' => 'search');
$this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block'));
$this->drupalPost('node', $terms, t('Search'));
$this->assertText('Your search yielded no results');
}
}
/**
* Test integration searching comments.
*/
class SearchCommentTestCase extends DrupalWebTestCase {
protected $admin_user;
public static function getInfo() {
return array(
'name' => 'Comment Search tests',
'description' => 'Verify text formats and filters used elsewhere.',
'group' => 'Search',
);
}
function setUp() {
parent::setUp('comment', 'search');
// Create and log in an administrative user having access to the Full HTML
// text format.
$full_html_format = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchObject();
$this->admin_user = $this->drupalCreateUser(array(
'administer filters',
filter_permission_name($full_html_format),
'administer permissions',
'create page content',
'post comments without approval',
));
$this->drupalLogin($this->admin_user);
}
/**
* Verify that comments are rendered using proper format in search results.
*/
function testSearchResultsComment() {
$comment_body = 'Test comment body';
variable_set('comment_preview_article', DRUPAL_OPTIONAL);
// Enable check_plain() for 'Filtered HTML' text format.
$filtered_html_format_id = db_query_range('SELECT format FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Filtered HTML'))->fetchField();
$edit = array(
'filters[filter_html_escape][status]' => $filtered_html_format_id,
);
$this->drupalPost('admin/config/content/formats/1', $edit, t('Save configuration'));
// Allow anonymous users to search content.
$edit = array(
DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
// @todo Comments are added to search index without checking first whether
// anonymous users are allowed to access comments.
DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
// @todo Without this permission, "Login or register to post comments" is
// added to the search index. Comment.module is not guilty; that text
// seems to be added via node links.
DRUPAL_ANONYMOUS_RID . '[post comments]' => 1,
);
$this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
// Create a node.
$node = $this->drupalCreateNode(array('type' => 'article'));
// Post a comment using 'Full HTML' text format.
$edit_comment = array();
$edit_comment['subject'] = 'Test comment subject';
$edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
$full_html_format_id = db_query_range('SELECT format FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchField();
$edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id;
$this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save'));
// Invoke search index update.
$this->drupalLogout();
$this->cronRun();
// Search for the comment subject.
$edit = array(
'search_block_form' => "'" . $edit_comment['subject'] . "'",
);
$this->drupalPost('', $edit, t('Search'));
$this->assertText($node->title, t('Node found in search results.'));
$this->assertText($edit_comment['subject'], t('Comment subject found in search results.'));
// Search for the comment body.
$edit = array(
'search_block_form' => "'" . $comment_body . "'",
);
$this->drupalPost('', $edit, t('Search'));
$this->assertText($node->title, t('Node found in search results.'));
// Verify that comment is rendered using proper format.
$this->assertText($comment_body, t('Comment body text found in search results.'));
$this->assertNoRaw(t('n/a'), t('HTML in comment body is not hidden.'));
$this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), t('HTML in comment body is not escaped.'));
// Hide comments.
$this->drupalLogin($this->admin_user);
$node->comment = 0;
node_save($node);
// Invoke search index update.
$this->drupalLogout();
$this->cronRun();
// Search for $title.
$this->drupalPost('', $edit, t('Search'));
$this->assertNoText($comment_body, t('Comment body text not found in search results.'));
}
}
/**
* Test search_simplify() on every Unicode character.
*/
class SearchSimplifyTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Search simplify',
'description' => 'Check that simplification works as intended.',
'group' => 'Search',
);
}
function testSearchSimplify() {
$input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
$strings = explode(chr(10), $input);
foreach ($strings as $key => $string) {
$simplified = search_simplify($string);
if ($key % 2) {
$this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
}
else {
$this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed on line $key.");
}
}
$string = '';
for ($i = 0; $i < 32; $i++) {
$string .= chr($i);
}
// Diff really does not like files starting with \0 so test it separately.
$this->assertIdentical(' ', search_simplify($string), t('Search simplify works for ASCII control characters.'));
}
}