Projet

Général

Profil

Paste
Télécharger (77,3 ko) Statistiques
| Branche: | Révision:

root / htmltest / modules / search / search.test @ 85ad3d82

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
class SearchMatchTestCase extends DrupalWebTestCase {
15
  public static function getInfo() {
16
    return array(
17
      'name' => 'Search engine queries',
18
      'description' => 'Indexes content and queries it.',
19
      'group' => 'Search',
20
    );
21
  }
22

    
23
  /**
24
   * Implementation setUp().
25
   */
26
  function setUp() {
27
    parent::setUp('search');
28
  }
29

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

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

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

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

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

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

    
101
      e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good
102
      e.g. dolore OR ut = (dolore) OR (ut) -> bad
103

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

    
154
      $set = $result ? $result->fetchAll() : array();
155
      $this->_testQueryMatching($query, $set, $results);
156
      $this->_testQueryScores($query, $set, $results);
157
    }
158

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

    
174
      $set = $result ? $result->fetchAll() : array();
175
      $this->_testQueryMatching($query, $set, $results);
176
      $this->_testQueryScores($query, $set, $results);
177
    }
178

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

    
197
      $set = $result ? $result->fetchAll() : array();
198
      $this->_testQueryMatching($query, $set, $results);
199
      $this->_testQueryScores($query, $set, $results);
200
    }
201
  }
202

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

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

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

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

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

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

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

    
257
  function setUp() {
258
    parent::setUp('search');
259

    
260
    // Create user.
261
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles'));
262
  }
263

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

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

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

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

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

    
310
class SearchAdvancedSearchForm extends DrupalWebTestCase {
311
  protected $node;
312

    
313
  public static function getInfo() {
314
    return array(
315
      'name' => 'Advanced search form',
316
      'description' => 'Indexes content and tests the advanced search form.',
317
      'group' => 'Search',
318
    );
319
  }
320

    
321
  function setUp() {
322
    parent::setUp('search');
323
    // Create and login user.
324
    $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes'));
325
    $this->drupalLogin($test_user);
326

    
327
    // Create initial node.
328
    $node = $this->drupalCreateNode();
329
    $this->node = $this->drupalCreateNode();
330

    
331
    // First update the index. This does the initial processing.
332
    node_update_index();
333

    
334
    // Then, run the shutdown function. Testing is a unique case where indexing
335
    // and searching has to happen in the same request, so running the shutdown
336
    // function manually is needed to finish the indexing process.
337
    search_update_totals();
338
  }
339

    
340
  /**
341
   * Test using the search form with GET and POST queries.
342
   * Test using the advanced search form to limit search to nodes of type "Basic page".
343
   */
344
  function testNodeType() {
345
    $this->assertTrue($this->node->type == 'page', 'Node type is Basic page.');
346

    
347
    // Assert that the dummy title doesn't equal the real title.
348
    $dummy_title = 'Lorem ipsum';
349
    $this->assertNotEqual($dummy_title, $this->node->title, "Dummy title doesn't equal node title");
350

    
351
    // Search for the dummy title with a GET query.
352
    $this->drupalGet('search/node/' . $dummy_title);
353
    $this->assertNoText($this->node->title, 'Basic page node is not found with dummy title.');
354

    
355
    // Search for the title of the node with a GET query.
356
    $this->drupalGet('search/node/' . $this->node->title);
357
    $this->assertText($this->node->title, 'Basic page node is found with GET query.');
358

    
359
    // Search for the title of the node with a POST query.
360
    $edit = array('or' => $this->node->title);
361
    $this->drupalPost('search/node', $edit, t('Advanced search'));
362
    $this->assertText($this->node->title, 'Basic page node is found with POST query.');
363

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

    
368
    $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search'));
369
    $this->assertText('bike shed', 'Article node is not found with POST query and type:article.');
370
  }
371
}
372

    
373
class SearchRankingTestCase extends DrupalWebTestCase {
374
  public static function getInfo() {
375
    return array(
376
      'name' => 'Search engine ranking',
377
      'description' => 'Indexes content and tests ranking factors.',
378
      'group' => 'Search',
379
    );
380
  }
381

    
382
  /**
383
   * Implementation setUp().
384
   */
385
  function setUp() {
386
    parent::setUp('search', 'statistics', 'comment');
387
  }
388

    
389
  function testRankings() {
390
    // Login with sufficient privileges.
391
    $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content')));
392

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

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

    
425
    // Update the search index.
426
    module_invoke_all('update_index');
427
    search_update_totals();
428

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

    
432
    // Add a comment to one of the nodes.
433
    $edit = array();
434
    $edit['subject'] = 'my comment title';
435
    $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment';
436
    $this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid);
437
    $this->drupalPost(NULL, $edit, t('Preview'));
438
    $this->drupalPost(NULL, $edit, t('Save'));
439

    
440
    // Enable counting of statistics.
441
    variable_set('statistics_count_content_views', 1);
442

    
443
    // Then View one of the nodes a bunch of times.
444
    for ($i = 0; $i < 5; $i ++) {
445
      $this->drupalGet('node/' . $nodes['views'][1]->nid);
446
    }
447

    
448
    // Test each of the possible rankings.
449
    foreach ($node_ranks as $node_rank) {
450
      // Disable all relevancy rankings except the one we are testing.
451
      foreach ($node_ranks as $var) {
452
        variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0);
453
      }
454

    
455
      // Do the search and assert the results.
456
      $set = node_search_execute('rocks');
457
      $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
458
    }
459
  }
460

    
461
  /**
462
   * Test rankings of HTML tags.
463
   */
464
  function testHTMLRankings() {
465
    // Login with sufficient privileges.
466
    $this->drupalLogin($this->drupalCreateUser(array('create page content')));
467

    
468
    // Test HTML tags with different weights.
469
    $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag');
470
    $shuffled_tags = $sorted_tags;
471

    
472
    // Shuffle tags to ensure HTML tags are ranked properly.
473
    shuffle($shuffled_tags);
474
    $settings = array(
475
      'type' => 'page',
476
      'title' => 'Simple node',
477
    );
478
    foreach ($shuffled_tags as $tag) {
479
      switch ($tag) {
480
        case 'a':
481
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 'full_html')));
482
          break;
483
        case 'notag':
484
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks')));
485
          break;
486
        default:
487
          $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html')));
488
          break;
489
      }
490
      $nodes[$tag] = $this->drupalCreateNode($settings);
491
    }
492

    
493
    // Update the search index.
494
    module_invoke_all('update_index');
495
    search_update_totals();
496

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

    
500
    // Disable all other rankings.
501
    $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views');
502
    foreach ($node_ranks as $node_rank) {
503
      variable_set('node_rank_' . $node_rank, 0);
504
    }
505
    $set = node_search_execute('rocks');
506

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

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

    
523
      // Update the search index.
524
      module_invoke_all('update_index');
525
      search_update_totals();
526

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

    
530
      $set = node_search_execute('rocks');
531

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

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

    
538
      // Delete node so it doesn't show up in subsequent search results.
539
      node_delete($node->nid);
540
    }
541
  }
542

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

    
552
    // See testRankings() above - build a node that will rank high for sticky.
553
    $settings = array(
554
      'type' => 'page',
555
      'title' => 'Drupal rocks',
556
      'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
557
      'sticky' => 1,
558
    );
559

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

    
562
    // Update the search index.
563
    module_invoke_all('update_index');
564
    search_update_totals();
565

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

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

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

    
583
class SearchBlockTestCase extends DrupalWebTestCase {
584
  public static function getInfo() {
585
    return array(
586
      'name' => 'Block availability',
587
      'description' => 'Check if the search form block is available.',
588
      'group' => 'Search',
589
    );
590
  }
591

    
592
  function setUp() {
593
    parent::setUp('search');
594

    
595
    // Create and login user
596
    $admin_user = $this->drupalCreateUser(array('administer blocks', 'search content'));
597
    $this->drupalLogin($admin_user);
598
  }
599

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

    
605
    // Set the block to a region to confirm block is available.
606
    $edit = array();
607
    $edit['blocks[search_form][region]'] = 'footer';
608
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
609
    $this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.');
610
  }
611

    
612
  /**
613
   * Test that the search block form works correctly.
614
   */
615
  function testBlock() {
616
    // Enable the block, and place it in the 'content' region so that it isn't
617
    // hidden on 404 pages.
618
    $edit = array('blocks[search_form][region]' => 'content');
619
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
620

    
621
    // Test a normal search via the block form, from the front page.
622
    $terms = array('search_block_form' => 'test');
623
    $this->drupalPost('node', $terms, t('Search'));
624
    $this->assertText('Your search yielded no results');
625

    
626
    // Test a search from the block on a 404 page.
627
    $this->drupalGet('foo');
628
    $this->assertResponse(404);
629
    $this->drupalPost(NULL, $terms, t('Search'));
630
    $this->assertResponse(200);
631
    $this->assertText('Your search yielded no results');
632

    
633
    // Test a search from the block when it doesn't appear on the search page.
634
    $edit = array('pages' => 'search');
635
    $this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block'));
636
    $this->drupalPost('node', $terms, t('Search'));
637
    $this->assertText('Your search yielded no results');
638

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

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

    
651
    // Confirm that the user is redirected to the search page, when form is submitted empty.
652
    $this->assertEqual(
653
      $this->getUrl(),
654
      url('search/node/', array('absolute' => TRUE)),
655
      'Redirected to correct url.'
656
    );
657
  }
658
}
659

    
660
/**
661
 * Tests that searching for a phrase gets the correct page count.
662
 */
663
class SearchExactTestCase extends DrupalWebTestCase {
664
  public static function getInfo() {
665
    return array(
666
      'name' => 'Search engine phrase queries',
667
      'description' => 'Tests that searching for a phrase gets the correct page count.',
668
      'group' => 'Search',
669
    );
670
  }
671

    
672
  function setUp() {
673
    parent::setUp('search');
674
  }
675

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

    
683
    $settings = array(
684
      'type' => 'page',
685
      'title' => 'Simple Node',
686
    );
687
    // Create nodes with exact phrase.
688
    for ($i = 0; $i <= 17; $i++) {
689
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza')));
690
      $this->drupalCreateNode($settings);
691
    }
692
    // Create nodes containing keywords.
693
    for ($i = 0; $i <= 17; $i++) {
694
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza')));
695
      $this->drupalCreateNode($settings);
696
    }
697

    
698
    // Update the search index.
699
    module_invoke_all('update_index');
700
    search_update_totals();
701

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

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

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

    
721
/**
722
 * Test integration searching comments.
723
 */
724
class SearchCommentTestCase extends DrupalWebTestCase {
725
  protected $admin_user;
726

    
727
  public static function getInfo() {
728
    return array(
729
      'name' => 'Comment Search tests',
730
      'description' => 'Verify text formats and filters used elsewhere.',
731
      'group' => 'Search',
732
    );
733
  }
734

    
735
  function setUp() {
736
    parent::setUp('comment', 'search');
737

    
738
    // Create and log in an administrative user having access to the Full HTML
739
    // text format.
740
    $full_html_format = filter_format_load('full_html');
741
    $permissions = array(
742
      'administer filters',
743
      filter_permission_name($full_html_format),
744
      'administer permissions',
745
      'create page content',
746
      'skip comment approval',
747
      'access comments',
748
    );
749
    $this->admin_user = $this->drupalCreateUser($permissions);
750
    $this->drupalLogin($this->admin_user);
751
  }
752

    
753
  /**
754
   * Verify that comments are rendered using proper format in search results.
755
   */
756
  function testSearchResultsComment() {
757
    $comment_body = 'Test comment body';
758

    
759
    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
760
    // Enable check_plain() for 'Filtered HTML' text format.
761
    $filtered_html_format_id = 'filtered_html';
762
    $edit = array(
763
      'filters[filter_html_escape][status]' => TRUE,
764
    );
765
    $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration'));
766
    // Allow anonymous users to search content.
767
    $edit = array(
768
      DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
769
      DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
770
      DRUPAL_ANONYMOUS_RID . '[post comments]' => 1,
771
    );
772
    $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
773

    
774
    // Create a node.
775
    $node = $this->drupalCreateNode(array('type' => 'article'));
776
    // Post a comment using 'Full HTML' text format.
777
    $edit_comment = array();
778
    $edit_comment['subject'] = 'Test comment subject';
779
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
780
    $full_html_format_id = 'full_html';
781
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id;
782
    $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save'));
783

    
784
    // Invoke search index update.
785
    $this->drupalLogout();
786
    $this->cronRun();
787

    
788
    // Search for the comment subject.
789
    $edit = array(
790
      'search_block_form' => "'" . $edit_comment['subject'] . "'",
791
    );
792
    $this->drupalPost('', $edit, t('Search'));
793
    $this->assertText($node->title, 'Node found in search results.');
794
    $this->assertText($edit_comment['subject'], 'Comment subject found in search results.');
795

    
796
    // Search for the comment body.
797
    $edit = array(
798
      'search_block_form' => "'" . $comment_body . "'",
799
    );
800
    $this->drupalPost('', $edit, t('Search'));
801
    $this->assertText($node->title, 'Node found in search results.');
802

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

    
808
    // Hide comments.
809
    $this->drupalLogin($this->admin_user);
810
    $node->comment = 0;
811
    node_save($node);
812

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

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

    
822
  /**
823
   * Verify access rules for comment indexing with different permissions.
824
   */
825
  function testSearchResultsCommentAccess() {
826
    $comment_body = 'Test comment body';
827
    $this->comment_subject = 'Test comment subject';
828
    $this->admin_role = $this->admin_user->roles;
829
    unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]);
830
    $this->admin_role = key($this->admin_role);
831

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

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

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

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

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

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

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

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

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

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

    
867
    // Verify that access comments permission is inherited from the
868
    // authenticated role.
869
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE);
870
    $this->setRolePermissions($this->admin_role);
871
    $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);
872

    
873
    // Verify that search content permission is inherited from the authenticated
874
    // role.
875
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE);
876
    $this->setRolePermissions($this->admin_role, TRUE, FALSE);
877
    $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);
878

    
879
  }
880

    
881
  /**
882
   * Set permissions for role.
883
   */
884
  function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
885
    $permissions = array(
886
      'access comments' => $access_comments,
887
      'search content' => $search_content,
888
    );
889
    user_role_change_permissions($rid, $permissions);
890
  }
891

    
892
  /**
893
   * Update search index and search for comment.
894
   */
895
  function checkCommentAccess($message, $assume_access = FALSE) {
896
    // Invoke search index update.
897
    search_touch_node($this->node->nid);
898
    $this->cronRun();
899

    
900
    // Search for the comment subject.
901
    $edit = array(
902
      'search_block_form' => "'" . $this->comment_subject . "'",
903
    );
904
    $this->drupalPost('', $edit, t('Search'));
905
    $method = $assume_access ? 'assertText' : 'assertNoText';
906
    $verb = $assume_access ? 'found' : 'not found';
907
    $this->{$method}($this->node->title, "Node $verb in search results: " . $message);
908
    $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message);
909
  }
910

    
911
  /**
912
   * Verify that 'add new comment' does not appear in search results or index.
913
   */
914
  function testAddNewComment() {
915
    // Create a node with a short body.
916
    $settings = array(
917
      'type' => 'article',
918
      'title' => 'short title',
919
      'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))),
920
    );
921

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

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

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

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

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

    
947
}
948

    
949
/**
950
 * Tests search_expression_insert() and search_expression_extract().
951
 *
952
 * @see http://drupal.org/node/419388 (issue)
953
 */
954
class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase {
955
  public static function getInfo() {
956
    return array(
957
      'name' => 'Search expression insert/extract',
958
      'description' => 'Tests the functions search_expression_insert() and search_expression_extract()',
959
      'group' => 'Search',
960
    );
961
  }
962

    
963
  function setUp() {
964
    drupal_load('module', 'search');
965
    parent::setUp();
966
  }
967

    
968
  /**
969
   * Tests search_expression_insert() and search_expression_extract().
970
   */
971
  function testInsertExtract() {
972
    $base_expression = "mykeyword";
973
    // Build an array of option, value, what should be in the expression, what
974
    // should be retrieved from expression.
975
    $cases = array(
976
      array('foo', 'bar', 'foo:bar', 'bar'), // Normal case.
977
      array('foo', NULL, '', NULL), // Empty value: shouldn't insert.
978
      array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string.
979
      array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string.
980
      array('foo', '0', 'foo:0', '0'), // String zero as value: should insert.
981
      array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert.
982
    );
983

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

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

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

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

    
1010
/**
1011
 * Tests that comment count display toggles properly on comment status of node
1012
 *
1013
 * Issue 537278
1014
 *
1015
 * - Nodes with comment status set to Open should always how comment counts
1016
 * - Nodes with comment status set to Closed should show comment counts
1017
 *     only when there are comments
1018
 * - Nodes with comment status set to Hidden should never show comment counts
1019
 */
1020
class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
1021
  protected $searching_user;
1022
  protected $searchable_nodes;
1023

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

    
1032
  function setUp() {
1033
    parent::setUp('search');
1034

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

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

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

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

    
1047
    // Create a comment array
1048
    $edit_comment = array();
1049
    $edit_comment['subject'] = $this->randomName();
1050
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
1051
    $filtered_html_format_id = 'filtered_html';
1052
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;
1053

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

    
1057
    // First update the index. This does the initial processing.
1058
    node_update_index();
1059

    
1060
    // Then, run the shutdown function. Testing is a unique case where indexing
1061
    // and searching has to happen in the same request, so running the shutdown
1062
    // function manually is needed to finish the indexing process.
1063
    search_update_totals();
1064
  }
1065

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

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

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

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

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

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

    
1102
/**
1103
 * Test search_simplify() on every Unicode character, and some other cases.
1104
 */
1105
class SearchSimplifyTestCase extends DrupalWebTestCase {
1106
  public static function getInfo() {
1107
    return array(
1108
      'name' => 'Search simplify',
1109
      'description' => 'Check that the search_simply() function works as intended.',
1110
      'group' => 'Search',
1111
    );
1112
  }
1113

    
1114
  /**
1115
   * Tests that all Unicode characters simplify correctly.
1116
   */
1117
  function testSearchSimplifyUnicode() {
1118
    // This test uses a file that was constructed so that the even lines are
1119
    // boundary characters, and the odd lines are valid word characters. (It
1120
    // was generated as a sequence of all the Unicode characters, and then the
1121
    // boundary chararacters (punctuation, spaces, etc.) were split off into
1122
    // their own lines).  So the even-numbered lines should simplify to nothing,
1123
    // and the odd-numbered lines we need to split into shorter chunks and
1124
    // verify that simplification doesn't lose any characters.
1125
    $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
1126
    $basestrings = explode(chr(10), $input);
1127
    $strings = array();
1128
    foreach ($basestrings as $key => $string) {
1129
      if ($key %2) {
1130
        // Even line - should simplify down to a space.
1131
        $simplified = search_simplify($string);
1132
        $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
1133
      }
1134
      else {
1135
        // Odd line, should be word characters.
1136
        // Split this into 30-character chunks, so we don't run into limits
1137
        // of truncation in search_simplify().
1138
        $start = 0;
1139
        while ($start < drupal_strlen($string)) {
1140
          $newstr = drupal_substr($string, $start, 30);
1141
          // Special case: leading zeros are removed from numeric strings,
1142
          // and there's one string in this file that is numbers starting with
1143
          // zero, so prepend a 1 on that string.
1144
          if (preg_match('/^[0-9]+$/', $newstr)) {
1145
            $newstr = '1' . $newstr;
1146
          }
1147
          $strings[] = $newstr;
1148
          $start += 30;
1149
        }
1150
      }
1151
    }
1152
    foreach ($strings as $key => $string) {
1153
      $simplified = search_simplify($string);
1154
      $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key.");
1155
    }
1156

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

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

    
1177
    foreach ($cases as $case) {
1178
      $out = trim(search_simplify($case[0]));
1179
      $this->assertEqual($out, $case[1], $case[2]);
1180
    }
1181
  }
1182
}
1183

    
1184

    
1185
/**
1186
 * Tests keywords and conditions.
1187
 */
1188
class SearchKeywordsConditions extends DrupalWebTestCase {
1189

    
1190
  public static function getInfo() {
1191
    return array(
1192
      'name' => 'Keywords and conditions',
1193
      'description' => 'Verify the search pulls in keywords and extra conditions.',
1194
      'group' => 'Search',
1195
    );
1196
  }
1197

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

    
1209
  /**
1210
   * Verify the kewords are captured and conditions respected.
1211
   */
1212
  function testSearchKeyswordsConditions() {
1213
    // No keys, not conditions - no results.
1214
    $this->drupalGet('search/dummy_path');
1215
    $this->assertNoText('Dummy search snippet to display');
1216
    // With keys - get results.
1217
    $keys = 'bike shed ' . $this->randomName();
1218
    $this->drupalGet("search/dummy_path/{$keys}");
1219
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
1220
    $keys = 'blue drop ' . $this->randomName();
1221
    $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
1222
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
1223
    // Add some conditions and keys.
1224
    $keys = 'moving drop ' . $this->randomName();
1225
    $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys)));
1226
    $this->assertText("Dummy search snippet to display.");
1227
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
1228
    // Add some conditions and no keys.
1229
    $keys = 'drop kick ' . $this->randomName();
1230
    $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys)));
1231
    $this->assertText("Dummy search snippet to display.");
1232
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
1233
  }
1234
}
1235

    
1236
/**
1237
 * Tests that numbers can be searched.
1238
 */
1239
class SearchNumbersTestCase extends DrupalWebTestCase {
1240
  protected $test_user;
1241
  protected $numbers;
1242
  protected $nodes;
1243

    
1244
  public static function getInfo() {
1245
    return array(
1246
      'name' => 'Search numbers',
1247
      'description' => 'Check that numbers can be searched',
1248
      'group' => 'Search',
1249
    );
1250
  }
1251

    
1252
  function setUp() {
1253
    parent::setUp('search');
1254

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

    
1258
    // Create content with various numbers in it.
1259
    // Note: 50 characters is the current limit of the search index's word
1260
    // field.
1261
    $this->numbers = array(
1262
      'ISBN' => '978-0446365383',
1263
      'UPC' => '036000 291452',
1264
      'EAN bar code' => '5901234123457',
1265
      'negative' => '-123456.7890',
1266
      'quoted negative' => '"-123456.7890"',
1267
      'leading zero' => '0777777777',
1268
      'tiny' => '111',
1269
      'small' => '22222222222222',
1270
      'medium' => '333333333333333333333333333',
1271
      'large' => '444444444444444444444444444444444444444',
1272
      'gigantic' => '5555555555555555555555555555555555555555555555555',
1273
      'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
1274
      'date', '01/02/2009',
1275
      'commas', '987,654,321',
1276
    );
1277

    
1278
    foreach ($this->numbers as $doc => $num) {
1279
      $info = array(
1280
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
1281
        'type' => 'page',
1282
        'language' => LANGUAGE_NONE,
1283
        'title' => $doc . ' number',
1284
      );
1285
      $this->nodes[$doc] = $this->drupalCreateNode($info);
1286
    }
1287

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

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

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

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

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

    
1324
/**
1325
 * Tests that numbers can be searched, with more complex matching.
1326
 */
1327
class SearchNumberMatchingTestCase extends DrupalWebTestCase {
1328
  protected $test_user;
1329
  protected $numbers;
1330
  protected $nodes;
1331

    
1332
  public static function getInfo() {
1333
    return array(
1334
      'name' => 'Search number matching',
1335
      'description' => 'Check that numbers can be searched with more complex matching',
1336
      'group' => 'Search',
1337
    );
1338
  }
1339

    
1340
  function setUp() {
1341
    parent::setUp('search');
1342

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

    
1346
    // Define a group of numbers that should all match each other --
1347
    // numbers with internal punctuation should match each other, as well
1348
    // as numbers with and without leading zeros and leading/trailing
1349
    // . and -.
1350
    $this->numbers = array(
1351
      '123456789',
1352
      '12/34/56789',
1353
      '12.3456789',
1354
      '12-34-56789',
1355
      '123,456,789',
1356
      '-123456789',
1357
      '0123456789',
1358
    );
1359

    
1360
    foreach ($this->numbers as $num) {
1361
      $info = array(
1362
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
1363
        'type' => 'page',
1364
        'language' => LANGUAGE_NONE,
1365
      );
1366
      $this->nodes[] = $this->drupalCreateNode($info);
1367
    }
1368

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

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

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

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

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

    
1404
  }
1405
}
1406

    
1407
/**
1408
 * Test config page.
1409
 */
1410
class SearchConfigSettingsForm extends DrupalWebTestCase {
1411
  public $search_user;
1412
  public $search_node;
1413

    
1414
  public static function getInfo() {
1415
    return array(
1416
      'name' => 'Config settings form',
1417
      'description' => 'Verify the search config settings form.',
1418
      'group' => 'Search',
1419
    );
1420
  }
1421

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

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

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

    
1439
    node_update_index();
1440
    search_update_totals();
1441

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

    
1448
  /**
1449
   * Verify the search settings form.
1450
   */
1451
  function testSearchSettingsPage() {
1452

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

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

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

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

    
1477
  /**
1478
   * Verify that you can disable individual search modules.
1479
   */
1480
  function testSearchModuleDisabling() {
1481
    // Array of search modules to test: 'path' is the search path, 'title' is
1482
    // the tab title, 'keys' are the keywords to search for, and 'text' is
1483
    // the text to assert is on the results page.
1484
    $module_info = array(
1485
      'node' => array(
1486
        'path' => 'node',
1487
        'title' => 'Content',
1488
        'keys' => 'pizza',
1489
        'text' => $this->search_node->title,
1490
      ),
1491
      'user' => array(
1492
        'path' => 'user',
1493
        'title' => 'User',
1494
        'keys' => $this->search_user->name,
1495
        'text' => $this->search_user->mail,
1496
      ),
1497
      'search_extra_type' => array(
1498
        'path' => 'dummy_path',
1499
        'title' => 'Dummy search type',
1500
        'keys' => 'foo',
1501
        'text' => 'Dummy search snippet to display',
1502
      ),
1503
    );
1504
    $modules = array_keys($module_info);
1505

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

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

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

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

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

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

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

    
1557
    foreach (array('search/node/pizza', 'search/node') as $path) {
1558
      $this->drupalGet($path);
1559
      foreach ($modules as $module) {
1560
        $title = $module_info[$module]['title'];
1561
        $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
1562
      }
1563
    }
1564
  }
1565
}
1566

    
1567
/**
1568
 * Tests the search_excerpt() function.
1569
 */
1570
class SearchExcerptTestCase extends DrupalWebTestCase {
1571
  public static function getInfo() {
1572
    return array(
1573
      'name' => 'Search excerpt extraction',
1574
      'description' => 'Tests that the search_excerpt() function works.',
1575
      'group' => 'Search',
1576
    );
1577
  }
1578

    
1579
  function setUp() {
1580
    parent::setUp('search');
1581
  }
1582

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

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

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

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

    
1613
    // The node body that will produce this rendered $text is:
1614
    // 123456789 HTMLTest +123456789+&lsquo;  +&lsquo;  +&lsquo;  +&lsquo;  +12345678  &nbsp;&nbsp;  +&lsquo;  +&lsquo;  +&lsquo;   &lsquo;
1615
    $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> ";
1616
    $result = search_excerpt('HTMLTest', $text);
1617
    $this->assertFalse(empty($result),  'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
1618
  }
1619

    
1620
  /**
1621
   * Tests search_excerpt() with search keywords matching simplified words.
1622
   *
1623
   * Excerpting should handle keywords that are matched only after going through
1624
   * search_simplify(). This test passes keywords that match simplified words
1625
   * and compares them with strings that contain the original unsimplified word.
1626
   */
1627
  function testSearchExcerptSimplified() {
1628
    $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.';
1629
    $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.';
1630

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

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

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

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

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

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

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

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

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

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

    
1670
/**
1671
 * Test the CJK tokenizer.
1672
 */
1673
class SearchTokenizerTestCase extends DrupalWebTestCase {
1674
  public static function getInfo() {
1675
    return array(
1676
      'name' => 'CJK tokenizer',
1677
      'description' => 'Check that CJK tokenizer works as intended.',
1678
      'group' => 'Search',
1679
    );
1680
  }
1681

    
1682
  function setUp() {
1683
    parent::setUp('search');
1684
  }
1685

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

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

    
1704
    // Beginnings of the character ranges.
1705
    $starts = array(
1706
      'CJK unified' => 0x4e00,
1707
      'CJK Ext A' => 0x3400,
1708
      'CJK Compat' => 0xf900,
1709
      'Hangul Jamo' => 0x1100,
1710
      'Hangul Ext A' => 0xa960,
1711
      'Hangul Ext B' => 0xd7b0,
1712
      'Hangul Compat' => 0x3131,
1713
      'Half non-punct 1' => 0xff21,
1714
      'Half non-punct 2' => 0xff41,
1715
      'Half non-punct 3' => 0xff66,
1716
      'Hangul Syllables' => 0xac00,
1717
      'Hiragana' => 0x3040,
1718
      'Katakana' => 0x30a1,
1719
      'Katakana Ext' => 0x31f0,
1720
      'CJK Reserve 1' => 0x20000,
1721
      'CJK Reserve 2' => 0x30000,
1722
      'Bomofo' => 0x3100,
1723
      'Bomofo Ext' => 0x31a0,
1724
      'Lisu' => 0xa4d0,
1725
      'Yi' => 0xa000,
1726
    );
1727

    
1728
    // Ends of the character ranges.
1729
    $ends = array(
1730
      'CJK unified' => 0x9fcf,
1731
      'CJK Ext A' => 0x4dbf,
1732
      'CJK Compat' => 0xfaff,
1733
      'Hangul Jamo' => 0x11ff,
1734
      'Hangul Ext A' => 0xa97f,
1735
      'Hangul Ext B' => 0xd7ff,
1736
      'Hangul Compat' => 0x318e,
1737
      'Half non-punct 1' => 0xff3a,
1738
      'Half non-punct 2' => 0xff5a,
1739
      'Half non-punct 3' => 0xffdc,
1740
      'Hangul Syllables' => 0xd7af,
1741
      'Hiragana' => 0x309f,
1742
      'Katakana' => 0x30ff,
1743
      'Katakana Ext' => 0x31ff,
1744
      'CJK Reserve 1' => 0x2fffd,
1745
      'CJK Reserve 2' => 0x3fffd,
1746
      'Bomofo' => 0x312f,
1747
      'Bomofo Ext' => 0x31b7,
1748
      'Lisu' => 0xa4fd,
1749
      'Yi' => 0xa48f,
1750
    );
1751

    
1752
    // Generate characters consisting of starts, midpoints, and ends.
1753
    $chars = array();
1754
    $charcodes = array();
1755
    foreach ($starts as $key => $value) {
1756
      $charcodes[] = $starts[$key];
1757
      $chars[] = $this->code2utf($starts[$key]);
1758
      $mid = round(0.5 * ($starts[$key] + $ends[$key]));
1759
      $charcodes[] = $mid;
1760
      $chars[] = $this->code2utf($mid);
1761
      $charcodes[] = $ends[$key];
1762
      $chars[] = $this->code2utf($ends[$key]);
1763
    }
1764

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

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

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

    
1787
    $letters = 'abcdefghijklmnopqrstuvwxyz';
1788
    $out = trim(search_simplify($letters));
1789

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

    
1793
  /**
1794
   * Like PHP chr() function, but for unicode characters.
1795
   *
1796
   * chr() only works for ASCII characters up to character 255. This function
1797
   * converts a number to the corresponding unicode character. Adapted from
1798
   * functions supplied in comments on several functions on php.net.
1799
   */
1800
  function code2utf($num) {
1801
    if ($num < 128) {
1802
      return chr($num);
1803
    }
1804

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

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

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

    
1817
    return '';
1818
  }
1819
}
1820

    
1821
/**
1822
 * Tests that we can embed a form in search results and submit it.
1823
 */
1824
class SearchEmbedForm extends DrupalWebTestCase {
1825
  /**
1826
   * Node used for testing.
1827
   */
1828
  public $node;
1829

    
1830
  /**
1831
   * Count of how many times the form has been submitted.
1832
   */
1833
  public $submit_count = 0;
1834

    
1835
  public static function getInfo() {
1836
    return array(
1837
      'name' => 'Embedded forms',
1838
      'description' => 'Verifies that a form embedded in search results works',
1839
      'group' => 'Search',
1840
    );
1841
  }
1842

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

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

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

    
1852
    node_update_index();
1853
    search_update_totals();
1854

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

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

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

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

    
1897
/**
1898
 * Tests that hook_search_page runs.
1899
 */
1900
class SearchPageOverride extends DrupalWebTestCase {
1901
  public $search_user;
1902

    
1903
  public static function getInfo() {
1904
    return array(
1905
      'name' => 'Search page override',
1906
      'description' => 'Verify that hook_search_page can override search page display.',
1907
      'group' => 'Search',
1908
    );
1909
  }
1910

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

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

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

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

    
1931
/**
1932
 * Test node search with multiple languages.
1933
 */
1934
class SearchLanguageTestCase extends DrupalWebTestCase {
1935
  public static function getInfo() {
1936
    return array(
1937
      'name' => 'Search language selection',
1938
      'description' => 'Tests advanced search with different languages enabled.',
1939
      'group' => 'Search',
1940
    );
1941
  }
1942

    
1943
  /**
1944
   * Implementation setUp().
1945
   */
1946
  function setUp() {
1947
    parent::setUp('search', 'locale');
1948

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

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

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

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

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

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

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

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

    
1996
/**
1997
 * Tests node search with node access control.
1998
 */
1999
class SearchNodeAccessTest extends DrupalWebTestCase {
2000
  public $test_user;
2001

    
2002
  public static function getInfo() {
2003
    return array(
2004
      'name' => 'Search and node access',
2005
      'description' => 'Tests search functionality with node access control.',
2006
      'group' => 'Search',
2007
    );
2008
  }
2009

    
2010
  function setUp() {
2011
    parent::setUp('search', 'node_access_test');
2012
    node_access_rebuild();
2013

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

    
2019
  /**
2020
   * Tests that search returns results with punctuation in the search phrase.
2021
   */
2022
  function testPhraseSearchPunctuation() {
2023
    $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy.")))));
2024

    
2025
    // Update the search index.
2026
    module_invoke_all('update_index');
2027
    search_update_totals();
2028

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

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

    
2039
/**
2040
 * Tests searching with locale values set.
2041
 */
2042
class SearchSetLocaleTest extends DrupalWebTestCase {
2043

    
2044
  public static function getInfo() {
2045
    return array(
2046
      'name' => 'Search with numeric locale set',
2047
      'description' => 'Check that search works with numeric locale settings',
2048
      'group' => 'Search',
2049
    );
2050
  }
2051

    
2052
  function setUp() {
2053
    parent::setUp('search');
2054

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

    
2061
    // Run cron to index.
2062
    $this->cronRun();
2063
  }
2064

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

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