Project

General

Profile

Paste
Download (81.1 KB) Statistics
| Branch: | Revision:

root / drupal7 / modules / search / search.test @ 27e02aed

1
<?php
2

    
3
/**
4
 * @file
5
 * Tests for search.module.
6
 */
7

    
8
// The search index can contain different types of content. Typically the type is 'node'.
9
// Here we test with _test_ and _test2_ as the type.
10
define('SEARCH_TYPE', '_test_');
11
define('SEARCH_TYPE_2', '_test2_');
12
define('SEARCH_TYPE_JPN', '_test3_');
13

    
14
/**
15
 * Indexes content and queries it.
16
 */
17
class SearchMatchTestCase extends DrupalWebTestCase {
18
  public static function getInfo() {
19
    return array(
20
      'name' => 'Search engine queries',
21
      'description' => 'Indexes content and queries it.',
22
      'group' => 'Search',
23
    );
24
  }
25

    
26
  /**
27
   * Implementation setUp().
28
   */
29
  function setUp() {
30
    parent::setUp('search');
31
  }
32

    
33
  /**
34
   * Test search indexing.
35
   */
36
  function testMatching() {
37
    $this->_setup();
38
    $this->_testQueries();
39
  }
40

    
41
  /**
42
   * Set up a small index of items to test against.
43
   */
44
  function _setup() {
45
    variable_set('minimum_word_size', 3);
46

    
47
    for ($i = 1; $i <= 7; ++$i) {
48
      search_index($i, SEARCH_TYPE, $this->getText($i));
49
    }
50
    for ($i = 1; $i <= 5; ++$i) {
51
      search_index($i + 7, SEARCH_TYPE_2, $this->getText2($i));
52
    }
53
    // No getText builder function for Japanese text; just a simple array.
54
    foreach (array(
55
      13 => '以呂波耳・ほへとち。リヌルヲ。',
56
      14 => 'ドルーパルが大好きよ!',
57
      15 => 'コーヒーとケーキ',
58
    ) as $i => $jpn) {
59
      search_index($i, SEARCH_TYPE_JPN, $jpn);
60
    }
61
    search_update_totals();
62
  }
63

    
64
  /**
65
   * _test_: Helper method for generating snippets of content.
66
   *
67
   * Generated items to test against:
68
   *   1  ipsum
69
   *   2  dolore sit
70
   *   3  sit am ut
71
   *   4  am ut enim am
72
   *   5  ut enim am minim veniam
73
   *   6  enim am minim veniam es cillum
74
   *   7  am minim veniam es cillum dolore eu
75
   */
76
  function getText($n) {
77
    $words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
78
    return implode(' ', array_slice($words, $n - 1, $n));
79
  }
80

    
81
  /**
82
   * _test2_: Helper method for generating snippets of content.
83
   *
84
   * Generated items to test against:
85
   *   8  dear
86
   *   9  king philip
87
   *   10 philip came over
88
   *   11 came over from germany
89
   *   12 over from germany swimming
90
   */
91
  function getText2($n) {
92
    $words = explode(' ', "Dear King Philip came over from Germany swimming.");
93
    return implode(' ', array_slice($words, $n - 1, $n));
94
  }
95

    
96
  /**
97
   * Run predefine queries looking for indexed terms.
98
   */
99
  function _testQueries() {
100
    /*
101
      Note: OR queries that include short words in OR groups are only accepted
102
      if the ORed terms are ANDed with at least one long word in the rest of the query.
103

    
104
      e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good
105
      e.g. dolore OR ut = (dolore) OR (ut) -> bad
106

    
107
      This is a design limitation to avoid full table scans.
108
    */
109
    $queries = array(
110
      // Simple AND queries.
111
      'ipsum' => array(1),
112
      'enim' => array(4, 5, 6),
113
      'xxxxx' => array(),
114
      'enim minim' => array(5, 6),
115
      'enim xxxxx' => array(),
116
      'dolore eu' => array(7),
117
      'dolore xx' => array(),
118
      'ut minim' => array(5),
119
      'xx minim' => array(),
120
      'enim veniam am minim ut' => array(5),
121
      // Simple OR queries.
122
      'dolore OR ipsum' => array(1, 2, 7),
123
      'dolore OR xxxxx' => array(2, 7),
124
      'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7),
125
      'ipsum OR dolore sit OR cillum' => array(2, 7),
126
      'minim dolore OR ipsum' => array(7),
127
      'dolore OR ipsum veniam' => array(7),
128
      'minim dolore OR ipsum OR enim' => array(5, 6, 7),
129
      'dolore xx OR yy' => array(),
130
      'xxxxx dolore OR ipsum' => array(),
131
      // Negative queries.
132
      'dolore -sit' => array(7),
133
      'dolore -eu' => array(2),
134
      'dolore -xxxxx' => array(2, 7),
135
      'dolore -xx' => array(2, 7),
136
      // Phrase queries.
137
      '"dolore sit"' => array(2),
138
      '"sit dolore"' => array(),
139
      '"am minim veniam es"' => array(6, 7),
140
      '"minim am veniam es"' => array(),
141
      // Mixed queries.
142
      '"am minim veniam es" OR dolore' => array(2, 6, 7),
143
      '"minim am veniam es" OR "dolore sit"' => array(2),
144
      '"minim am veniam es" OR "sit dolore"' => array(),
145
      '"am minim veniam es" -eu' => array(6),
146
      '"am minim veniam" -"cillum dolore"' => array(5, 6),
147
      '"am minim veniam" -"dolore cillum"' => array(5, 6, 7),
148
      'xxxxx "minim am veniam es" OR dolore' => array(),
149
      'xx "minim am veniam es" OR dolore' => array()
150
    );
151
    foreach ($queries as $query => $results) {
152
      $result = db_select('search_index', 'i')
153
        ->extend('SearchQuery')
154
        ->searchExpression($query, SEARCH_TYPE)
155
        ->execute();
156

    
157
      $set = $result ? $result->fetchAll() : array();
158
      $this->_testQueryMatching($query, $set, $results);
159
      $this->_testQueryScores($query, $set, $results);
160
    }
161

    
162
    // These queries are run against the second index type, SEARCH_TYPE_2.
163
    $queries = array(
164
      // Simple AND queries.
165
      'ipsum' => array(),
166
      'enim' => array(),
167
      'enim minim' => array(),
168
      'dear' => array(8),
169
      'germany' => array(11, 12),
170
    );
171
    foreach ($queries as $query => $results) {
172
      $result = db_select('search_index', 'i')
173
        ->extend('SearchQuery')
174
        ->searchExpression($query, SEARCH_TYPE_2)
175
        ->execute();
176

    
177
      $set = $result ? $result->fetchAll() : array();
178
      $this->_testQueryMatching($query, $set, $results);
179
      $this->_testQueryScores($query, $set, $results);
180
    }
181

    
182
    // These queries are run against the third index type, SEARCH_TYPE_JPN.
183
    $queries = array(
184
      // Simple AND queries.
185
      '呂波耳' => array(13),
186
      '以呂波耳' => array(13),
187
      'ほへと ヌルヲ' => array(13),
188
      'とちリ' => array(),
189
      'ドルーパル' => array(14),
190
      'パルが大' => array(14),
191
      'コーヒー' => array(15),
192
      'ヒーキ' => array(),
193
    );
194
    foreach ($queries as $query => $results) {
195
      $result = db_select('search_index', 'i')
196
        ->extend('SearchQuery')
197
        ->searchExpression($query, SEARCH_TYPE_JPN)
198
        ->execute();
199

    
200
      $set = $result ? $result->fetchAll() : array();
201
      $this->_testQueryMatching($query, $set, $results);
202
      $this->_testQueryScores($query, $set, $results);
203
    }
204
  }
205

    
206
  /**
207
   * Test the matching abilities of the engine.
208
   *
209
   * Verify if a query produces the correct results.
210
   */
211
  function _testQueryMatching($query, $set, $results) {
212
    // Get result IDs.
213
    $found = array();
214
    foreach ($set as $item) {
215
      $found[] = $item->sid;
216
    }
217

    
218
    // Compare $results and $found.
219
    sort($found);
220
    sort($results);
221
    $this->assertEqual($found, $results, "Query matching '$query'");
222
  }
223

    
224
  /**
225
   * Test the scoring abilities of the engine.
226
   *
227
   * Verify if a query produces normalized, monotonous scores.
228
   */
229
  function _testQueryScores($query, $set, $results) {
230
    // Get result scores.
231
    $scores = array();
232
    foreach ($set as $item) {
233
      $scores[] = $item->calculated_score;
234
    }
235

    
236
    // Check order.
237
    $sorted = $scores;
238
    sort($sorted);
239
    $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
240

    
241
    // Check range.
242
    $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
243
  }
244
}
245

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

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

    
260
  function setUp() {
261
    parent::setUp('search');
262

    
263
    // Create user.
264
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles'));
265
  }
266

    
267
  /**
268
   * Tests the failed search text, and various other text on the search page.
269
   */
270
  function testSearchText() {
271
    $this->drupalLogin($this->searching_user);
272
    $this->drupalGet('search/node');
273
    $this->assertText(t('Enter your keywords'));
274
    $this->assertText(t('Search'));
275
    $title = t('Search') . ' | Drupal';
276
    $this->assertTitle($title, 'Search page title is correct');
277

    
278
    $edit = array();
279
    $edit['keys'] = 'bike shed ' . $this->randomName();
280
    $this->drupalPost('search/node', $edit, t('Search'));
281
    $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.');
282
    $this->assertText(t('Search'));
283
    $this->assertTitle($title, 'Search page title is correct');
284

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

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

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

    
313
/**
314
 * Indexes content and tests the advanced search form.
315
 */
316
class SearchAdvancedSearchForm extends DrupalWebTestCase {
317
  protected $node;
318

    
319
  public static function getInfo() {
320
    return array(
321
      'name' => 'Advanced search form',
322
      'description' => 'Indexes content and tests the advanced search form.',
323
      'group' => 'Search',
324
    );
325
  }
326

    
327
  function setUp() {
328
    parent::setUp('search');
329
    // Create and login user.
330
    $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes'));
331
    $this->drupalLogin($test_user);
332

    
333
    // Create initial node.
334
    $node = $this->drupalCreateNode();
335
    $this->node = $this->drupalCreateNode();
336

    
337
    // First update the index. This does the initial processing.
338
    node_update_index();
339

    
340
    // Then, run the shutdown function. Testing is a unique case where indexing
341
    // and searching has to happen in the same request, so running the shutdown
342
    // function manually is needed to finish the indexing process.
343
    search_update_totals();
344
  }
345

    
346
  /**
347
   * Test using the search form with GET and POST queries.
348
   * Test using the advanced search form to limit search to nodes of type "Basic page".
349
   */
350
  function testNodeType() {
351
    $this->assertTrue($this->node->type == 'page', 'Node type is Basic page.');
352

    
353
    // Assert that the dummy title doesn't equal the real title.
354
    $dummy_title = 'Lorem ipsum';
355
    $this->assertNotEqual($dummy_title, $this->node->title, "Dummy title doesn't equal node title");
356

    
357
    // Search for the dummy title with a GET query.
358
    $this->drupalGet('search/node/' . $dummy_title);
359
    $this->assertNoText($this->node->title, 'Basic page node is not found with dummy title.');
360

    
361
    // Search for the title of the node with a GET query.
362
    $this->drupalGet('search/node/' . $this->node->title);
363
    $this->assertText($this->node->title, 'Basic page node is found with GET query.');
364

    
365
    // Search for the title of the node with a POST query.
366
    $edit = array('or' => $this->node->title);
367
    $this->drupalPost('search/node', $edit, t('Advanced search'));
368
    $this->assertText($this->node->title, 'Basic page node is found with POST query.');
369

    
370
    // Advanced search type option.
371
    $this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search'));
372
    $this->assertText($this->node->title, 'Basic page node is found with POST query and type:page.');
373

    
374
    $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search'));
375
    $this->assertText('bike shed', 'Article node is not found with POST query and type:article.');
376
  }
377
}
378

    
379
/**
380
 * Indexes content and tests ranking factors.
381
 */
382
class SearchRankingTestCase extends DrupalWebTestCase {
383
  public static function getInfo() {
384
    return array(
385
      'name' => 'Search engine ranking',
386
      'description' => 'Indexes content and tests ranking factors.',
387
      'group' => 'Search',
388
    );
389
  }
390

    
391
  /**
392
   * Implementation setUp().
393
   */
394
  function setUp() {
395
    parent::setUp('search', 'statistics', 'comment');
396
  }
397

    
398
  function testRankings() {
399
    // Login with sufficient privileges.
400
    $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content')));
401

    
402
    // Build a list of the rankings to test.
403
    $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
404

    
405
    // Create nodes for testing.
406
    foreach ($node_ranks as $node_rank) {
407
      $settings = array(
408
        'type' => 'page',
409
        'title' => 'Drupal rocks',
410
        'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
411
      );
412
      foreach (array(0, 1) as $num) {
413
        if ($num == 1) {
414
          switch ($node_rank) {
415
            case 'sticky':
416
            case 'promote':
417
              $settings[$node_rank] = 1;
418
              break;
419
            case 'relevance':
420
              $settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks";
421
              break;
422
            case 'recent':
423
              $settings['created'] = REQUEST_TIME + 3600;
424
              break;
425
            case 'comments':
426
              $settings['comment'] = 2;
427
              break;
428
          }
429
        }
430
        $nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
431
      }
432
    }
433

    
434
    // Update the search index.
435
    module_invoke_all('update_index');
436
    search_update_totals();
437

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

    
441
    // Add a comment to one of the nodes.
442
    $edit = array();
443
    $edit['subject'] = 'my comment title';
444
    $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment';
445
    $this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid);
446
    $this->drupalPost(NULL, $edit, t('Preview'));
447
    $this->drupalPost(NULL, $edit, t('Save'));
448

    
449
    // Enable counting of statistics.
450
    variable_set('statistics_count_content_views', 1);
451

    
452
    // Then View one of the nodes a bunch of times.
453
    for ($i = 0; $i < 5; $i ++) {
454
      $this->drupalGet('node/' . $nodes['views'][1]->nid);
455
    }
456

    
457
    // Test each of the possible rankings.
458
    foreach ($node_ranks as $node_rank) {
459
      // Disable all relevancy rankings except the one we are testing.
460
      foreach ($node_ranks as $var) {
461
        variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0);
462
      }
463

    
464
      // Do the search and assert the results.
465
      $set = node_search_execute('rocks');
466
      $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
467
    }
468
  }
469

    
470
  /**
471
   * Test rankings of HTML tags.
472
   */
473
  function testHTMLRankings() {
474
    // Login with sufficient privileges.
475
    $this->drupalLogin($this->drupalCreateUser(array('create page content')));
476

    
477
    // Test HTML tags with different weights.
478
    $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag');
479
    $shuffled_tags = $sorted_tags;
480

    
481
    // Shuffle tags to ensure HTML tags are ranked properly.
482
    shuffle($shuffled_tags);
483
    $settings = array(
484
      'type' => 'page',
485
      'title' => 'Simple node',
486
    );
487
    foreach ($shuffled_tags as $tag) {
488
      switch ($tag) {
489
        case 'a':
490
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 'full_html')));
491
          break;
492
        case 'notag':
493
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks')));
494
          break;
495
        default:
496
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html')));
497
          break;
498
      }
499
      $nodes[$tag] = $this->drupalCreateNode($settings);
500
    }
501

    
502
    // Update the search index.
503
    module_invoke_all('update_index');
504
    search_update_totals();
505

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

    
509
    // Disable all other rankings.
510
    $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views');
511
    foreach ($node_ranks as $node_rank) {
512
      variable_set('node_rank_' . $node_rank, 0);
513
    }
514
    $set = node_search_execute('rocks');
515

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

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

    
532
      // Update the search index.
533
      module_invoke_all('update_index');
534
      search_update_totals();
535

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

    
539
      $set = node_search_execute('rocks');
540

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

    
544
      // Assert the results.
545
      $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "&lt;' . $tag . '&gt;" order.');
546

    
547
      // Delete node so it doesn't show up in subsequent search results.
548
      node_delete($node->nid);
549
    }
550
  }
551

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

    
561
    // See testRankings() above - build a node that will rank high for sticky.
562
    $settings = array(
563
      'type' => 'page',
564
      'title' => 'Drupal rocks',
565
      'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
566
      'sticky' => 1,
567
    );
568

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

    
571
    // Update the search index.
572
    module_invoke_all('update_index');
573
    search_update_totals();
574

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

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

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

    
592
/**
593
 * Tests the rendering of the search block.
594
 */
595
class SearchBlockTestCase extends DrupalWebTestCase {
596
  public static function getInfo() {
597
    return array(
598
      'name' => 'Block availability',
599
      'description' => 'Check if the search form block is available.',
600
      'group' => 'Search',
601
    );
602
  }
603

    
604
  function setUp() {
605
    parent::setUp('search');
606

    
607
    // Create and login user
608
    $admin_user = $this->drupalCreateUser(array('administer blocks', 'search content'));
609
    $this->drupalLogin($admin_user);
610
  }
611

    
612
  function testSearchFormBlock() {
613
    // Set block title to confirm that the interface is available.
614
    $this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), t('Save block'));
615
    $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.');
616

    
617
    // Set the block to a region to confirm block is available.
618
    $edit = array();
619
    $edit['blocks[search_form][region]'] = 'footer';
620
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
621
    $this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.');
622
  }
623

    
624
  /**
625
   * Test that the search block form works correctly.
626
   */
627
  function testBlock() {
628
    // Enable the block, and place it in the 'content' region so that it isn't
629
    // hidden on 404 pages.
630
    $edit = array('blocks[search_form][region]' => 'content');
631
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
632

    
633
    // Test a normal search via the block form, from the front page.
634
    $terms = array('search_block_form' => 'test');
635
    $this->drupalPost('node', $terms, t('Search'));
636
    $this->assertText('Your search yielded no results');
637

    
638
    // Test a search from the block on a 404 page.
639
    $this->drupalGet('foo');
640
    $this->assertResponse(404);
641
    $this->drupalPost(NULL, $terms, t('Search'));
642
    $this->assertResponse(200);
643
    $this->assertText('Your search yielded no results');
644

    
645
    // Test a search from the block when it doesn't appear on the search page.
646
    $edit = array('pages' => 'search');
647
    $this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block'));
648
    $this->drupalPost('node', $terms, t('Search'));
649
    $this->assertText('Your search yielded no results');
650

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

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

    
663
    // Confirm that the user is redirected to the search page, when form is submitted empty.
664
    $this->assertEqual(
665
      $this->getUrl(),
666
      url('search/node/', array('absolute' => TRUE)),
667
      'Redirected to correct url.'
668
    );
669

    
670
    // Test that after entering a too-short keyword in the form, you can then
671
    // search again with a longer keyword. First test using the block form.
672
    $terms = array('search_block_form' => 'a');
673
    $this->drupalPost('node', $terms, t('Search'));
674
    $this->assertText('You must include at least one positive keyword with 3 characters or more');
675
    $terms = array('search_block_form' => 'foo');
676
    $this->drupalPost(NULL, $terms, t('Search'));
677
    $this->assertNoText('You must include at least one positive keyword with 3 characters or more');
678
    $this->assertText('Your search yielded no results');
679

    
680
    // Same test again, using the search page form for the second search this time.
681
    $terms = array('search_block_form' => 'a');
682
    $this->drupalPost('node', $terms, t('Search'));
683
    $terms = array('keys' => 'foo');
684
    $this->drupalPost(NULL, $terms, t('Search'));
685
    $this->assertNoText('You must include at least one positive keyword with 3 characters or more');
686
    $this->assertText('Your search yielded no results');
687
  }
688
}
689

    
690
/**
691
 * Tests that searching for a phrase gets the correct page count.
692
 */
693
class SearchExactTestCase extends DrupalWebTestCase {
694
  public static function getInfo() {
695
    return array(
696
      'name' => 'Search engine phrase queries',
697
      'description' => 'Tests that searching for a phrase gets the correct page count.',
698
      'group' => 'Search',
699
    );
700
  }
701

    
702
  function setUp() {
703
    parent::setUp('search');
704
  }
705

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

    
713
    $settings = array(
714
      'type' => 'page',
715
      'title' => 'Simple Node',
716
    );
717
    // Create nodes with exact phrase.
718
    for ($i = 0; $i <= 17; $i++) {
719
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza')));
720
      $this->drupalCreateNode($settings);
721
    }
722
    // Create nodes containing keywords.
723
    for ($i = 0; $i <= 17; $i++) {
724
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza')));
725
      $this->drupalCreateNode($settings);
726
    }
727

    
728
    // Update the search index.
729
    module_invoke_all('update_index');
730
    search_update_totals();
731

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

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

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

    
751
/**
752
 * Test integration searching comments.
753
 */
754
class SearchCommentTestCase extends DrupalWebTestCase {
755
  protected $admin_user;
756

    
757
  public static function getInfo() {
758
    return array(
759
      'name' => 'Comment Search tests',
760
      'description' => 'Test integration searching comments.',
761
      'group' => 'Search',
762
    );
763
  }
764

    
765
  function setUp() {
766
    parent::setUp('comment', 'search');
767

    
768
    // Create and log in an administrative user having access to the Full HTML
769
    // text format.
770
    $full_html_format = filter_format_load('full_html');
771
    $permissions = array(
772
      'administer filters',
773
      filter_permission_name($full_html_format),
774
      'administer permissions',
775
      'create page content',
776
      'skip comment approval',
777
      'access comments',
778
    );
779
    $this->admin_user = $this->drupalCreateUser($permissions);
780
    $this->drupalLogin($this->admin_user);
781
  }
782

    
783
  /**
784
   * Verify that comments are rendered using proper format in search results.
785
   */
786
  function testSearchResultsComment() {
787
    $comment_body = 'Test comment body';
788

    
789
    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
790
    // Enable check_plain() for 'Filtered HTML' text format.
791
    $filtered_html_format_id = 'filtered_html';
792
    $edit = array(
793
      'filters[filter_html_escape][status]' => TRUE,
794
    );
795
    $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration'));
796
    // Allow anonymous users to search content.
797
    $edit = array(
798
      DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
799
      DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
800
      DRUPAL_ANONYMOUS_RID . '[post comments]' => 1,
801
    );
802
    $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
803

    
804
    // Create a node.
805
    $node = $this->drupalCreateNode(array('type' => 'article'));
806
    // Post a comment using 'Full HTML' text format.
807
    $edit_comment = array();
808
    $edit_comment['subject'] = 'Test comment subject';
809
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
810
    $full_html_format_id = 'full_html';
811
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id;
812
    $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save'));
813

    
814
    // Invoke search index update.
815
    $this->drupalLogout();
816
    $this->cronRun();
817

    
818
    // Search for the comment subject.
819
    $edit = array(
820
      'search_block_form' => "'" . $edit_comment['subject'] . "'",
821
    );
822
    $this->drupalPost('', $edit, t('Search'));
823
    $this->assertText($node->title, 'Node found in search results.');
824
    $this->assertText($edit_comment['subject'], 'Comment subject found in search results.');
825

    
826
    // Search for the comment body.
827
    $edit = array(
828
      'search_block_form' => "'" . $comment_body . "'",
829
    );
830
    $this->drupalPost('', $edit, t('Search'));
831
    $this->assertText($node->title, 'Node found in search results.');
832

    
833
    // Verify that comment is rendered using proper format.
834
    $this->assertText($comment_body, 'Comment body text found in search results.');
835
    $this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.');
836
    $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), 'HTML in comment body is not escaped.');
837

    
838
    // Hide comments.
839
    $this->drupalLogin($this->admin_user);
840
    $node->comment = 0;
841
    node_save($node);
842

    
843
    // Invoke search index update.
844
    $this->drupalLogout();
845
    $this->cronRun();
846

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

    
852
  /**
853
   * Verify access rules for comment indexing with different permissions.
854
   */
855
  function testSearchResultsCommentAccess() {
856
    $comment_body = 'Test comment body';
857
    $this->comment_subject = 'Test comment subject';
858
    $this->admin_role = $this->admin_user->roles;
859
    unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]);
860
    $this->admin_role = key($this->admin_role);
861

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

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

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

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

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

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

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

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

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

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

    
897
    // Verify that access comments permission is inherited from the
898
    // authenticated role.
899
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE);
900
    $this->setRolePermissions($this->admin_role);
901
    $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);
902

    
903
    // Verify that search content permission is inherited from the authenticated
904
    // role.
905
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE);
906
    $this->setRolePermissions($this->admin_role, TRUE, FALSE);
907
    $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);
908

    
909
  }
910

    
911
  /**
912
   * Set permissions for role.
913
   */
914
  function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
915
    $permissions = array(
916
      'access comments' => $access_comments,
917
      'search content' => $search_content,
918
    );
919
    user_role_change_permissions($rid, $permissions);
920
  }
921

    
922
  /**
923
   * Update search index and search for comment.
924
   */
925
  function checkCommentAccess($message, $assume_access = FALSE) {
926
    // Invoke search index update.
927
    search_touch_node($this->node->nid);
928
    $this->cronRun();
929

    
930
    // Search for the comment subject.
931
    $edit = array(
932
      'search_block_form' => "'" . $this->comment_subject . "'",
933
    );
934
    $this->drupalPost('', $edit, t('Search'));
935
    $method = $assume_access ? 'assertText' : 'assertNoText';
936
    $verb = $assume_access ? 'found' : 'not found';
937
    $this->{$method}($this->node->title, "Node $verb in search results: " . $message);
938
    $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message);
939
  }
940

    
941
  /**
942
   * Verify that 'add new comment' does not appear in search results or index.
943
   */
944
  function testAddNewComment() {
945
    // Create a node with a short body.
946
    $settings = array(
947
      'type' => 'article',
948
      'title' => 'short title',
949
      'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))),
950
    );
951

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

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

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

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

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

    
977
}
978

    
979
/**
980
 * Tests search_expression_insert() and search_expression_extract().
981
 *
982
 * @see http://drupal.org/node/419388 (issue)
983
 */
984
class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase {
985
  public static function getInfo() {
986
    return array(
987
      'name' => 'Search expression insert/extract',
988
      'description' => 'Tests the functions search_expression_insert() and search_expression_extract()',
989
      'group' => 'Search',
990
    );
991
  }
992

    
993
  function setUp() {
994
    drupal_load('module', 'search');
995
    parent::setUp();
996
  }
997

    
998
  /**
999
   * Tests search_expression_insert() and search_expression_extract().
1000
   */
1001
  function testInsertExtract() {
1002
    $base_expression = "mykeyword";
1003
    // Build an array of option, value, what should be in the expression, what
1004
    // should be retrieved from expression.
1005
    $cases = array(
1006
      array('foo', 'bar', 'foo:bar', 'bar'), // Normal case.
1007
      array('foo', NULL, '', NULL), // Empty value: shouldn't insert.
1008
      array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string.
1009
      array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string.
1010
      array('foo', '0', 'foo:0', '0'), // String zero as value: should insert.
1011
      array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert.
1012
    );
1013

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

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

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

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

    
1040
/**
1041
 * Tests that comment count display toggles properly on comment status of node
1042
 *
1043
 * Issue 537278
1044
 *
1045
 * - Nodes with comment status set to Open should always how comment counts
1046
 * - Nodes with comment status set to Closed should show comment counts
1047
 *     only when there are comments
1048
 * - Nodes with comment status set to Hidden should never show comment counts
1049
 */
1050
class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
1051
  protected $searching_user;
1052
  protected $searchable_nodes;
1053

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

    
1062
  function setUp() {
1063
    parent::setUp('search');
1064

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

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

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

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

    
1077
    // Create a comment array
1078
    $edit_comment = array();
1079
    $edit_comment['subject'] = $this->randomName();
1080
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
1081
    $filtered_html_format_id = 'filtered_html';
1082
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;
1083

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

    
1087
    // First update the index. This does the initial processing.
1088
    node_update_index();
1089

    
1090
    // Then, run the shutdown function. Testing is a unique case where indexing
1091
    // and searching has to happen in the same request, so running the shutdown
1092
    // function manually is needed to finish the indexing process.
1093
    search_update_totals();
1094
  }
1095

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

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

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

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

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

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

    
1132
/**
1133
 * Test search_simplify() on every Unicode character, and some other cases.
1134
 */
1135
class SearchSimplifyTestCase extends DrupalWebTestCase {
1136
  public static function getInfo() {
1137
    return array(
1138
      'name' => 'Search simplify',
1139
      'description' => 'Check that the search_simply() function works as intended.',
1140
      'group' => 'Search',
1141
    );
1142
  }
1143

    
1144
  /**
1145
   * Tests that all Unicode characters simplify correctly.
1146
   */
1147
  function testSearchSimplifyUnicode() {
1148
    // This test uses a file that was constructed so that the even lines are
1149
    // boundary characters, and the odd lines are valid word characters. (It
1150
    // was generated as a sequence of all the Unicode characters, and then the
1151
    // boundary chararacters (punctuation, spaces, etc.) were split off into
1152
    // their own lines).  So the even-numbered lines should simplify to nothing,
1153
    // and the odd-numbered lines we need to split into shorter chunks and
1154
    // verify that simplification doesn't lose any characters.
1155
    $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
1156
    $basestrings = explode(chr(10), $input);
1157
    $strings = array();
1158
    foreach ($basestrings as $key => $string) {
1159
      if ($key %2) {
1160
        // Even line - should simplify down to a space.
1161
        $simplified = search_simplify($string);
1162
        $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
1163
      }
1164
      else {
1165
        // Odd line, should be word characters.
1166
        // Split this into 30-character chunks, so we don't run into limits
1167
        // of truncation in search_simplify().
1168
        $start = 0;
1169
        while ($start < drupal_strlen($string)) {
1170
          $newstr = drupal_substr($string, $start, 30);
1171
          // Special case: leading zeros are removed from numeric strings,
1172
          // and there's one string in this file that is numbers starting with
1173
          // zero, so prepend a 1 on that string.
1174
          if (preg_match('/^[0-9]+$/', $newstr)) {
1175
            $newstr = '1' . $newstr;
1176
          }
1177
          $strings[] = $newstr;
1178
          $start += 30;
1179
        }
1180
      }
1181
    }
1182
    foreach ($strings as $key => $string) {
1183
      $simplified = search_simplify($string);
1184
      $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key.");
1185
    }
1186

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

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

    
1207
    foreach ($cases as $case) {
1208
      $out = trim(search_simplify($case[0]));
1209
      $this->assertEqual($out, $case[1], $case[2]);
1210
    }
1211
  }
1212
}
1213

    
1214

    
1215
/**
1216
 * Tests keywords and conditions.
1217
 */
1218
class SearchKeywordsConditions extends DrupalWebTestCase {
1219

    
1220
  public static function getInfo() {
1221
    return array(
1222
      'name' => 'Keywords and conditions',
1223
      'description' => 'Verify the search pulls in keywords and extra conditions.',
1224
      'group' => 'Search',
1225
    );
1226
  }
1227

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

    
1239
  /**
1240
   * Verify the kewords are captured and conditions respected.
1241
   */
1242
  function testSearchKeyswordsConditions() {
1243
    // No keys, not conditions - no results.
1244
    $this->drupalGet('search/dummy_path');
1245
    $this->assertNoText('Dummy search snippet to display');
1246
    // With keys - get results.
1247
    $keys = 'bike shed ' . $this->randomName();
1248
    $this->drupalGet("search/dummy_path/{$keys}");
1249
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
1250
    $keys = 'blue drop ' . $this->randomName();
1251
    $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
1252
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
1253
    // Add some conditions and keys.
1254
    $keys = 'moving drop ' . $this->randomName();
1255
    $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys)));
1256
    $this->assertText("Dummy search snippet to display.");
1257
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
1258
    // Add some conditions and no keys.
1259
    $keys = 'drop kick ' . $this->randomName();
1260
    $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys)));
1261
    $this->assertText("Dummy search snippet to display.");
1262
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
1263
  }
1264
}
1265

    
1266
/**
1267
 * Tests that numbers can be searched.
1268
 */
1269
class SearchNumbersTestCase extends DrupalWebTestCase {
1270
  protected $test_user;
1271
  protected $numbers;
1272
  protected $nodes;
1273

    
1274
  public static function getInfo() {
1275
    return array(
1276
      'name' => 'Search numbers',
1277
      'description' => 'Check that numbers can be searched',
1278
      'group' => 'Search',
1279
    );
1280
  }
1281

    
1282
  function setUp() {
1283
    parent::setUp('search');
1284

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

    
1288
    // Create content with various numbers in it.
1289
    // Note: 50 characters is the current limit of the search index's word
1290
    // field.
1291
    $this->numbers = array(
1292
      'ISBN' => '978-0446365383',
1293
      'UPC' => '036000 291452',
1294
      'EAN bar code' => '5901234123457',
1295
      'negative' => '-123456.7890',
1296
      'quoted negative' => '"-123456.7890"',
1297
      'leading zero' => '0777777777',
1298
      'tiny' => '111',
1299
      'small' => '22222222222222',
1300
      'medium' => '333333333333333333333333333',
1301
      'large' => '444444444444444444444444444444444444444',
1302
      'gigantic' => '5555555555555555555555555555555555555555555555555',
1303
      'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
1304
      'date', '01/02/2009',
1305
      'commas', '987,654,321',
1306
    );
1307

    
1308
    foreach ($this->numbers as $doc => $num) {
1309
      $info = array(
1310
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
1311
        'type' => 'page',
1312
        'language' => LANGUAGE_NONE,
1313
        'title' => $doc . ' number',
1314
      );
1315
      $this->nodes[$doc] = $this->drupalCreateNode($info);
1316
    }
1317

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

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

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

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

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

    
1354
/**
1355
 * Tests that numbers can be searched, with more complex matching.
1356
 */
1357
class SearchNumberMatchingTestCase extends DrupalWebTestCase {
1358
  protected $test_user;
1359
  protected $numbers;
1360
  protected $nodes;
1361

    
1362
  public static function getInfo() {
1363
    return array(
1364
      'name' => 'Search number matching',
1365
      'description' => 'Check that numbers can be searched with more complex matching',
1366
      'group' => 'Search',
1367
    );
1368
  }
1369

    
1370
  function setUp() {
1371
    parent::setUp('search');
1372

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

    
1376
    // Define a group of numbers that should all match each other --
1377
    // numbers with internal punctuation should match each other, as well
1378
    // as numbers with and without leading zeros and leading/trailing
1379
    // . and -.
1380
    $this->numbers = array(
1381
      '123456789',
1382
      '12/34/56789',
1383
      '12.3456789',
1384
      '12-34-56789',
1385
      '123,456,789',
1386
      '-123456789',
1387
      '0123456789',
1388
    );
1389

    
1390
    foreach ($this->numbers as $num) {
1391
      $info = array(
1392
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
1393
        'type' => 'page',
1394
        'language' => LANGUAGE_NONE,
1395
      );
1396
      $this->nodes[] = $this->drupalCreateNode($info);
1397
    }
1398

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

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

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

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

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

    
1434
  }
1435
}
1436

    
1437
/**
1438
 * Test config page.
1439
 */
1440
class SearchConfigSettingsForm extends DrupalWebTestCase {
1441
  public $search_user;
1442
  public $search_node;
1443

    
1444
  public static function getInfo() {
1445
    return array(
1446
      'name' => 'Config settings form',
1447
      'description' => 'Verify the search config settings form.',
1448
      'group' => 'Search',
1449
    );
1450
  }
1451

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

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

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

    
1469
    node_update_index();
1470
    search_update_totals();
1471

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

    
1478
  /**
1479
   * Verify the search settings form.
1480
   */
1481
  function testSearchSettingsPage() {
1482

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

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

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

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

    
1506
    // Test logging setting. It should be on by default.
1507
    $text = $this->randomName(5);
1508
    $this->drupalPost('search/node', array('keys' => $text), t('Search'));
1509
    $this->drupalGet('admin/reports/dblog');
1510
    $this->assertLink('Searched Content for ' . $text . '.', 0, 'Search was logged');
1511

    
1512
    // Turn off logging.
1513
    variable_set('search_logging', FALSE);
1514
    $text = $this->randomName(5);
1515
    $this->drupalPost('search/node', array('keys' => $text), t('Search'));
1516
    $this->drupalGet('admin/reports/dblog');
1517
    $this->assertNoLink('Searched Content for ' . $text . '.', 'Search was not logged');
1518
  }
1519

    
1520
  /**
1521
   * Verify that you can disable individual search modules.
1522
   */
1523
  function testSearchModuleDisabling() {
1524
    // Array of search modules to test: 'path' is the search path, 'title' is
1525
    // the tab title, 'keys' are the keywords to search for, and 'text' is
1526
    // the text to assert is on the results page.
1527
    $module_info = array(
1528
      'node' => array(
1529
        'path' => 'node',
1530
        'title' => 'Content',
1531
        'keys' => 'pizza',
1532
        'text' => $this->search_node->title,
1533
      ),
1534
      'user' => array(
1535
        'path' => 'user',
1536
        'title' => 'User',
1537
        'keys' => $this->search_user->name,
1538
        'text' => $this->search_user->mail,
1539
      ),
1540
      'search_extra_type' => array(
1541
        'path' => 'dummy_path',
1542
        'title' => 'Dummy search type',
1543
        'keys' => 'foo',
1544
        'text' => 'Dummy search snippet to display',
1545
      ),
1546
    );
1547
    $modules = array_keys($module_info);
1548

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

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

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

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

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

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

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

    
1600
    foreach (array('search/node/pizza', 'search/node') as $path) {
1601
      $this->drupalGet($path);
1602
      foreach ($modules as $module) {
1603
        $title = $module_info[$module]['title'];
1604
        $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
1605
      }
1606
    }
1607
  }
1608
}
1609

    
1610
/**
1611
 * Tests the search_excerpt() function.
1612
 */
1613
class SearchExcerptTestCase extends DrupalWebTestCase {
1614
  public static function getInfo() {
1615
    return array(
1616
      'name' => 'Search excerpt extraction',
1617
      'description' => 'Tests that the search_excerpt() function works.',
1618
      'group' => 'Search',
1619
    );
1620
  }
1621

    
1622
  function setUp() {
1623
    parent::setUp('search');
1624
  }
1625

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

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

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

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

    
1656
    // The node body that will produce this rendered $text is:
1657
    // 123456789 HTMLTest +123456789+&lsquo;  +&lsquo;  +&lsquo;  +&lsquo;  +12345678  &nbsp;&nbsp;  +&lsquo;  +&lsquo;  +&lsquo;   &lsquo;
1658
    $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> ";
1659
    $result = search_excerpt('HTMLTest', $text);
1660
    $this->assertFalse(empty($result),  'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
1661
  }
1662

    
1663
  /**
1664
   * Tests search_excerpt() with search keywords matching simplified words.
1665
   *
1666
   * Excerpting should handle keywords that are matched only after going through
1667
   * search_simplify(). This test passes keywords that match simplified words
1668
   * and compares them with strings that contain the original unsimplified word.
1669
   */
1670
  function testSearchExcerptSimplified() {
1671
    $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.';
1672
    $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.';
1673

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

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

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

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

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

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

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

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

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

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

    
1713
/**
1714
 * Test the CJK tokenizer.
1715
 */
1716
class SearchTokenizerTestCase extends DrupalWebTestCase {
1717
  public static function getInfo() {
1718
    return array(
1719
      'name' => 'CJK tokenizer',
1720
      'description' => 'Check that CJK tokenizer works as intended.',
1721
      'group' => 'Search',
1722
    );
1723
  }
1724

    
1725
  function setUp() {
1726
    parent::setUp('search');
1727
  }
1728

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

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

    
1747
    // Beginnings of the character ranges.
1748
    $starts = array(
1749
      'CJK unified' => 0x4e00,
1750
      'CJK Ext A' => 0x3400,
1751
      'CJK Compat' => 0xf900,
1752
      'Hangul Jamo' => 0x1100,
1753
      'Hangul Ext A' => 0xa960,
1754
      'Hangul Ext B' => 0xd7b0,
1755
      'Hangul Compat' => 0x3131,
1756
      'Half non-punct 1' => 0xff21,
1757
      'Half non-punct 2' => 0xff41,
1758
      'Half non-punct 3' => 0xff66,
1759
      'Hangul Syllables' => 0xac00,
1760
      'Hiragana' => 0x3040,
1761
      'Katakana' => 0x30a1,
1762
      'Katakana Ext' => 0x31f0,
1763
      'CJK Reserve 1' => 0x20000,
1764
      'CJK Reserve 2' => 0x30000,
1765
      'Bomofo' => 0x3100,
1766
      'Bomofo Ext' => 0x31a0,
1767
      'Lisu' => 0xa4d0,
1768
      'Yi' => 0xa000,
1769
    );
1770

    
1771
    // Ends of the character ranges.
1772
    $ends = array(
1773
      'CJK unified' => 0x9fcf,
1774
      'CJK Ext A' => 0x4dbf,
1775
      'CJK Compat' => 0xfaff,
1776
      'Hangul Jamo' => 0x11ff,
1777
      'Hangul Ext A' => 0xa97f,
1778
      'Hangul Ext B' => 0xd7ff,
1779
      'Hangul Compat' => 0x318e,
1780
      'Half non-punct 1' => 0xff3a,
1781
      'Half non-punct 2' => 0xff5a,
1782
      'Half non-punct 3' => 0xffdc,
1783
      'Hangul Syllables' => 0xd7af,
1784
      'Hiragana' => 0x309f,
1785
      'Katakana' => 0x30ff,
1786
      'Katakana Ext' => 0x31ff,
1787
      'CJK Reserve 1' => 0x2fffd,
1788
      'CJK Reserve 2' => 0x3fffd,
1789
      'Bomofo' => 0x312f,
1790
      'Bomofo Ext' => 0x31b7,
1791
      'Lisu' => 0xa4fd,
1792
      'Yi' => 0xa48f,
1793
    );
1794

    
1795
    // Generate characters consisting of starts, midpoints, and ends.
1796
    $chars = array();
1797
    $charcodes = array();
1798
    foreach ($starts as $key => $value) {
1799
      $charcodes[] = $starts[$key];
1800
      $chars[] = $this->code2utf($starts[$key]);
1801
      $mid = round(0.5 * ($starts[$key] + $ends[$key]));
1802
      $charcodes[] = $mid;
1803
      $chars[] = $this->code2utf($mid);
1804
      $charcodes[] = $ends[$key];
1805
      $chars[] = $this->code2utf($ends[$key]);
1806
    }
1807

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

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

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

    
1830
    $letters = 'abcdefghijklmnopqrstuvwxyz';
1831
    $out = trim(search_simplify($letters));
1832

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

    
1836
  /**
1837
   * Like PHP chr() function, but for unicode characters.
1838
   *
1839
   * chr() only works for ASCII characters up to character 255. This function
1840
   * converts a number to the corresponding unicode character. Adapted from
1841
   * functions supplied in comments on several functions on php.net.
1842
   */
1843
  function code2utf($num) {
1844
    if ($num < 128) {
1845
      return chr($num);
1846
    }
1847

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

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

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

    
1860
    return '';
1861
  }
1862
}
1863

    
1864
/**
1865
 * Tests that we can embed a form in search results and submit it.
1866
 */
1867
class SearchEmbedForm extends DrupalWebTestCase {
1868
  /**
1869
   * Node used for testing.
1870
   */
1871
  public $node;
1872

    
1873
  /**
1874
   * Count of how many times the form has been submitted.
1875
   */
1876
  public $submit_count = 0;
1877

    
1878
  public static function getInfo() {
1879
    return array(
1880
      'name' => 'Embedded forms',
1881
      'description' => 'Verifies that a form embedded in search results works',
1882
      'group' => 'Search',
1883
    );
1884
  }
1885

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

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

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

    
1895
    node_update_index();
1896
    search_update_totals();
1897

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

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

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

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

    
1940
/**
1941
 * Tests that hook_search_page runs.
1942
 */
1943
class SearchPageOverride extends DrupalWebTestCase {
1944
  public $search_user;
1945

    
1946
  public static function getInfo() {
1947
    return array(
1948
      'name' => 'Search page override',
1949
      'description' => 'Verify that hook_search_page can override search page display.',
1950
      'group' => 'Search',
1951
    );
1952
  }
1953

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

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

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

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

    
1974
/**
1975
 * Test node search with multiple languages.
1976
 */
1977
class SearchLanguageTestCase extends DrupalWebTestCase {
1978
  public static function getInfo() {
1979
    return array(
1980
      'name' => 'Search language selection',
1981
      'description' => 'Tests advanced search with different languages enabled.',
1982
      'group' => 'Search',
1983
    );
1984
  }
1985

    
1986
  /**
1987
   * Implementation setUp().
1988
   */
1989
  function setUp() {
1990
    parent::setUp('search', 'locale');
1991

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

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

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

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

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

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

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

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

    
2039
/**
2040
 * Tests node search with node access control.
2041
 */
2042
class SearchNodeAccessTest extends DrupalWebTestCase {
2043
  public $test_user;
2044

    
2045
  public static function getInfo() {
2046
    return array(
2047
      'name' => 'Search and node access',
2048
      'description' => 'Tests search functionality with node access control.',
2049
      'group' => 'Search',
2050
    );
2051
  }
2052

    
2053
  function setUp() {
2054
    parent::setUp('search', 'node_access_test');
2055
    node_access_rebuild();
2056

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

    
2062
  /**
2063
   * Tests that search works with punctuation and HTML entities.
2064
   */
2065
  function testPhraseSearchPunctuation() {
2066
    $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy.")))));
2067
    $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.')))));
2068

    
2069
    // Update the search index.
2070
    module_invoke_all('update_index');
2071
    search_update_totals();
2072

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

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

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

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

    
2094
/**
2095
 * Tests node search with query tags.
2096
 */
2097
class SearchNodeTagTest extends DrupalWebTestCase {
2098
  public $test_user;
2099

    
2100
  public static function getInfo() {
2101
    return array(
2102
      'name' => 'Node search query tags',
2103
      'description' => 'Tests Node search tags functionality.',
2104
      'group' => 'Search',
2105
    );
2106
  }
2107

    
2108
  function setUp() {
2109
    parent::setUp('search', 'search_node_tags');
2110
    node_access_rebuild();
2111

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

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

    
2123
    // Update the search index.
2124
    module_invoke_all('update_index');
2125
    search_update_totals();
2126

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

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

    
2135
/**
2136
 * Tests searching with locale values set.
2137
 */
2138
class SearchSetLocaleTest extends DrupalWebTestCase {
2139

    
2140
  public static function getInfo() {
2141
    return array(
2142
      'name' => 'Search with numeric locale set',
2143
      'description' => 'Check that search works with numeric locale settings',
2144
      'group' => 'Search',
2145
    );
2146
  }
2147

    
2148
  function setUp() {
2149
    parent::setUp('search');
2150

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

    
2157
    // Run cron to index.
2158
    $this->cronRun();
2159
  }
2160

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

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