<?php

/**
 * @file
 * Tests for search.module.
 */

// 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_');

/**
 * Indexes content and queries it.
 */
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'");
  }
}

/**
 * Tests the bike shed text on no results page, and text on the search page.
 */
class SearchPageText extends DrupalWebTestCase {
  protected $searching_user;

  public static function getInfo() {
    return array(
      'name' => 'Search page text',
      'description' => 'Tests the bike shed text on the no results page, and various other text on search pages.',
      'group' => 'Search'
    );
  }

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

    // Create user.
    $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.'), '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.');

    // Test a search input exceeding the limit of AND/OR combinations to test
    // the Denial-of-Service protection.
    $limit = variable_get('search_and_or_limit', 7);
    $keys = array();
    for ($i = 0; $i < $limit + 1; $i++) {
      $keys[] = $this->randomName(3);
      if ($i % 2 == 0) {
        $keys[] = 'OR';
      }
    }
    $edit['keys'] = implode(' ', $keys);
    $this->drupalPost('search/node', $edit, t('Search'));
    $this->assertRaw(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $limit)));
  }
}

/**
 * Indexes content and tests the advanced search form.
 */
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', '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, "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, '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, '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, '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, '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', 'Article node is not found with POST query and type:article.');
  }
}

/**
 * Indexes content and tests ranking factors.
 */
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('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 '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.');
    }
  }

  /**
   * 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',
      'title' => 'Simple node',
    );
    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.');
      } else {
        $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(
      'type' => 'page',
      '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.');
  }
}

/**
 * Tests the rendering of the search block.
 */
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 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.'), 'Block configuration set.');

    // Set the block to a region to confirm block is available.
    $edit = array();
    $edit['blocks[search_form][region]'] = 'footer';
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
    $this->assertText(t('The block settings have been updated.'), '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)),
      '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)),
      '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' => 'Test integration searching comments.',
      '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');
    $permissions = array(
      'administer filters',
      filter_permission_name($full_html_format),
      'administer permissions',
      'create page content',
      'skip comment approval',
      '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() {
    $comment_body = 'Test comment body';

    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
    // Enable check_plain() for 'Filtered HTML' text format.
    $filtered_html_format_id = 'filtered_html';
    $edit = array(
      '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 = array();
    $edit_comment['subject'] = 'Test comment subject';
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
    $full_html_format_id = 'full_html';
    $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, 'Node found in search results.');
    $this->assertText($edit_comment['subject'], '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, 'Node found in search results.');

    // Verify that comment is rendered using proper format.
    $this->assertText($comment_body, 'Comment body text found in search results.');
    $this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.');
    $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), '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, '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'), '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'), '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, 'Search for keyword worked');
    $this->assertNoText(t('Add new comment'), '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
 *
 * Issue 537278
 *
 * - Nodes with comment status set to Open should always how comment counts
 * - Nodes with comment status set to Closed should show comment counts
 *     only when there are comments
 * - Nodes with comment status set to Hidden should never show comment counts
 */
class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
  protected $searching_user;
  protected $searchable_nodes;

  public static function getInfo() {
    return array(
      'name' => 'Comment count toggle',
      'description' => 'Verify that comment count display toggles properly on comment status of node.',
      'group' => 'Search',
    );
  }

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

    // Create searching user.
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));

    // Create initial nodes.
    $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase'))));

    $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params);
    $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params);

    // Login with sufficient privileges.
    $this->drupalLogin($this->searching_user);

    // Create a comment array
    $edit_comment = array();
    $edit_comment['subject'] = $this->randomName();
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
    $filtered_html_format_id = 'filtered_html';
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;

    // Post comment to the test node with comment
    $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save'));

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

  /**
   * Verify that comment count display toggles properly on comment status of node
   */
  function testSearchCommentCountToggle() {
    // Search for the nodes by string in the node body.
    $edit = array(
      'search_block_form' => "'SearchCommentToggleTestCase'",
    );

    // Test comment count display for nodes with comment status set to Open
    $this->drupalPost('', $edit, t('Search'));
    $this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open');
    $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open');

    // Test comment count display for nodes with comment status set to Closed
    $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED;
    node_save($this->searchable_nodes['0 comments']);
    $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED;
    node_save($this->searchable_nodes['1 comment']);

    $this->drupalPost('', $edit, t('Search'));
    $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed');
    $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed');

    // Test comment count display for nodes with comment status set to Hidden
    $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN;
    node_save($this->searchable_nodes['0 comments']);
    $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN;
    node_save($this->searchable_nodes['1 comment']);

    $this->drupalPost('', $edit, t('Search'));
    $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden');
    $this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden');
  }
}

/**
 * Test search_simplify() on every Unicode character, and some other cases.
 */
class SearchSimplifyTestCase extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Search simplify',
      'description' => 'Check that the search_simply() function works as intended.',
      'group' => 'Search',
    );
  }

  /**
   * Tests that all Unicode characters simplify correctly.
   */
  function testSearchSimplifyUnicode() {
    // This test uses a file that was constructed so that the even lines are
    // boundary characters, and the odd lines are valid word characters. (It
    // was generated as a sequence of all the Unicode characters, and then the
    // boundary chararacters (punctuation, spaces, etc.) were split off into
    // their own lines).  So the even-numbered lines should simplify to nothing,
    // and the odd-numbered lines we need to split into shorter chunks and
    // verify that simplification doesn't lose any characters.
    $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
    $basestrings = explode(chr(10), $input);
    $strings = array();
    foreach ($basestrings as $key => $string) {
      if ($key %2) {
        // Even line - should simplify down to a space.
        $simplified = search_simplify($string);
        $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
      }
      else {
        // Odd line, should be word characters.
        // Split this into 30-character chunks, so we don't run into limits
        // of truncation in search_simplify().
        $start = 0;
        while ($start < drupal_strlen($string)) {
          $newstr = drupal_substr($string, $start, 30);
          // Special case: leading zeros are removed from numeric strings,
          // and there's one string in this file that is numbers starting with
          // zero, so prepend a 1 on that string.
          if (preg_match('/^[0-9]+$/', $newstr)) {
            $newstr = '1' . $newstr;
          }
          $strings[] = $newstr;
          $start += 30;
        }
      }
    }
    foreach ($strings as $key => $string) {
      $simplified = search_simplify($string);
      $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key.");
    }

    // Test the low-numbered ASCII control characters separately. They are not
    // in the text file because they are problematic for diff, especially \0.
    $string = '';
    for ($i = 0; $i < 32; $i++) {
      $string .= chr($i);
    }
    $this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.');
  }

  /**
   * Tests that search_simplify() does the right thing with punctuation.
   */
  function testSearchSimplifyPunctuation() {
    $cases = array(
      array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'),
      array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'),
      array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'),
      array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'),
    );

    foreach ($cases as $case) {
      $out = trim(search_simplify($case[0]));
      $this->assertEqual($out, $case[1], $case[2]);
    }
  }
}


/**
 * Tests keywords and conditions.
 */
class SearchKeywordsConditions extends DrupalWebTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Keywords and conditions',
      'description' => 'Verify the search pulls in keywords and extra conditions.',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search', 'search_extra_type');
    // Create searching user.
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));
    // Login with sufficient privileges.
    $this->drupalLogin($this->searching_user);
    // Test with all search modules enabled.
    variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'));
    menu_rebuild();
  }

  /**
   * Verify the kewords are captured and conditions respected.
   */
  function testSearchKeyswordsConditions() {
    // No keys, not conditions - no results.
    $this->drupalGet('search/dummy_path');
    $this->assertNoText('Dummy search snippet to display');
    // With keys - get results.
    $keys = 'bike shed ' . $this->randomName();
    $this->drupalGet("search/dummy_path/{$keys}");
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
    $keys = 'blue drop ' . $this->randomName();
    $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
    // Add some conditions and keys.
    $keys = 'moving drop ' . $this->randomName();
    $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys)));
    $this->assertText("Dummy search snippet to display.");
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
    // Add some conditions and no keys.
    $keys = 'drop kick ' . $this->randomName();
    $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys)));
    $this->assertText("Dummy search snippet to display.");
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
  }
}

/**
 * Tests that numbers can be searched.
 */
class SearchNumbersTestCase extends DrupalWebTestCase {
  protected $test_user;
  protected $numbers;
  protected $nodes;

  public static function getInfo() {
    return array(
      'name' => 'Search numbers',
      'description' => 'Check that numbers can be searched',
      'group' => 'Search',
    );
  }

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

    $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
    $this->drupalLogin($this->test_user);

    // Create content with various numbers in it.
    // Note: 50 characters is the current limit of the search index's word
    // field.
    $this->numbers = array(
      'ISBN' => '978-0446365383',
      'UPC' => '036000 291452',
      'EAN bar code' => '5901234123457',
      'negative' => '-123456.7890',
      'quoted negative' => '"-123456.7890"',
      'leading zero' => '0777777777',
      'tiny' => '111',
      'small' => '22222222222222',
      'medium' => '333333333333333333333333333',
      'large' => '444444444444444444444444444444444444444',
      'gigantic' => '5555555555555555555555555555555555555555555555555',
      'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
      'date', '01/02/2009',
      'commas', '987,654,321',
    );

    foreach ($this->numbers as $doc => $num) {
      $info = array(
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
        'type' => 'page',
        'language' => LANGUAGE_NONE,
        'title' => $doc . ' number',
      );
      $this->nodes[$doc] = $this->drupalCreateNode($info);
    }

    // Run cron to ensure the content is indexed.
    $this->cronRun();
    $this->drupalGet('admin/reports/dblog');
    $this->assertText(t('Cron run completed'), 'Log shows cron run completed');
  }

  /**
   * Tests that all the numbers can be searched.
   */
  function testNumberSearching() {
    $types = array_keys($this->numbers);

    foreach ($types as $type) {
      $number = $this->numbers[$type];
      // If the number is negative, remove the - sign, because - indicates
      // "not keyword" when searching.
      $number = ltrim($number, '-');
      $node = $this->nodes[$type];

      // Verify that the node title does not appear on the search page
      // with a dummy search.
      $this->drupalPost('search/node',
        array('keys' => 'foo'),
        t('Search'));
      $this->assertNoText($node->title, $type . ': node title not shown in dummy search');

      // Verify that the node title does appear as a link on the search page
      // when searching for the number.
      $this->drupalPost('search/node',
        array('keys' => $number),
        t('Search'));
      $this->assertText($node->title, format_string('%type: node title shown (search found the node) in search for number %number.', array('%type' => $type, '%number' => $number)));
    }
  }
}

/**
 * Tests that numbers can be searched, with more complex matching.
 */
class SearchNumberMatchingTestCase extends DrupalWebTestCase {
  protected $test_user;
  protected $numbers;
  protected $nodes;

  public static function getInfo() {
    return array(
      'name' => 'Search number matching',
      'description' => 'Check that numbers can be searched with more complex matching',
      'group' => 'Search',
    );
  }

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

    $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
    $this->drupalLogin($this->test_user);

    // Define a group of numbers that should all match each other --
    // numbers with internal punctuation should match each other, as well
    // as numbers with and without leading zeros and leading/trailing
    // . and -.
    $this->numbers = array(
      '123456789',
      '12/34/56789',
      '12.3456789',
      '12-34-56789',
      '123,456,789',
      '-123456789',
      '0123456789',
    );

    foreach ($this->numbers as $num) {
      $info = array(
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
        'type' => 'page',
        'language' => LANGUAGE_NONE,
      );
      $this->nodes[] = $this->drupalCreateNode($info);
    }

    // Run cron to ensure the content is indexed.
    $this->cronRun();
    $this->drupalGet('admin/reports/dblog');
    $this->assertText(t('Cron run completed'), 'Log shows cron run completed');
  }

  /**
   * Tests that all the numbers can be searched.
   */
  function testNumberSearching() {
    for ($i = 0; $i < count($this->numbers); $i++) {
      $node = $this->nodes[$i];

      // Verify that the node title does not appear on the search page
      // with a dummy search.
      $this->drupalPost('search/node',
        array('keys' => 'foo'),
        t('Search'));
      $this->assertNoText($node->title, format_string('%number: node title not shown in dummy search', array('%number' => $i)));

      // Now verify that we can find node i by searching for any of the
      // numbers.
      for ($j = 0; $j < count($this->numbers); $j++) {
        $number = $this->numbers[$j];
        // If the number is negative, remove the - sign, because - indicates
        // "not keyword" when searching.
        $number = ltrim($number, '-');

        $this->drupalPost('search/node',
          array('keys' => $number),
          t('Search'));
        $this->assertText($node->title, format_string('%i: node title shown (search found the node) in search for number %number', array('%i' => $i, '%number' => $number)));
      }
    }

  }
}

/**
 * Test config page.
 */
class SearchConfigSettingsForm extends DrupalWebTestCase {
  public $search_user;
  public $search_node;

  public static function getInfo() {
    return array(
      'name' => 'Config settings form',
      'description' => 'Verify the search config settings form.',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search', 'search_extra_type');

    // Login as a user that can create and search content.
    $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks'));
    $this->drupalLogin($this->search_user);

    // Add a single piece of content and index it.
    $node = $this->drupalCreateNode();
    $this->search_node = $node;
    // Link the node to itself to test that it's only indexed once. The content
    // also needs the word "pizza" so we can use it as the search keyword.
    $langcode = LANGUAGE_NONE;
    $body_key = "body[$langcode][0][value]";
    $edit[$body_key] = l($node->title, 'node/' . $node->nid) . ' pizza sandwich';
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));

    node_update_index();
    search_update_totals();

    // Enable the search block.
    $edit = array();
    $edit['blocks[search_form][region]'] = 'content';
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
  }

  /**
   * Verify the search settings form.
   */
  function testSearchSettingsPage() {

    // Test that the settings form displays the correct count of items left to index.
    $this->drupalGet('admin/config/search/settings');
    $this->assertText(t('There are @count items left to index.', array('@count' => 0)));

    // Test the re-index button.
    $this->drupalPost('admin/config/search/settings', array(), t('Re-index site'));
    $this->assertText(t('Are you sure you want to re-index the site'));
    $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site'));
    $this->assertText(t('The index will be rebuilt'));
    $this->drupalGet('admin/config/search/settings');
    $this->assertText(t('There is 1 item left to index.'));

    // Test that the form saves with the default values.
    $this->drupalPost('admin/config/search/settings', array(), t('Save configuration'));
    $this->assertText(t('The configuration options have been saved.'), 'Form saves with the default values.');

    // Test that the form does not save with an invalid word length.
    $edit = array(
      'minimum_word_size' => $this->randomName(3),
    );
    $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
    $this->assertNoText(t('The configuration options have been saved.'), 'Form does not save with an invalid word length.');
  }

  /**
   * Verify that you can disable individual search modules.
   */
  function testSearchModuleDisabling() {
    // Array of search modules to test: 'path' is the search path, 'title' is
    // the tab title, 'keys' are the keywords to search for, and 'text' is
    // the text to assert is on the results page.
    $module_info = array(
      'node' => array(
        'path' => 'node',
        'title' => 'Content',
        'keys' => 'pizza',
        'text' => $this->search_node->title,
      ),
      'user' => array(
        'path' => 'user',
        'title' => 'User',
        'keys' => $this->search_user->name,
        'text' => $this->search_user->mail,
      ),
      'search_extra_type' => array(
        'path' => 'dummy_path',
        'title' => 'Dummy search type',
        'keys' => 'foo',
        'text' => 'Dummy search snippet to display',
      ),
    );
    $modules = array_keys($module_info);

    // Test each module if it's enabled as the only search module.
    foreach ($modules as $module) {
      // Enable the one module and disable other ones.
      $info = $module_info[$module];
      $edit = array();
      foreach ($modules as $other) {
        $edit['search_active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE);
      }
      $edit['search_default_module'] = $module;
      $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));

      // Run a search from the correct search URL.
      $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']);
      $this->assertNoText('no results', $info['title'] . ' search found results');
      $this->assertText($info['text'], 'Correct search text found');

      // Verify that other module search tab titles are not visible.
      foreach ($modules as $other) {
        if ($other != $module) {
          $title = $module_info[$other]['title'];
          $this->assertNoText($title, $title . ' search tab is not shown');
        }
      }

      // Run a search from the search block on the node page. Verify you get
      // to this module's search results page.
      $terms = array('search_block_form' => $info['keys']);
      $this->drupalPost('node', $terms, t('Search'));
      $this->assertEqual(
        $this->getURL(),
        url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)),
        'Block redirected to right search page');

      // Try an invalid search path. Should redirect to our active module.
      $this->drupalGet('search/not_a_module_path');
      $this->assertEqual(
        $this->getURL(),
        url('search/' . $info['path'], array('absolute' => TRUE)),
        'Invalid search path redirected to default search page');
    }

    // Test with all search modules enabled. When you go to the search
    // page or run search, all modules should be shown.
    $edit = array();
    foreach ($modules as $module) {
      $edit['search_active_modules[' . $module . ']'] = $module;
    }
    $edit['search_default_module'] = 'node';

    $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));

    foreach (array('search/node/pizza', 'search/node') as $path) {
      $this->drupalGet($path);
      foreach ($modules as $module) {
        $title = $module_info[$module]['title'];
        $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
      }
    }
  }
}

/**
 * Tests the search_excerpt() function.
 */
class SearchExcerptTestCase extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Search excerpt extraction',
      'description' => 'Tests that the search_excerpt() function works.',
      'group' => 'Search',
    );
  }

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

  /**
   * Tests search_excerpt() with several simulated search keywords.
   *
   * Passes keywords and a sample marked up string, "The quick
   * brown fox jumps over the lazy dog", and compares it to the
   * correctly marked up string. The correctly marked up string
   * contains either highlighted keywords or the original marked
   * up string if no keywords matched the string.
   */
  function testSearchExcerpt() {
    // Make some text with entities and tags.
    $text = 'The <strong>quick</strong> <a href="#">brown</a> fox &amp; jumps <h2>over</h2> the lazy dog';
    // Note: The search_excerpt() function adds some extra spaces -- not
    // important for HTML formatting. Remove these for comparison.
    $expected = 'The quick brown fox &amp; jumps over the lazy dog';
    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
    $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string');

    $result = preg_replace('| +|', ' ', search_excerpt('fox', $text));
    $this->assertEqual($result, 'The quick brown <strong>fox</strong> &amp; jumps over the lazy dog ...', 'Found keyword is highlighted');

    $longtext = str_repeat($text . ' ', 10);
    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $longtext));
    $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');

    $entities = str_repeat('k&eacute;sz&iacute;t&eacute;se ', 20);
    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities));
    $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
    $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');

    // The node body that will produce this rendered $text is:
    // 123456789 HTMLTest +123456789+&lsquo;  +&lsquo;  +&lsquo;  +&lsquo;  +12345678  &nbsp;&nbsp;  +&lsquo;  +&lsquo;  +&lsquo;   &lsquo;
    $text = "<div class=\"field field-name-body field-type-text-with-summary field-label-hidden\"><div class=\"field-items\"><div class=\"field-item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘  +‘  +‘  +‘  +12345678      +‘  +‘  +‘   ‘</p>\n</div></div></div> ";
    $result = search_excerpt('HTMLTest', $text);
    $this->assertFalse(empty($result),  'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
  }

  /**
   * Tests search_excerpt() with search keywords matching simplified words.
   *
   * Excerpting should handle keywords that are matched only after going through
   * search_simplify(). This test passes keywords that match simplified words
   * and compares them with strings that contain the original unsimplified word.
   */
  function testSearchExcerptSimplified() {
    $lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.';
    $lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.';

    // Make some text with some keywords that will get simplified.
    $text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
    // Note: The search_excerpt() function adds some extra spaces -- not
    // important for HTML formatting. Remove these for comparison.
    $result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text));
    $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');

    $result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text));
    $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');

    $result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text));
    $this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');

    $result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text));
    $this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');

    $result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text));
    $this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');

    $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text));
    $this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');

    // Test phrases with characters which are being truncated.
    $result = preg_replace('| +|', ' ', search_excerpt('"ipsum _"', $text));
    $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');

    $result = preg_replace('| +|', ' ', search_excerpt('"ipsum 0000"', $text));
    $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');

    // Test combination of the valid keyword and keyword containing only
    // characters which are being truncated during simplification.
    $result = preg_replace('| +|', ' ', search_excerpt('ipsum _', $text));
    $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');

    $result = preg_replace('| +|', ' ', search_excerpt('ipsum 0000', $text));
    $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
  }
}

/**
 * Test the CJK tokenizer.
 */
class SearchTokenizerTestCase extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'CJK tokenizer',
      'description' => 'Check that CJK tokenizer works as intended.',
      'group' => 'Search',
    );
  }

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

  /**
   * Verifies that strings of CJK characters are tokenized.
   *
   * The search_simplify() function does special things with numbers, symbols,
   * and punctuation. So we only test that CJK characters that are not in these
   * character classes are tokenized properly. See PREG_CLASS_CKJ for more
   * information.
   */
  function testTokenizer() {
    // Set the minimum word size to 1 (to split all CJK characters) and make
    // sure CJK tokenizing is turned on.
    variable_set('minimum_word_size', 1);
    variable_set('overlap_cjk', TRUE);
    $this->refreshVariables();

    // Create a string of CJK characters from various character ranges in
    // the Unicode tables.

    // Beginnings of the character ranges.
    $starts = array(
      'CJK unified' => 0x4e00,
      'CJK Ext A' => 0x3400,
      'CJK Compat' => 0xf900,
      'Hangul Jamo' => 0x1100,
      'Hangul Ext A' => 0xa960,
      'Hangul Ext B' => 0xd7b0,
      'Hangul Compat' => 0x3131,
      'Half non-punct 1' => 0xff21,
      'Half non-punct 2' => 0xff41,
      'Half non-punct 3' => 0xff66,
      'Hangul Syllables' => 0xac00,
      'Hiragana' => 0x3040,
      'Katakana' => 0x30a1,
      'Katakana Ext' => 0x31f0,
      'CJK Reserve 1' => 0x20000,
      'CJK Reserve 2' => 0x30000,
      'Bomofo' => 0x3100,
      'Bomofo Ext' => 0x31a0,
      'Lisu' => 0xa4d0,
      'Yi' => 0xa000,
    );

    // Ends of the character ranges.
    $ends = array(
      'CJK unified' => 0x9fcf,
      'CJK Ext A' => 0x4dbf,
      'CJK Compat' => 0xfaff,
      'Hangul Jamo' => 0x11ff,
      'Hangul Ext A' => 0xa97f,
      'Hangul Ext B' => 0xd7ff,
      'Hangul Compat' => 0x318e,
      'Half non-punct 1' => 0xff3a,
      'Half non-punct 2' => 0xff5a,
      'Half non-punct 3' => 0xffdc,
      'Hangul Syllables' => 0xd7af,
      'Hiragana' => 0x309f,
      'Katakana' => 0x30ff,
      'Katakana Ext' => 0x31ff,
      'CJK Reserve 1' => 0x2fffd,
      'CJK Reserve 2' => 0x3fffd,
      'Bomofo' => 0x312f,
      'Bomofo Ext' => 0x31b7,
      'Lisu' => 0xa4fd,
      'Yi' => 0xa48f,
    );

    // Generate characters consisting of starts, midpoints, and ends.
    $chars = array();
    $charcodes = array();
    foreach ($starts as $key => $value) {
      $charcodes[] = $starts[$key];
      $chars[] = $this->code2utf($starts[$key]);
      $mid = round(0.5 * ($starts[$key] + $ends[$key]));
      $charcodes[] = $mid;
      $chars[] = $this->code2utf($mid);
      $charcodes[] = $ends[$key];
      $chars[] = $this->code2utf($ends[$key]);
    }

    // Merge into a string and tokenize.
    $string = implode('', $chars);
    $out = trim(search_simplify($string));
    $expected = drupal_strtolower(implode(' ', $chars));

    // Verify that the output matches what we expect.
    $this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
  }

  /**
   * Verifies that strings of non-CJK characters are not tokenized.
   *
   * This is just a sanity check - it verifies that strings of letters are
   * not tokenized.
   */
  function testNoTokenizer() {
    // Set the minimum word size to 1 (to split all CJK characters) and make
    // sure CJK tokenizing is turned on.
    variable_set('minimum_word_size', 1);
    variable_set('overlap_cjk', TRUE);
    $this->refreshVariables();

    $letters = 'abcdefghijklmnopqrstuvwxyz';
    $out = trim(search_simplify($letters));

    $this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
  }

  /**
   * Like PHP chr() function, but for unicode characters.
   *
   * chr() only works for ASCII characters up to character 255. This function
   * converts a number to the corresponding unicode character. Adapted from
   * functions supplied in comments on several functions on php.net.
   */
  function code2utf($num) {
    if ($num < 128) {
      return chr($num);
    }

    if ($num < 2048) {
      return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
    }

    if ($num < 65536) {
      return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
    }

    if ($num < 2097152) {
      return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
    }

    return '';
  }
}

/**
 * Tests that we can embed a form in search results and submit it.
 */
class SearchEmbedForm extends DrupalWebTestCase {
  /**
   * Node used for testing.
   */
  public $node;

  /**
   * Count of how many times the form has been submitted.
   */
  public $submit_count = 0;

  public static function getInfo() {
    return array(
      'name' => 'Embedded forms',
      'description' => 'Verifies that a form embedded in search results works',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search', 'search_embedded_form');

    // Create a user and a node, and update the search index.
    $test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes'));
    $this->drupalLogin($test_user);

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

    node_update_index();
    search_update_totals();

    // Set up a dummy initial count of times the form has been submitted.
    $this->submit_count = 12;
    variable_set('search_embedded_form_submitted', $this->submit_count);
    $this->refreshVariables();
  }

  /**
   * Tests that the embedded form appears and can be submitted.
   */
  function testEmbeddedForm() {
    // First verify we can submit the form from the module's page.
    $this->drupalPost('search_embedded_form',
      array('name' => 'John'),
      t('Send away'));
    $this->assertText(t('Test form was submitted'), 'Form message appears');
    $count = variable_get('search_embedded_form_submitted', 0);
    $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct');
    $this->submit_count = $count;

    // Now verify that we can see and submit the form from the search results.
    $this->drupalGet('search/node/' . $this->node->title);
    $this->assertText(t('Your name'), 'Form is visible');
    $this->drupalPost('search/node/' . $this->node->title,
      array('name' => 'John'),
      t('Send away'));
    $this->assertText(t('Test form was submitted'), 'Form message appears');
    $count = variable_get('search_embedded_form_submitted', 0);
    $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct');
    $this->submit_count = $count;

    // Now verify that if we submit the search form, it doesn't count as
    // our form being submitted.
    $this->drupalPost('search',
      array('keys' => 'foo'),
      t('Search'));
    $this->assertNoText(t('Test form was submitted'), 'Form message does not appear');
    $count = variable_get('search_embedded_form_submitted', 0);
    $this->assertEqual($this->submit_count, $count, 'Form submission count is correct');
    $this->submit_count = $count;
  }
}

/**
 * Tests that hook_search_page runs.
 */
class SearchPageOverride extends DrupalWebTestCase {
  public $search_user;

  public static function getInfo() {
    return array(
      'name' => 'Search page override',
      'description' => 'Verify that hook_search_page can override search page display.',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search', 'search_extra_type');

    // Login as a user that can create and search content.
    $this->search_user = $this->drupalCreateUser(array('search content', 'administer search'));
    $this->drupalLogin($this->search_user);

    // Enable the extra type module for searching.
    variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'));
    menu_rebuild();
  }

  function testSearchPageHook() {
    $keys = 'bike shed ' . $this->randomName();
    $this->drupalGet("search/dummy_path/{$keys}");
    $this->assertText('Dummy search snippet', 'Dummy search snippet is shown');
    $this->assertText('Test page text is here', 'Page override is working');
  }
}

/**
 * Test node search with multiple languages.
 */
class SearchLanguageTestCase extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Search language selection',
      'description' => 'Tests advanced search with different languages enabled.',
      'group' => 'Search',
    );
  }

  /**
   * Implementation setUp().
   */
  function setUp() {
    parent::setUp('search', 'locale');

    // Create and login user.
    $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages'));
    $this->drupalLogin($test_user);
  }

  function testLanguages() {
    // Check that there are initially no languages displayed.
    $this->drupalGet('search/node');
    $this->assertNoText(t('Languages'), 'No languages to choose from.');

    // Add predefined language.
    $edit = array('langcode' => 'fr');
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
    $this->assertText('fr', 'Language added successfully.');

    // Now we should have languages displayed.
    $this->drupalGet('search/node');
    $this->assertText(t('Languages'), 'Languages displayed to choose from.');
    $this->assertText(t('English'), 'English is a possible choice.');
    $this->assertText(t('French'), 'French is a possible choice.');

    // Ensure selecting no language does not make the query different.
    $this->drupalPost('search/node', array(), t('Advanced search'));
    $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), 'Correct page redirection, no language filtering.');

    // Pick French and ensure it is selected.
    $edit = array('language[fr]' => TRUE);
    $this->drupalPost('search/node', $edit, t('Advanced search'));
    $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', 'Language filter added to query.');

    // Change the default language and disable English.
    $path = 'admin/config/regional/language';
    $this->drupalGet($path);
    $this->assertFieldChecked('edit-site-default-en', 'English is the default language.');
    $edit = array('site_default' => 'fr');
    $this->drupalPost(NULL, $edit, t('Save configuration'));
    $this->assertNoFieldChecked('edit-site-default-en', 'Default language updated.');
    $edit = array('enabled[en]' => FALSE);
    $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration'));
    $this->assertNoFieldChecked('edit-enabled-en', 'Language disabled.');

    // Check that there are again no languages displayed.
    $this->drupalGet('search/node');
    $this->assertNoText(t('Languages'), 'No languages to choose from.');
  }
}

/**
 * Tests node search with node access control.
 */
class SearchNodeAccessTest extends DrupalWebTestCase {
  public $test_user;

  public static function getInfo() {
    return array(
      'name' => 'Search and node access',
      'description' => 'Tests search functionality with node access control.',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search', 'node_access_test');
    node_access_rebuild();

    // Create a test user and log in.
    $this->test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search'));
    $this->drupalLogin($this->test_user);
  }

  /**
   * Tests that search works with punctuation and HTML entities.
   */
  function testPhraseSearchPunctuation() {
    $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy.")))));
    $node2 = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'Dignissim Aliquam &amp; Quieligo meus natu quae quia te. Damnum&copy; erat&mdash; neo pneum. Facilisi feugiat ibidem ratis.')))));

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

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

    // Submit a phrase wrapped in double quotes to include the punctuation.
    $edit = array('keys' => '"bunny\'s"');
    $this->drupalPost('search/node', $edit, t('Search'));
    $this->assertText($node->title);

    // Search for "&" and verify entities are not broken up in the output.
    $edit = array('keys' => '&');
    $this->drupalPost('search/node', $edit, t('Search'));
    $this->assertNoRaw('<strong>&</strong>amp;');
    $this->assertText('You must include at least one positive keyword');

    $edit = array('keys' => '&amp;');
    $this->drupalPost('search/node', $edit, t('Search'));
    $this->assertNoRaw('<strong>&</strong>amp;');
    $this->assertText('You must include at least one positive keyword');
  }
}

/**
 * Tests node search with query tags.
 */
class SearchNodeTagTest extends DrupalWebTestCase {
  public $test_user;

  public static function getInfo() {
    return array(
      'name' => 'Node search query tags',
      'description' => 'Tests Node search tags functionality.',
      'group' => 'Search',
    );
  }

  function setUp() {
    parent::setUp('search', 'search_node_tags');
    node_access_rebuild();

    // Create a test user and log in.
    $this->test_user = $this->drupalCreateUser(array('search content'));
    $this->drupalLogin($this->test_user);
  }

  /**
   * Tests that the correct tags are available and hooks invoked.
   */
  function testNodeSearchQueryTags() {
    $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'testing testing testing.')))));

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

    $edit = array('keys' => 'testing');
    $this->drupalPost('search/node', $edit, t('Search'));

    $this->assertTrue(variable_get('search_node_tags_test_query_tag', FALSE), 'hook_query_alter() was invoked and the query contained the "search_node" tag.');
    $this->assertTrue(variable_get('search_node_tags_test_query_tag_hook', FALSE), 'hook_query_search_node_alter() was invoked.');
  }
}

/**
 * Tests searching with locale values set.
 */
class SearchSetLocaleTest extends DrupalWebTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Search with numeric locale set',
      'description' => 'Check that search works with numeric locale settings',
      'group' => 'Search',
    );
  }

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

    // Create a simple node so something will be put in the index.
    $info = array(
      'body' => array(LANGUAGE_NONE => array(array('value' => 'Tapir'))),
    );
    $this->drupalCreateNode($info);

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

  /**
   * Verify that search works with a numeric locale set.
   */
  public function testSearchWithNumericLocale() {
    // French decimal point is comma.
    setlocale(LC_NUMERIC, 'fr_FR');

    // An exception will be thrown if a float in the wrong format occurs in the
    // query to the database, so an assertion is not necessary here.
    db_select('search_index', 'i')
      ->extend('searchquery')
      ->searchexpression('tapir', 'node')
      ->execute();
  }
}
