Skip to content
search.test 73.2 KiB
Newer Older
// 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_2', '_test2_');

class SearchMatchTestCase extends DrupalWebTestCase {
      '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);
    }
   * _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'");
  }
}
/**
 * Tests the bike shed text on no results page, and text on the search page.
 */
class SearchPageText extends DrupalWebTestCase {
      'name' => 'Search page text',
      'description' => 'Tests the bike shed text on the no results page, and various other text on search pages.',
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles'));
  /**
   * Tests the failed search text, and various other text on the search page.
   */
  function testSearchText() {
    $this->drupalLogin($this->searching_user);
    $this->drupalGet('search/node');
    $this->assertText(t('Enter your keywords'));
    $this->assertText(t('Search'));
    $title = t('Search') . ' | Drupal';
    $this->assertTitle($title, 'Search page title is correct');

    $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.'));
    $this->assertText(t('Search'));
    $this->assertTitle($title, 'Search page title is correct');

    $edit['keys'] = $this->searching_user->name;
    $this->drupalPost('search/user', $edit, t('Search'));
    $this->assertText(t('Search'));
    $this->assertTitle($title, 'Search page title is correct');

    // Test that search keywords containing slashes are correctly loaded
    // from the path and displayed in the search form.
    $arg = $this->randomName() . '/' . $this->randomName();
    $this->drupalGet('search/node/' . $arg);
    $input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']");
    $this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.');
class SearchAdvancedSearchForm extends DrupalWebTestCase {
  protected $node;

      '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".
    $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 doesn'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 {
      'name' => 'Search engine ranking',
      'description' => 'Indexes content and tests ranking factors.',
      'group' => 'Search',
    parent::setUp('search', 'statistics', 'comment');
  }

  function testRankings() {
    // Login with sufficient privileges.
    $this->drupalLogin($this->drupalCreateUser(array('skip comment 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' => '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 'comments':
              $settings['comment'] = 2;
              break;
          }
        }
        $nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
      }
    }

    // Update the search index.
    module_invoke_all('update_index');
    // Refresh variables after the treatment.
    $this->refreshVariables();

    $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);
      $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
  /**
   * Test rankings of HTML tags.
   */
  function testHTMLRankings() {
    // Login with sufficient privileges.
    $this->drupalLogin($this->drupalCreateUser(array('create page content')));
    // Test HTML tags with different weights.
    $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag');
    $shuffled_tags = $sorted_tags;

    // Shuffle tags to ensure HTML tags are ranked properly.
    shuffle($shuffled_tags);
    $settings = array(
      'type' => 'page',
    );
    foreach ($shuffled_tags as $tag) {
      switch ($tag) {
        case 'a':
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 'full_html')));
          break;
        case 'notag':
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks')));
          break;
        default:
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html')));
          break;
      }
      $nodes[$tag] = $this->drupalCreateNode($settings);
    }

    // Update the search index.
    module_invoke_all('update_index');
    search_update_totals();

    // Refresh variables after the treatment.
    $this->refreshVariables();
    // Disable all other rankings.
    $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views');
    foreach ($node_ranks as $node_rank) {
      variable_set('node_rank_' . $node_rank, 0);
    }
    $set = node_search_execute('rocks');

    // Test the ranking of each tag.
    foreach ($sorted_tags as $tag_rank => $tag) {
      // Assert the results.
      if ($tag == 'notag') {
        $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for plain text order.');
        $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for "&lt;' . $sorted_tags[$tag_rank] . '&gt;" order.');
      }
    }

    // Test tags with the same weight against the sorted tags.
    $unsorted_tags = array('u', 'b', 'i', 'strong', 'em');
    foreach ($unsorted_tags as $tag) {
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html')));
      $node = $this->drupalCreateNode($settings);

      // Update the search index.
      module_invoke_all('update_index');
      search_update_totals();

      // Refresh variables after the treatment.
      $this->refreshVariables();

      $set = node_search_execute('rocks');

      // Ranking should always be second to last.
      $set = array_slice($set, -2, 1);

      // Assert the results.
      $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "&lt;' . $tag . '&gt;" order.');
      // Delete node so it doesn't show up in subsequent search results.
      node_delete($node->nid);
    }
  }

  /**
   * Verifies that if we combine two rankings, search still works.
   *
   * See issue http://drupal.org/node/771596
   */
  function testDoubleRankings() {
    // Login with sufficient privileges.
    $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content')));

    // See testRankings() above - build a node that will rank high for sticky.
    $settings = array(
      'title' => 'Drupal rocks',
      'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
      'sticky' => 1,
    );

    $node = $this->drupalCreateNode($settings);

    // Update the search index.
    module_invoke_all('update_index');
    search_update_totals();

    // Refresh variables after the treatment.
    $this->refreshVariables();

    // Set up for ranking sticky and lots of comments; make sure others are
    // disabled.
    $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
    foreach ($node_ranks as $var) {
      $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0;
      variable_set('node_rank_' . $var, $value);
    }

    // Do the search and assert the results.
    $set = node_search_execute('rocks');
    $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search double ranking order.');
  }

class SearchBlockTestCase extends DrupalWebTestCase {
      '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 available.
    $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 available.
    $edit['blocks[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('blocks[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');

    // Confirm that the user is redirected to the search page.
    $this->assertEqual(
      $this->getUrl(),
      url('search/node/' . $terms['search_block_form'], array('absolute' => TRUE)),
      t('Redirected to correct url.')
    );

    // Test an empty search via the block form, from the front page.
    $terms = array('search_block_form' => '');
    $this->drupalPost('node', $terms, t('Search'));
    $this->assertText('Please enter some keywords');

    // Confirm that the user is redirected to the search page, when form is submitted empty.
    $this->assertEqual(
      $this->getUrl(),
      url('search/node/', array('absolute' => TRUE)),
      t('Redirected to correct url.')
    );
/**
 * Tests that searching for a phrase gets the correct page count.
 */
class SearchExactTestCase extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Search engine phrase queries',
      'description' => 'Tests that searching for a phrase gets the correct page count.',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search');
  }

  /**
   * Tests that the correct number of pager links are found for both keywords and phrases.
   */
  function testExactQuery() {
    // Login with sufficient privileges.
    $this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content')));

    $settings = array(
      'type' => 'page',
      'title' => 'Simple Node',
    );
    // Create nodes with exact phrase.
    for ($i = 0; $i <= 17; $i++) {
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza')));
      $this->drupalCreateNode($settings);
    }
    // Create nodes containing keywords.
    for ($i = 0; $i <= 17; $i++) {
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza')));
      $this->drupalCreateNode($settings);
    }

    // Update the search index.
    module_invoke_all('update_index');
    search_update_totals();

    // Refresh variables after the treatment.
    $this->refreshVariables();

    // Test that the correct number of pager links are found for keyword search.
    $edit = array('keys' => 'love pizza');
    $this->drupalPost('search/node', $edit, t('Search'));
    $this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
    $this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
    $this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
    $this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');

    // Test that the correct number of pager links are found for exact phrase search.
    $edit = array('keys' => '"love pizza"');
    $this->drupalPost('search/node', $edit, t('Search'));
    $this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
    $this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
  }
}

/**
 * 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 = filter_format_load('full_html');
      filter_permission_name($full_html_format),
      'access comments',
    );
    $this->admin_user = $this->drupalCreateUser($permissions);
    $this->drupalLogin($this->admin_user);
  }

  /**
   * Verify that comments are rendered using proper format in search results.
   */
  function testSearchResultsComment() {
    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
    // Enable check_plain() for 'Filtered HTML' text format.
      'filters[filter_html_escape][status]' => TRUE,
    $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration'));
    // Allow anonymous users to search content.
    $edit = array(
      DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
      DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
      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['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
    $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();
    // 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.'));
    );
    $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();

    // Search for $title.
    $this->drupalPost('', $edit, t('Search'));
    $this->assertNoText($comment_body, t('Comment body text not found in search results.'));

  /**
   * Verify access rules for comment indexing with different permissions.
   */
  function testSearchResultsCommentAccess() {
    $comment_body = 'Test comment body';
    $this->comment_subject = 'Test comment subject';
    $this->admin_role = $this->admin_user->roles;
    unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]);
    $this->admin_role = key($this->admin_role);

    // Create a node.
    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
    $this->node = $this->drupalCreateNode(array('type' => 'article'));

    // Post a comment using 'Full HTML' text format.
    $edit_comment = array();
    $edit_comment['subject'] = $this->comment_subject;
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
    $this->drupalPost('comment/reply/' . $this->node->nid, $edit_comment, t('Save'));

    $this->drupalLogout();
    $this->setRolePermissions(DRUPAL_ANONYMOUS_RID);
    $this->checkCommentAccess('Anon user has search permission but no access comments permission, comments should not be indexed');

    $this->setRolePermissions(DRUPAL_ANONYMOUS_RID, TRUE);
    $this->checkCommentAccess('Anon user has search permission and access comments permission, comments should be indexed', TRUE);

    $this->drupalLogin($this->admin_user);
    $this->drupalGet('admin/people/permissions');

    // Disable search access for authenticated user to test admin user.
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, FALSE, FALSE);

    $this->setRolePermissions($this->admin_role);
    $this->checkCommentAccess('Admin user has search permission but no access comments permission, comments should not be indexed');

    $this->setRolePermissions($this->admin_role, TRUE);
    $this->checkCommentAccess('Admin user has search permission and access comments permission, comments should be indexed', TRUE);

    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID);
    $this->checkCommentAccess('Authenticated user has search permission but no access comments permission, comments should not be indexed');

    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE);
    $this->checkCommentAccess('Authenticated user has search permission and access comments permission, comments should be indexed', TRUE);

    // Verify that access comments permission is inherited from the
    // authenticated role.
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE);
    $this->setRolePermissions($this->admin_role);
    $this->checkCommentAccess('Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments', TRUE);

    // Verify that search content permission is inherited from the authenticated
    // role.
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE);
    $this->setRolePermissions($this->admin_role, TRUE, FALSE);
    $this->checkCommentAccess('Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search', TRUE);
  }

  /**
   * Set permissions for role.
   */
  function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
    $permissions = array(
      'access comments' => $access_comments,
      'search content' => $search_content,
    );
    user_role_change_permissions($rid, $permissions);
  }

  /**
   * Update search index and search for comment.
   */
  function checkCommentAccess($message, $assume_access = FALSE) {
    // Invoke search index update.
    search_touch_node($this->node->nid);
    $this->cronRun();

    // Search for the comment subject.
    $edit = array(
      'search_block_form' => "'" . $this->comment_subject . "'",
    );
    $this->drupalPost('', $edit, t('Search'));
    $method = $assume_access ? 'assertText' : 'assertNoText';
    $verb = $assume_access ? 'found' : 'not found';
    $this->{$method}($this->node->title, "Node $verb in search results: " . $message);
    $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message);
  }

  /**
   * Verify that 'add new comment' does not appear in search results or index.
   */
  function testAddNewComment() {
    // Create a node with a short body.
    $settings = array(
      'type' => 'article',
      'title' => 'short title',
      'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))),
    );

    $user = $this->drupalCreateUser(array('search content', 'create article content', 'access content'));
    $this->drupalLogin($user);

    $node = $this->drupalCreateNode($settings);
    // Verify that if you view the node on its own page, 'add new comment'
    // is there.
    $this->drupalGet('node/' . $node->nid);
    $this->assertText(t('Add new comment'), t('Add new comment appears on node page'));

    // Run cron to index this page.
    $this->drupalLogout();
    $this->cronRun();

    // Search for 'comment'. Should be no results.
    $this->drupalLogin($user);
    $this->drupalPost('search/node', array('keys' => 'comment'), t('Search'));
    $this->assertText(t('Your search yielded no results'), t('No results searching for the word comment'));

    // Search for the node title. Should be found, and 'Add new comment' should
    // not be part of the search snippet.
    $this->drupalPost('search/node', array('keys' => 'short'), t('Search'));
    $this->assertText($node->title, t('Search for keyword worked'));
    $this->assertNoText(t('Add new comment'), t('Add new comment does not appear on search results page'));
  }

/**
 * Tests search_expression_insert() and search_expression_extract().
 *
 * @see http://drupal.org/node/419388 (issue)
 */
class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Search expression insert/extract',
      'description' => 'Tests the functions search_expression_insert() and search_expression_extract()',
      'group' => 'Search',
    );
  }

  function setUp() {
    drupal_load('module', 'search');
    parent::setUp();
  }

  /**
   * Tests search_expression_insert() and search_expression_extract().
   */
  function testInsertExtract() {
    $base_expression = "mykeyword";
    // Build an array of option, value, what should be in the expression, what
    // should be retrieved from expression.
    $cases = array(
      array('foo', 'bar', 'foo:bar', 'bar'), // Normal case.
      array('foo', NULL, '', NULL), // Empty value: shouldn't insert.
      array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string.
      array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string.
      array('foo', '0', 'foo:0', '0'), // String zero as value: should insert.
      array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert.
    );

    foreach ($cases as $index => $case) {
      $after_insert = search_expression_insert($base_expression, $case[0], $case[1]);
      if (empty($case[2])) {
        $this->assertEqual($after_insert, $base_expression, "Empty insert does not change expression in case $index");
      }
      else {
        $this->assertEqual($after_insert, $base_expression . ' ' . $case[2], "Insert added correct expression for case $index");
      }

      $retrieved = search_expression_extract($after_insert, $case[0]);
      if (!isset($case[3])) {
        $this->assertFalse(isset($retrieved), "Empty retrieval results in unset value in case $index");
      }
      else {
        $this->assertEqual($retrieved, $case[3], "Value is retrieved for case $index");
      }

      $after_clear = search_expression_insert($after_insert, $case[0]);
      $this->assertEqual(trim($after_clear), $base_expression, "After clearing, base expression is restored for case $index");

      $cleared = search_expression_extract($after_clear, $case[0]);
      $this->assertFalse(isset($cleared), "After clearing, value could not be retrieved for case $index");
    }
  }
}

/**
 * Tests that comment count display toggles properly on comment status of node