Projet

Général

Profil

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

root / drupal7 / modules / search / search.test @ 76597ebf

1 85ad3d82 Assos Assos
<?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 4444412d Julien Enselme
/**
15
 * Indexes content and queries it.
16
 */
17 85ad3d82 Assos Assos
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 4444412d Julien Enselme
/**
314
 * Indexes content and tests the advanced search form.
315
 */
316 85ad3d82 Assos Assos
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 4444412d Julien Enselme
/**
380
 * Indexes content and tests ranking factors.
381
 */
382 85ad3d82 Assos Assos
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 4444412d Julien Enselme
/**
593
 * Tests the rendering of the search block.
594
 */
595 85ad3d82 Assos Assos
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
}
671
672
/**
673
 * Tests that searching for a phrase gets the correct page count.
674
 */
675
class SearchExactTestCase extends DrupalWebTestCase {
676
  public static function getInfo() {
677
    return array(
678
      'name' => 'Search engine phrase queries',
679
      'description' => 'Tests that searching for a phrase gets the correct page count.',
680
      'group' => 'Search',
681
    );
682
  }
683
684
  function setUp() {
685
    parent::setUp('search');
686
  }
687
688
  /**
689
   * Tests that the correct number of pager links are found for both keywords and phrases.
690
   */
691
  function testExactQuery() {
692
    // Login with sufficient privileges.
693
    $this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content')));
694
695
    $settings = array(
696
      'type' => 'page',
697
      'title' => 'Simple Node',
698
    );
699
    // Create nodes with exact phrase.
700
    for ($i = 0; $i <= 17; $i++) {
701
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza')));
702
      $this->drupalCreateNode($settings);
703
    }
704
    // Create nodes containing keywords.
705
    for ($i = 0; $i <= 17; $i++) {
706
      $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza')));
707
      $this->drupalCreateNode($settings);
708
    }
709
710
    // Update the search index.
711
    module_invoke_all('update_index');
712
    search_update_totals();
713
714
    // Refresh variables after the treatment.
715
    $this->refreshVariables();
716
717
    // Test that the correct number of pager links are found for keyword search.
718
    $edit = array('keys' => 'love pizza');
719
    $this->drupalPost('search/node', $edit, t('Search'));
720
    $this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
721
    $this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
722
    $this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
723
    $this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');
724
725
    // Test that the correct number of pager links are found for exact phrase search.
726
    $edit = array('keys' => '"love pizza"');
727
    $this->drupalPost('search/node', $edit, t('Search'));
728
    $this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
729
    $this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
730
  }
731
}
732
733
/**
734
 * Test integration searching comments.
735
 */
736
class SearchCommentTestCase extends DrupalWebTestCase {
737
  protected $admin_user;
738
739
  public static function getInfo() {
740
    return array(
741
      'name' => 'Comment Search tests',
742 4444412d Julien Enselme
      'description' => 'Test integration searching comments.',
743 85ad3d82 Assos Assos
      'group' => 'Search',
744
    );
745
  }
746
747
  function setUp() {
748
    parent::setUp('comment', 'search');
749
750
    // Create and log in an administrative user having access to the Full HTML
751
    // text format.
752
    $full_html_format = filter_format_load('full_html');
753
    $permissions = array(
754
      'administer filters',
755
      filter_permission_name($full_html_format),
756
      'administer permissions',
757
      'create page content',
758
      'skip comment approval',
759
      'access comments',
760
    );
761
    $this->admin_user = $this->drupalCreateUser($permissions);
762
    $this->drupalLogin($this->admin_user);
763
  }
764
765
  /**
766
   * Verify that comments are rendered using proper format in search results.
767
   */
768
  function testSearchResultsComment() {
769
    $comment_body = 'Test comment body';
770
771
    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
772
    // Enable check_plain() for 'Filtered HTML' text format.
773
    $filtered_html_format_id = 'filtered_html';
774
    $edit = array(
775
      'filters[filter_html_escape][status]' => TRUE,
776
    );
777
    $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration'));
778
    // Allow anonymous users to search content.
779
    $edit = array(
780
      DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
781
      DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
782
      DRUPAL_ANONYMOUS_RID . '[post comments]' => 1,
783
    );
784
    $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
785
786
    // Create a node.
787
    $node = $this->drupalCreateNode(array('type' => 'article'));
788
    // Post a comment using 'Full HTML' text format.
789
    $edit_comment = array();
790
    $edit_comment['subject'] = 'Test comment subject';
791
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
792
    $full_html_format_id = 'full_html';
793
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id;
794
    $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save'));
795
796
    // Invoke search index update.
797
    $this->drupalLogout();
798
    $this->cronRun();
799
800
    // Search for the comment subject.
801
    $edit = array(
802
      'search_block_form' => "'" . $edit_comment['subject'] . "'",
803
    );
804
    $this->drupalPost('', $edit, t('Search'));
805
    $this->assertText($node->title, 'Node found in search results.');
806
    $this->assertText($edit_comment['subject'], 'Comment subject found in search results.');
807
808
    // Search for the comment body.
809
    $edit = array(
810
      'search_block_form' => "'" . $comment_body . "'",
811
    );
812
    $this->drupalPost('', $edit, t('Search'));
813
    $this->assertText($node->title, 'Node found in search results.');
814
815
    // Verify that comment is rendered using proper format.
816
    $this->assertText($comment_body, 'Comment body text found in search results.');
817
    $this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.');
818
    $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), 'HTML in comment body is not escaped.');
819
820
    // Hide comments.
821
    $this->drupalLogin($this->admin_user);
822
    $node->comment = 0;
823
    node_save($node);
824
825
    // Invoke search index update.
826
    $this->drupalLogout();
827
    $this->cronRun();
828
829
    // Search for $title.
830
    $this->drupalPost('', $edit, t('Search'));
831
    $this->assertNoText($comment_body, 'Comment body text not found in search results.');
832
  }
833
834
  /**
835
   * Verify access rules for comment indexing with different permissions.
836
   */
837
  function testSearchResultsCommentAccess() {
838
    $comment_body = 'Test comment body';
839
    $this->comment_subject = 'Test comment subject';
840
    $this->admin_role = $this->admin_user->roles;
841
    unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]);
842
    $this->admin_role = key($this->admin_role);
843
844
    // Create a node.
845
    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
846
    $this->node = $this->drupalCreateNode(array('type' => 'article'));
847
848
    // Post a comment using 'Full HTML' text format.
849
    $edit_comment = array();
850
    $edit_comment['subject'] = $this->comment_subject;
851
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
852
    $this->drupalPost('comment/reply/' . $this->node->nid, $edit_comment, t('Save'));
853
854
    $this->drupalLogout();
855
    $this->setRolePermissions(DRUPAL_ANONYMOUS_RID);
856
    $this->checkCommentAccess('Anon user has search permission but no access comments permission, comments should not be indexed');
857
858
    $this->setRolePermissions(DRUPAL_ANONYMOUS_RID, TRUE);
859
    $this->checkCommentAccess('Anon user has search permission and access comments permission, comments should be indexed', TRUE);
860
861
    $this->drupalLogin($this->admin_user);
862
    $this->drupalGet('admin/people/permissions');
863
864
    // Disable search access for authenticated user to test admin user.
865
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, FALSE, FALSE);
866
867
    $this->setRolePermissions($this->admin_role);
868
    $this->checkCommentAccess('Admin user has search permission but no access comments permission, comments should not be indexed');
869
870
    $this->setRolePermissions($this->admin_role, TRUE);
871
    $this->checkCommentAccess('Admin user has search permission and access comments permission, comments should be indexed', TRUE);
872
873
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID);
874
    $this->checkCommentAccess('Authenticated user has search permission but no access comments permission, comments should not be indexed');
875
876
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE);
877
    $this->checkCommentAccess('Authenticated user has search permission and access comments permission, comments should be indexed', TRUE);
878
879
    // Verify that access comments permission is inherited from the
880
    // authenticated role.
881
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE);
882
    $this->setRolePermissions($this->admin_role);
883
    $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);
884
885
    // Verify that search content permission is inherited from the authenticated
886
    // role.
887
    $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE);
888
    $this->setRolePermissions($this->admin_role, TRUE, FALSE);
889
    $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);
890
891
  }
892
893
  /**
894
   * Set permissions for role.
895
   */
896
  function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
897
    $permissions = array(
898
      'access comments' => $access_comments,
899
      'search content' => $search_content,
900
    );
901
    user_role_change_permissions($rid, $permissions);
902
  }
903
904
  /**
905
   * Update search index and search for comment.
906
   */
907
  function checkCommentAccess($message, $assume_access = FALSE) {
908
    // Invoke search index update.
909
    search_touch_node($this->node->nid);
910
    $this->cronRun();
911
912
    // Search for the comment subject.
913
    $edit = array(
914
      'search_block_form' => "'" . $this->comment_subject . "'",
915
    );
916
    $this->drupalPost('', $edit, t('Search'));
917
    $method = $assume_access ? 'assertText' : 'assertNoText';
918
    $verb = $assume_access ? 'found' : 'not found';
919
    $this->{$method}($this->node->title, "Node $verb in search results: " . $message);
920
    $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message);
921
  }
922
923
  /**
924
   * Verify that 'add new comment' does not appear in search results or index.
925
   */
926
  function testAddNewComment() {
927
    // Create a node with a short body.
928
    $settings = array(
929
      'type' => 'article',
930
      'title' => 'short title',
931
      'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))),
932
    );
933
934
    $user = $this->drupalCreateUser(array('search content', 'create article content', 'access content'));
935
    $this->drupalLogin($user);
936
937
    $node = $this->drupalCreateNode($settings);
938
    // Verify that if you view the node on its own page, 'add new comment'
939
    // is there.
940
    $this->drupalGet('node/' . $node->nid);
941
    $this->assertText(t('Add new comment'), 'Add new comment appears on node page');
942
943
    // Run cron to index this page.
944
    $this->drupalLogout();
945
    $this->cronRun();
946
947
    // Search for 'comment'. Should be no results.
948
    $this->drupalLogin($user);
949
    $this->drupalPost('search/node', array('keys' => 'comment'), t('Search'));
950
    $this->assertText(t('Your search yielded no results'), 'No results searching for the word comment');
951
952
    // Search for the node title. Should be found, and 'Add new comment' should
953
    // not be part of the search snippet.
954
    $this->drupalPost('search/node', array('keys' => 'short'), t('Search'));
955
    $this->assertText($node->title, 'Search for keyword worked');
956
    $this->assertNoText(t('Add new comment'), 'Add new comment does not appear on search results page');
957
  }
958
959
}
960
961
/**
962
 * Tests search_expression_insert() and search_expression_extract().
963
 *
964
 * @see http://drupal.org/node/419388 (issue)
965
 */
966
class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase {
967
  public static function getInfo() {
968
    return array(
969
      'name' => 'Search expression insert/extract',
970
      'description' => 'Tests the functions search_expression_insert() and search_expression_extract()',
971
      'group' => 'Search',
972
    );
973
  }
974
975
  function setUp() {
976
    drupal_load('module', 'search');
977
    parent::setUp();
978
  }
979
980
  /**
981
   * Tests search_expression_insert() and search_expression_extract().
982
   */
983
  function testInsertExtract() {
984
    $base_expression = "mykeyword";
985
    // Build an array of option, value, what should be in the expression, what
986
    // should be retrieved from expression.
987
    $cases = array(
988
      array('foo', 'bar', 'foo:bar', 'bar'), // Normal case.
989
      array('foo', NULL, '', NULL), // Empty value: shouldn't insert.
990
      array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string.
991
      array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string.
992
      array('foo', '0', 'foo:0', '0'), // String zero as value: should insert.
993
      array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert.
994
    );
995
996
    foreach ($cases as $index => $case) {
997
      $after_insert = search_expression_insert($base_expression, $case[0], $case[1]);
998
      if (empty($case[2])) {
999
        $this->assertEqual($after_insert, $base_expression, "Empty insert does not change expression in case $index");
1000
      }
1001
      else {
1002
        $this->assertEqual($after_insert, $base_expression . ' ' . $case[2], "Insert added correct expression for case $index");
1003
      }
1004
1005
      $retrieved = search_expression_extract($after_insert, $case[0]);
1006
      if (!isset($case[3])) {
1007
        $this->assertFalse(isset($retrieved), "Empty retrieval results in unset value in case $index");
1008
      }
1009
      else {
1010
        $this->assertEqual($retrieved, $case[3], "Value is retrieved for case $index");
1011
      }
1012
1013
      $after_clear = search_expression_insert($after_insert, $case[0]);
1014
      $this->assertEqual(trim($after_clear), $base_expression, "After clearing, base expression is restored for case $index");
1015
1016
      $cleared = search_expression_extract($after_clear, $case[0]);
1017
      $this->assertFalse(isset($cleared), "After clearing, value could not be retrieved for case $index");
1018
    }
1019
  }
1020
}
1021
1022
/**
1023
 * Tests that comment count display toggles properly on comment status of node
1024
 *
1025
 * Issue 537278
1026
 *
1027
 * - Nodes with comment status set to Open should always how comment counts
1028
 * - Nodes with comment status set to Closed should show comment counts
1029
 *     only when there are comments
1030
 * - Nodes with comment status set to Hidden should never show comment counts
1031
 */
1032
class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
1033
  protected $searching_user;
1034
  protected $searchable_nodes;
1035
1036
  public static function getInfo() {
1037
    return array(
1038
      'name' => 'Comment count toggle',
1039
      'description' => 'Verify that comment count display toggles properly on comment status of node.',
1040
      'group' => 'Search',
1041
    );
1042
  }
1043
1044
  function setUp() {
1045
    parent::setUp('search');
1046
1047
    // Create searching user.
1048
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));
1049
1050
    // Create initial nodes.
1051
    $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase'))));
1052
1053
    $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params);
1054
    $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params);
1055
1056
    // Login with sufficient privileges.
1057
    $this->drupalLogin($this->searching_user);
1058
1059
    // Create a comment array
1060
    $edit_comment = array();
1061
    $edit_comment['subject'] = $this->randomName();
1062
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
1063
    $filtered_html_format_id = 'filtered_html';
1064
    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;
1065
1066
    // Post comment to the test node with comment
1067
    $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save'));
1068
1069
    // First update the index. This does the initial processing.
1070
    node_update_index();
1071
1072
    // Then, run the shutdown function. Testing is a unique case where indexing
1073
    // and searching has to happen in the same request, so running the shutdown
1074
    // function manually is needed to finish the indexing process.
1075
    search_update_totals();
1076
  }
1077
1078
  /**
1079
   * Verify that comment count display toggles properly on comment status of node
1080
   */
1081
  function testSearchCommentCountToggle() {
1082
    // Search for the nodes by string in the node body.
1083
    $edit = array(
1084
      'search_block_form' => "'SearchCommentToggleTestCase'",
1085
    );
1086
1087
    // Test comment count display for nodes with comment status set to Open
1088
    $this->drupalPost('', $edit, t('Search'));
1089
    $this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open');
1090
    $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open');
1091
1092
    // Test comment count display for nodes with comment status set to Closed
1093
    $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED;
1094
    node_save($this->searchable_nodes['0 comments']);
1095
    $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED;
1096
    node_save($this->searchable_nodes['1 comment']);
1097
1098
    $this->drupalPost('', $edit, t('Search'));
1099
    $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed');
1100
    $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed');
1101
1102
    // Test comment count display for nodes with comment status set to Hidden
1103
    $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN;
1104
    node_save($this->searchable_nodes['0 comments']);
1105
    $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN;
1106
    node_save($this->searchable_nodes['1 comment']);
1107
1108
    $this->drupalPost('', $edit, t('Search'));
1109
    $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden');
1110
    $this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden');
1111
  }
1112
}
1113
1114
/**
1115
 * Test search_simplify() on every Unicode character, and some other cases.
1116
 */
1117
class SearchSimplifyTestCase extends DrupalWebTestCase {
1118
  public static function getInfo() {
1119
    return array(
1120
      'name' => 'Search simplify',
1121
      'description' => 'Check that the search_simply() function works as intended.',
1122
      'group' => 'Search',
1123
    );
1124
  }
1125
1126
  /**
1127
   * Tests that all Unicode characters simplify correctly.
1128
   */
1129
  function testSearchSimplifyUnicode() {
1130
    // This test uses a file that was constructed so that the even lines are
1131
    // boundary characters, and the odd lines are valid word characters. (It
1132
    // was generated as a sequence of all the Unicode characters, and then the
1133
    // boundary chararacters (punctuation, spaces, etc.) were split off into
1134
    // their own lines).  So the even-numbered lines should simplify to nothing,
1135
    // and the odd-numbered lines we need to split into shorter chunks and
1136
    // verify that simplification doesn't lose any characters.
1137
    $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
1138
    $basestrings = explode(chr(10), $input);
1139
    $strings = array();
1140
    foreach ($basestrings as $key => $string) {
1141
      if ($key %2) {
1142
        // Even line - should simplify down to a space.
1143
        $simplified = search_simplify($string);
1144
        $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
1145
      }
1146
      else {
1147
        // Odd line, should be word characters.
1148
        // Split this into 30-character chunks, so we don't run into limits
1149
        // of truncation in search_simplify().
1150
        $start = 0;
1151
        while ($start < drupal_strlen($string)) {
1152
          $newstr = drupal_substr($string, $start, 30);
1153
          // Special case: leading zeros are removed from numeric strings,
1154
          // and there's one string in this file that is numbers starting with
1155
          // zero, so prepend a 1 on that string.
1156
          if (preg_match('/^[0-9]+$/', $newstr)) {
1157
            $newstr = '1' . $newstr;
1158
          }
1159
          $strings[] = $newstr;
1160
          $start += 30;
1161
        }
1162
      }
1163
    }
1164
    foreach ($strings as $key => $string) {
1165
      $simplified = search_simplify($string);
1166
      $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key.");
1167
    }
1168
1169
    // Test the low-numbered ASCII control characters separately. They are not
1170
    // in the text file because they are problematic for diff, especially \0.
1171
    $string = '';
1172
    for ($i = 0; $i < 32; $i++) {
1173
      $string .= chr($i);
1174
    }
1175
    $this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.');
1176
  }
1177
1178
  /**
1179
   * Tests that search_simplify() does the right thing with punctuation.
1180
   */
1181
  function testSearchSimplifyPunctuation() {
1182
    $cases = array(
1183
      array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'),
1184
      array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'),
1185
      array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'),
1186
      array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'),
1187
    );
1188
1189
    foreach ($cases as $case) {
1190
      $out = trim(search_simplify($case[0]));
1191
      $this->assertEqual($out, $case[1], $case[2]);
1192
    }
1193
  }
1194
}
1195
1196
1197
/**
1198
 * Tests keywords and conditions.
1199
 */
1200
class SearchKeywordsConditions extends DrupalWebTestCase {
1201
1202
  public static function getInfo() {
1203
    return array(
1204
      'name' => 'Keywords and conditions',
1205
      'description' => 'Verify the search pulls in keywords and extra conditions.',
1206
      'group' => 'Search',
1207
    );
1208
  }
1209
1210
  function setUp() {
1211
    parent::setUp('search', 'search_extra_type');
1212
    // Create searching user.
1213
    $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));
1214
    // Login with sufficient privileges.
1215
    $this->drupalLogin($this->searching_user);
1216
    // Test with all search modules enabled.
1217
    variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'));
1218
    menu_rebuild();
1219
  }
1220
1221
  /**
1222
   * Verify the kewords are captured and conditions respected.
1223
   */
1224
  function testSearchKeyswordsConditions() {
1225
    // No keys, not conditions - no results.
1226
    $this->drupalGet('search/dummy_path');
1227
    $this->assertNoText('Dummy search snippet to display');
1228
    // With keys - get results.
1229
    $keys = 'bike shed ' . $this->randomName();
1230
    $this->drupalGet("search/dummy_path/{$keys}");
1231
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
1232
    $keys = 'blue drop ' . $this->randomName();
1233
    $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
1234
    $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
1235
    // Add some conditions and keys.
1236
    $keys = 'moving drop ' . $this->randomName();
1237
    $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys)));
1238
    $this->assertText("Dummy search snippet to display.");
1239
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
1240
    // Add some conditions and no keys.
1241
    $keys = 'drop kick ' . $this->randomName();
1242
    $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys)));
1243
    $this->assertText("Dummy search snippet to display.");
1244
    $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
1245
  }
1246
}
1247
1248
/**
1249
 * Tests that numbers can be searched.
1250
 */
1251
class SearchNumbersTestCase extends DrupalWebTestCase {
1252
  protected $test_user;
1253
  protected $numbers;
1254
  protected $nodes;
1255
1256
  public static function getInfo() {
1257
    return array(
1258
      'name' => 'Search numbers',
1259
      'description' => 'Check that numbers can be searched',
1260
      'group' => 'Search',
1261
    );
1262
  }
1263
1264
  function setUp() {
1265
    parent::setUp('search');
1266
1267
    $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
1268
    $this->drupalLogin($this->test_user);
1269
1270
    // Create content with various numbers in it.
1271
    // Note: 50 characters is the current limit of the search index's word
1272
    // field.
1273
    $this->numbers = array(
1274
      'ISBN' => '978-0446365383',
1275
      'UPC' => '036000 291452',
1276
      'EAN bar code' => '5901234123457',
1277
      'negative' => '-123456.7890',
1278
      'quoted negative' => '"-123456.7890"',
1279
      'leading zero' => '0777777777',
1280
      'tiny' => '111',
1281
      'small' => '22222222222222',
1282
      'medium' => '333333333333333333333333333',
1283
      'large' => '444444444444444444444444444444444444444',
1284
      'gigantic' => '5555555555555555555555555555555555555555555555555',
1285
      'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
1286
      'date', '01/02/2009',
1287
      'commas', '987,654,321',
1288
    );
1289
1290
    foreach ($this->numbers as $doc => $num) {
1291
      $info = array(
1292
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
1293
        'type' => 'page',
1294
        'language' => LANGUAGE_NONE,
1295
        'title' => $doc . ' number',
1296
      );
1297
      $this->nodes[$doc] = $this->drupalCreateNode($info);
1298
    }
1299
1300
    // Run cron to ensure the content is indexed.
1301
    $this->cronRun();
1302
    $this->drupalGet('admin/reports/dblog');
1303
    $this->assertText(t('Cron run completed'), 'Log shows cron run completed');
1304
  }
1305
1306
  /**
1307
   * Tests that all the numbers can be searched.
1308
   */
1309
  function testNumberSearching() {
1310
    $types = array_keys($this->numbers);
1311
1312
    foreach ($types as $type) {
1313
      $number = $this->numbers[$type];
1314
      // If the number is negative, remove the - sign, because - indicates
1315
      // "not keyword" when searching.
1316
      $number = ltrim($number, '-');
1317
      $node = $this->nodes[$type];
1318
1319
      // Verify that the node title does not appear on the search page
1320
      // with a dummy search.
1321
      $this->drupalPost('search/node',
1322
        array('keys' => 'foo'),
1323
        t('Search'));
1324
      $this->assertNoText($node->title, $type . ': node title not shown in dummy search');
1325
1326
      // Verify that the node title does appear as a link on the search page
1327
      // when searching for the number.
1328
      $this->drupalPost('search/node',
1329
        array('keys' => $number),
1330
        t('Search'));
1331
      $this->assertText($node->title, format_string('%type: node title shown (search found the node) in search for number %number.', array('%type' => $type, '%number' => $number)));
1332
    }
1333
  }
1334
}
1335
1336
/**
1337
 * Tests that numbers can be searched, with more complex matching.
1338
 */
1339
class SearchNumberMatchingTestCase extends DrupalWebTestCase {
1340
  protected $test_user;
1341
  protected $numbers;
1342
  protected $nodes;
1343
1344
  public static function getInfo() {
1345
    return array(
1346
      'name' => 'Search number matching',
1347
      'description' => 'Check that numbers can be searched with more complex matching',
1348
      'group' => 'Search',
1349
    );
1350
  }
1351
1352
  function setUp() {
1353
    parent::setUp('search');
1354
1355
    $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
1356
    $this->drupalLogin($this->test_user);
1357
1358
    // Define a group of numbers that should all match each other --
1359
    // numbers with internal punctuation should match each other, as well
1360
    // as numbers with and without leading zeros and leading/trailing
1361
    // . and -.
1362
    $this->numbers = array(
1363
      '123456789',
1364
      '12/34/56789',
1365
      '12.3456789',
1366
      '12-34-56789',
1367
      '123,456,789',
1368
      '-123456789',
1369
      '0123456789',
1370
    );
1371
1372
    foreach ($this->numbers as $num) {
1373
      $info = array(
1374
        'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
1375
        'type' => 'page',
1376
        'language' => LANGUAGE_NONE,
1377
      );
1378
      $this->nodes[] = $this->drupalCreateNode($info);
1379
    }
1380
1381
    // Run cron to ensure the content is indexed.
1382
    $this->cronRun();
1383
    $this->drupalGet('admin/reports/dblog');
1384
    $this->assertText(t('Cron run completed'), 'Log shows cron run completed');
1385
  }
1386
1387
  /**
1388
   * Tests that all the numbers can be searched.
1389
   */
1390
  function testNumberSearching() {
1391
    for ($i = 0; $i < count($this->numbers); $i++) {
1392
      $node = $this->nodes[$i];
1393
1394
      // Verify that the node title does not appear on the search page
1395
      // with a dummy search.
1396
      $this->drupalPost('search/node',
1397
        array('keys' => 'foo'),
1398
        t('Search'));
1399
      $this->assertNoText($node->title, format_string('%number: node title not shown in dummy search', array('%number' => $i)));
1400
1401
      // Now verify that we can find node i by searching for any of the
1402
      // numbers.
1403
      for ($j = 0; $j < count($this->numbers); $j++) {
1404
        $number = $this->numbers[$j];
1405
        // If the number is negative, remove the - sign, because - indicates
1406
        // "not keyword" when searching.
1407
        $number = ltrim($number, '-');
1408
1409
        $this->drupalPost('search/node',
1410
          array('keys' => $number),
1411
          t('Search'));
1412
        $this->assertText($node->title, format_string('%i: node title shown (search found the node) in search for number %number', array('%i' => $i, '%number' => $number)));
1413
      }
1414
    }
1415
1416
  }
1417
}
1418
1419
/**
1420
 * Test config page.
1421
 */
1422
class SearchConfigSettingsForm extends DrupalWebTestCase {
1423
  public $search_user;
1424
  public $search_node;
1425
1426
  public static function getInfo() {
1427
    return array(
1428
      'name' => 'Config settings form',
1429
      'description' => 'Verify the search config settings form.',
1430
      'group' => 'Search',
1431
    );
1432
  }
1433
1434
  function setUp() {
1435
    parent::setUp('search', 'search_extra_type');
1436
1437
    // Login as a user that can create and search content.
1438
    $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks'));
1439
    $this->drupalLogin($this->search_user);
1440
1441
    // Add a single piece of content and index it.
1442
    $node = $this->drupalCreateNode();
1443
    $this->search_node = $node;
1444
    // Link the node to itself to test that it's only indexed once. The content
1445
    // also needs the word "pizza" so we can use it as the search keyword.
1446
    $langcode = LANGUAGE_NONE;
1447
    $body_key = "body[$langcode][0][value]";
1448
    $edit[$body_key] = l($node->title, 'node/' . $node->nid) . ' pizza sandwich';
1449
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
1450
1451
    node_update_index();
1452
    search_update_totals();
1453
1454
    // Enable the search block.
1455
    $edit = array();
1456
    $edit['blocks[search_form][region]'] = 'content';
1457
    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
1458
  }
1459
1460
  /**
1461
   * Verify the search settings form.
1462
   */
1463
  function testSearchSettingsPage() {
1464
1465
    // Test that the settings form displays the correct count of items left to index.
1466
    $this->drupalGet('admin/config/search/settings');
1467
    $this->assertText(t('There are @count items left to index.', array('@count' => 0)));
1468
1469
    // Test the re-index button.
1470
    $this->drupalPost('admin/config/search/settings', array(), t('Re-index site'));
1471
    $this->assertText(t('Are you sure you want to re-index the site'));
1472
    $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site'));
1473
    $this->assertText(t('The index will be rebuilt'));
1474
    $this->drupalGet('admin/config/search/settings');
1475
    $this->assertText(t('There is 1 item left to index.'));
1476
1477
    // Test that the form saves with the default values.
1478
    $this->drupalPost('admin/config/search/settings', array(), t('Save configuration'));
1479
    $this->assertText(t('The configuration options have been saved.'), 'Form saves with the default values.');
1480
1481
    // Test that the form does not save with an invalid word length.
1482
    $edit = array(
1483
      'minimum_word_size' => $this->randomName(3),
1484
    );
1485
    $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
1486
    $this->assertNoText(t('The configuration options have been saved.'), 'Form does not save with an invalid word length.');
1487
  }
1488
1489
  /**
1490
   * Verify that you can disable individual search modules.
1491
   */
1492
  function testSearchModuleDisabling() {
1493
    // Array of search modules to test: 'path' is the search path, 'title' is
1494
    // the tab title, 'keys' are the keywords to search for, and 'text' is
1495
    // the text to assert is on the results page.
1496
    $module_info = array(
1497
      'node' => array(
1498
        'path' => 'node',
1499
        'title' => 'Content',
1500
        'keys' => 'pizza',
1501
        'text' => $this->search_node->title,
1502
      ),
1503
      'user' => array(
1504
        'path' => 'user',
1505
        'title' => 'User',
1506
        'keys' => $this->search_user->name,
1507
        'text' => $this->search_user->mail,
1508
      ),
1509
      'search_extra_type' => array(
1510
        'path' => 'dummy_path',
1511
        'title' => 'Dummy search type',
1512
        'keys' => 'foo',
1513
        'text' => 'Dummy search snippet to display',
1514
      ),
1515
    );
1516
    $modules = array_keys($module_info);
1517
1518
    // Test each module if it's enabled as the only search module.
1519
    foreach ($modules as $module) {
1520
      // Enable the one module and disable other ones.
1521
      $info = $module_info[$module];
1522
      $edit = array();
1523
      foreach ($modules as $other) {
1524
        $edit['search_active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE);
1525
      }
1526
      $edit['search_default_module'] = $module;
1527
      $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
1528
1529
      // Run a search from the correct search URL.
1530
      $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']);
1531
      $this->assertNoText('no results', $info['title'] . ' search found results');
1532
      $this->assertText($info['text'], 'Correct search text found');
1533
1534
      // Verify that other module search tab titles are not visible.
1535
      foreach ($modules as $other) {
1536
        if ($other != $module) {
1537
          $title = $module_info[$other]['title'];
1538
          $this->assertNoText($title, $title . ' search tab is not shown');
1539
        }
1540
      }
1541
1542
      // Run a search from the search block on the node page. Verify you get
1543
      // to this module's search results page.
1544
      $terms = array('search_block_form' => $info['keys']);
1545
      $this->drupalPost('node', $terms, t('Search'));
1546
      $this->assertEqual(
1547
        $this->getURL(),
1548
        url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)),
1549
        'Block redirected to right search page');
1550
1551
      // Try an invalid search path. Should redirect to our active module.
1552
      $this->drupalGet('search/not_a_module_path');
1553
      $this->assertEqual(
1554
        $this->getURL(),
1555
        url('search/' . $info['path'], array('absolute' => TRUE)),
1556
        'Invalid search path redirected to default search page');
1557
    }
1558
1559
    // Test with all search modules enabled. When you go to the search
1560
    // page or run search, all modules should be shown.
1561
    $edit = array();
1562
    foreach ($modules as $module) {
1563
      $edit['search_active_modules[' . $module . ']'] = $module;
1564
    }
1565
    $edit['search_default_module'] = 'node';
1566
1567
    $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
1568
1569
    foreach (array('search/node/pizza', 'search/node') as $path) {
1570
      $this->drupalGet($path);
1571
      foreach ($modules as $module) {
1572
        $title = $module_info[$module]['title'];
1573
        $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
1574
      }
1575
    }
1576
  }
1577
}
1578
1579
/**
1580
 * Tests the search_excerpt() function.
1581
 */
1582
class SearchExcerptTestCase extends DrupalWebTestCase {
1583
  public static function getInfo() {
1584
    return array(
1585
      'name' => 'Search excerpt extraction',
1586
      'description' => 'Tests that the search_excerpt() function works.',
1587
      'group' => 'Search',
1588
    );
1589
  }
1590
1591
  function setUp() {
1592
    parent::setUp('search');
1593
  }
1594
1595
  /**
1596
   * Tests search_excerpt() with several simulated search keywords.
1597
   *
1598
   * Passes keywords and a sample marked up string, "The quick
1599
   * brown fox jumps over the lazy dog", and compares it to the
1600
   * correctly marked up string. The correctly marked up string
1601
   * contains either highlighted keywords or the original marked
1602
   * up string if no keywords matched the string.
1603
   */
1604
  function testSearchExcerpt() {
1605
    // Make some text with entities and tags.
1606
    $text = 'The <strong>quick</strong> <a href="#">brown</a> fox &amp; jumps <h2>over</h2> the lazy dog';
1607
    // Note: The search_excerpt() function adds some extra spaces -- not
1608
    // important for HTML formatting. Remove these for comparison.
1609
    $expected = 'The quick brown fox &amp; jumps over the lazy dog';
1610
    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
1611
    $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string');
1612
1613
    $result = preg_replace('| +|', ' ', search_excerpt('fox', $text));
1614
    $this->assertEqual($result, 'The quick brown <strong>fox</strong> &amp; jumps over the lazy dog ...', 'Found keyword is highlighted');
1615
1616
    $longtext = str_repeat($text . ' ', 10);
1617
    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $longtext));
1618
    $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
1619
1620
    $entities = str_repeat('k&eacute;sz&iacute;t&eacute;se ', 20);
1621
    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities));
1622
    $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
1623
    $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
1624
1625
    // The node body that will produce this rendered $text is:
1626
    // 123456789 HTMLTest +123456789+&lsquo;  +&lsquo;  +&lsquo;  +&lsquo;  +12345678  &nbsp;&nbsp;  +&lsquo;  +&lsquo;  +&lsquo;   &lsquo;
1627
    $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> ";
1628
    $result = search_excerpt('HTMLTest', $text);
1629
    $this->assertFalse(empty($result),  'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
1630
  }
1631
1632
  /**
1633
   * Tests search_excerpt() with search keywords matching simplified words.
1634
   *
1635
   * Excerpting should handle keywords that are matched only after going through
1636
   * search_simplify(). This test passes keywords that match simplified words
1637
   * and compares them with strings that contain the original unsimplified word.
1638
   */
1639
  function testSearchExcerptSimplified() {
1640
    $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.';
1641
    $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.';
1642
1643
    // Make some text with some keywords that will get simplified.
1644
    $text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
1645
    // Note: The search_excerpt() function adds some extra spaces -- not
1646
    // important for HTML formatting. Remove these for comparison.
1647
    $result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text));
1648
    $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');
1649
1650
    $result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text));
1651
    $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');
1652
1653
    $result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text));
1654
    $this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');
1655
1656
    $result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text));
1657
    $this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');
1658
1659
    $result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text));
1660
    $this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');
1661
1662
    $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text));
1663
    $this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');
1664
1665
    // Test phrases with characters which are being truncated.
1666
    $result = preg_replace('| +|', ' ', search_excerpt('"ipsum _"', $text));
1667
    $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');
1668
1669
    $result = preg_replace('| +|', ' ', search_excerpt('"ipsum 0000"', $text));
1670
    $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');
1671
1672
    // Test combination of the valid keyword and keyword containing only
1673
    // characters which are being truncated during simplification.
1674
    $result = preg_replace('| +|', ' ', search_excerpt('ipsum _', $text));
1675
    $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');
1676
1677
    $result = preg_replace('| +|', ' ', search_excerpt('ipsum 0000', $text));
1678
    $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
1679
  }
1680
}
1681
1682
/**
1683
 * Test the CJK tokenizer.
1684
 */
1685
class SearchTokenizerTestCase extends DrupalWebTestCase {
1686
  public static function getInfo() {
1687
    return array(
1688
      'name' => 'CJK tokenizer',
1689
      'description' => 'Check that CJK tokenizer works as intended.',
1690
      'group' => 'Search',
1691
    );
1692
  }
1693
1694
  function setUp() {
1695
    parent::setUp('search');
1696
  }
1697
1698
  /**
1699
   * Verifies that strings of CJK characters are tokenized.
1700
   *
1701
   * The search_simplify() function does special things with numbers, symbols,
1702
   * and punctuation. So we only test that CJK characters that are not in these
1703
   * character classes are tokenized properly. See PREG_CLASS_CKJ for more
1704
   * information.
1705
   */
1706
  function testTokenizer() {
1707
    // Set the minimum word size to 1 (to split all CJK characters) and make
1708
    // sure CJK tokenizing is turned on.
1709
    variable_set('minimum_word_size', 1);
1710
    variable_set('overlap_cjk', TRUE);
1711
    $this->refreshVariables();
1712
1713
    // Create a string of CJK characters from various character ranges in
1714
    // the Unicode tables.
1715
1716
    // Beginnings of the character ranges.
1717
    $starts = array(
1718
      'CJK unified' => 0x4e00,
1719
      'CJK Ext A' => 0x3400,
1720
      'CJK Compat' => 0xf900,
1721
      'Hangul Jamo' => 0x1100,
1722
      'Hangul Ext A' => 0xa960,
1723
      'Hangul Ext B' => 0xd7b0,
1724
      'Hangul Compat' => 0x3131,
1725
      'Half non-punct 1' => 0xff21,
1726
      'Half non-punct 2' => 0xff41,
1727
      'Half non-punct 3' => 0xff66,
1728
      'Hangul Syllables' => 0xac00,
1729
      'Hiragana' => 0x3040,
1730
      'Katakana' => 0x30a1,
1731
      'Katakana Ext' => 0x31f0,
1732
      'CJK Reserve 1' => 0x20000,
1733
      'CJK Reserve 2' => 0x30000,
1734
      'Bomofo' => 0x3100,
1735
      'Bomofo Ext' => 0x31a0,
1736
      'Lisu' => 0xa4d0,
1737
      'Yi' => 0xa000,
1738
    );
1739
1740
    // Ends of the character ranges.
1741
    $ends = array(
1742
      'CJK unified' => 0x9fcf,
1743
      'CJK Ext A' => 0x4dbf,
1744
      'CJK Compat' => 0xfaff,
1745
      'Hangul Jamo' => 0x11ff,
1746
      'Hangul Ext A' => 0xa97f,
1747
      'Hangul Ext B' => 0xd7ff,
1748
      'Hangul Compat' => 0x318e,
1749
      'Half non-punct 1' => 0xff3a,
1750
      'Half non-punct 2' => 0xff5a,
1751
      'Half non-punct 3' => 0xffdc,
1752
      'Hangul Syllables' => 0xd7af,
1753
      'Hiragana' => 0x309f,
1754
      'Katakana' => 0x30ff,
1755
      'Katakana Ext' => 0x31ff,
1756
      'CJK Reserve 1' => 0x2fffd,
1757
      'CJK Reserve 2' => 0x3fffd,
1758
      'Bomofo' => 0x312f,
1759
      'Bomofo Ext' => 0x31b7,
1760
      'Lisu' => 0xa4fd,
1761
      'Yi' => 0xa48f,
1762
    );
1763
1764
    // Generate characters consisting of starts, midpoints, and ends.
1765
    $chars = array();
1766
    $charcodes = array();
1767
    foreach ($starts as $key => $value) {
1768
      $charcodes[] = $starts[$key];
1769
      $chars[] = $this->code2utf($starts[$key]);
1770
      $mid = round(0.5 * ($starts[$key] + $ends[$key]));
1771
      $charcodes[] = $mid;
1772
      $chars[] = $this->code2utf($mid);
1773
      $charcodes[] = $ends[$key];
1774
      $chars[] = $this->code2utf($ends[$key]);
1775
    }
1776
1777
    // Merge into a string and tokenize.
1778
    $string = implode('', $chars);
1779
    $out = trim(search_simplify($string));
1780
    $expected = drupal_strtolower(implode(' ', $chars));
1781
1782
    // Verify that the output matches what we expect.
1783
    $this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
1784
  }
1785
1786
  /**
1787
   * Verifies that strings of non-CJK characters are not tokenized.
1788
   *
1789
   * This is just a sanity check - it verifies that strings of letters are
1790
   * not tokenized.
1791
   */
1792
  function testNoTokenizer() {
1793
    // Set the minimum word size to 1 (to split all CJK characters) and make
1794
    // sure CJK tokenizing is turned on.
1795
    variable_set('minimum_word_size', 1);
1796
    variable_set('overlap_cjk', TRUE);
1797
    $this->refreshVariables();
1798
1799
    $letters = 'abcdefghijklmnopqrstuvwxyz';
1800
    $out = trim(search_simplify($letters));
1801
1802
    $this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
1803
  }
1804
1805
  /**
1806
   * Like PHP chr() function, but for unicode characters.
1807
   *
1808
   * chr() only works for ASCII characters up to character 255. This function
1809
   * converts a number to the corresponding unicode character. Adapted from
1810
   * functions supplied in comments on several functions on php.net.
1811
   */
1812
  function code2utf($num) {
1813
    if ($num < 128) {
1814
      return chr($num);
1815
    }
1816
1817
    if ($num < 2048) {
1818
      return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1819
    }
1820
1821
    if ($num < 65536) {
1822
      return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1823
    }
1824
1825
    if ($num < 2097152) {
1826
      return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1827
    }
1828
1829
    return '';
1830
  }
1831
}
1832
1833
/**
1834
 * Tests that we can embed a form in search results and submit it.
1835
 */
1836
class SearchEmbedForm extends DrupalWebTestCase {
1837
  /**
1838
   * Node used for testing.
1839
   */
1840
  public $node;
1841
1842
  /**
1843
   * Count of how many times the form has been submitted.
1844
   */
1845
  public $submit_count = 0;
1846
1847
  public static function getInfo() {
1848
    return array(
1849
      'name' => 'Embedded forms',
1850
      'description' => 'Verifies that a form embedded in search results works',
1851
      'group' => 'Search',
1852
    );
1853
  }
1854
1855
  function setUp() {
1856
    parent::setUp('search', 'search_embedded_form');
1857
1858
    // Create a user and a node, and update the search index.
1859
    $test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes'));
1860
    $this->drupalLogin($test_user);
1861
1862
    $this->node = $this->drupalCreateNode();
1863
1864
    node_update_index();
1865
    search_update_totals();
1866
1867
    // Set up a dummy initial count of times the form has been submitted.
1868
    $this->submit_count = 12;
1869
    variable_set('search_embedded_form_submitted', $this->submit_count);
1870
    $this->refreshVariables();
1871
  }
1872
1873
  /**
1874
   * Tests that the embedded form appears and can be submitted.
1875
   */
1876
  function testEmbeddedForm() {
1877
    // First verify we can submit the form from the module's page.
1878
    $this->drupalPost('search_embedded_form',
1879
      array('name' => 'John'),
1880
      t('Send away'));
1881
    $this->assertText(t('Test form was submitted'), 'Form message appears');
1882
    $count = variable_get('search_embedded_form_submitted', 0);
1883
    $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct');
1884
    $this->submit_count = $count;
1885
1886
    // Now verify that we can see and submit the form from the search results.
1887
    $this->drupalGet('search/node/' . $this->node->title);
1888
    $this->assertText(t('Your name'), 'Form is visible');
1889
    $this->drupalPost('search/node/' . $this->node->title,
1890
      array('name' => 'John'),
1891
      t('Send away'));
1892
    $this->assertText(t('Test form was submitted'), 'Form message appears');
1893
    $count = variable_get('search_embedded_form_submitted', 0);
1894
    $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct');
1895
    $this->submit_count = $count;
1896
1897
    // Now verify that if we submit the search form, it doesn't count as
1898
    // our form being submitted.
1899
    $this->drupalPost('search',
1900
      array('keys' => 'foo'),
1901
      t('Search'));
1902
    $this->assertNoText(t('Test form was submitted'), 'Form message does not appear');
1903
    $count = variable_get('search_embedded_form_submitted', 0);
1904
    $this->assertEqual($this->submit_count, $count, 'Form submission count is correct');
1905
    $this->submit_count = $count;
1906
  }
1907
}
1908
1909
/**
1910
 * Tests that hook_search_page runs.
1911
 */
1912
class SearchPageOverride extends DrupalWebTestCase {
1913
  public $search_user;
1914
1915
  public static function getInfo() {
1916
    return array(
1917
      'name' => 'Search page override',
1918
      'description' => 'Verify that hook_search_page can override search page display.',
1919
      'group' => 'Search',
1920
    );
1921
  }
1922
1923
  function setUp() {
1924
    parent::setUp('search', 'search_extra_type');
1925
1926
    // Login as a user that can create and search content.
1927
    $this->search_user = $this->drupalCreateUser(array('search content', 'administer search'));
1928
    $this->drupalLogin($this->search_user);
1929
1930
    // Enable the extra type module for searching.
1931
    variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'));
1932
    menu_rebuild();
1933
  }
1934
1935
  function testSearchPageHook() {
1936
    $keys = 'bike shed ' . $this->randomName();
1937
    $this->drupalGet("search/dummy_path/{$keys}");
1938
    $this->assertText('Dummy search snippet', 'Dummy search snippet is shown');
1939
    $this->assertText('Test page text is here', 'Page override is working');
1940
  }
1941
}
1942
1943
/**
1944
 * Test node search with multiple languages.
1945
 */
1946
class SearchLanguageTestCase extends DrupalWebTestCase {
1947
  public static function getInfo() {
1948
    return array(
1949
      'name' => 'Search language selection',
1950
      'description' => 'Tests advanced search with different languages enabled.',
1951
      'group' => 'Search',
1952
    );
1953
  }
1954
1955
  /**
1956
   * Implementation setUp().
1957
   */
1958
  function setUp() {
1959
    parent::setUp('search', 'locale');
1960
1961
    // Create and login user.
1962
    $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages'));
1963
    $this->drupalLogin($test_user);
1964
  }
1965
1966
  function testLanguages() {
1967
    // Check that there are initially no languages displayed.
1968
    $this->drupalGet('search/node');
1969
    $this->assertNoText(t('Languages'), 'No languages to choose from.');
1970
1971
    // Add predefined language.
1972
    $edit = array('langcode' => 'fr');
1973
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
1974
    $this->assertText('fr', 'Language added successfully.');
1975
1976
    // Now we should have languages displayed.
1977
    $this->drupalGet('search/node');
1978
    $this->assertText(t('Languages'), 'Languages displayed to choose from.');
1979
    $this->assertText(t('English'), 'English is a possible choice.');
1980
    $this->assertText(t('French'), 'French is a possible choice.');
1981
1982
    // Ensure selecting no language does not make the query different.
1983
    $this->drupalPost('search/node', array(), t('Advanced search'));
1984
    $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), 'Correct page redirection, no language filtering.');
1985
1986
    // Pick French and ensure it is selected.
1987
    $edit = array('language[fr]' => TRUE);
1988
    $this->drupalPost('search/node', $edit, t('Advanced search'));
1989
    $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', 'Language filter added to query.');
1990
1991
    // Change the default language and disable English.
1992
    $path = 'admin/config/regional/language';
1993
    $this->drupalGet($path);
1994
    $this->assertFieldChecked('edit-site-default-en', 'English is the default language.');
1995
    $edit = array('site_default' => 'fr');
1996
    $this->drupalPost(NULL, $edit, t('Save configuration'));
1997
    $this->assertNoFieldChecked('edit-site-default-en', 'Default language updated.');
1998
    $edit = array('enabled[en]' => FALSE);
1999
    $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration'));
2000
    $this->assertNoFieldChecked('edit-enabled-en', 'Language disabled.');
2001
2002
    // Check that there are again no languages displayed.
2003
    $this->drupalGet('search/node');
2004
    $this->assertNoText(t('Languages'), 'No languages to choose from.');
2005
  }
2006
}
2007
2008
/**
2009
 * Tests node search with node access control.
2010
 */
2011
class SearchNodeAccessTest extends DrupalWebTestCase {
2012
  public $test_user;
2013
2014
  public static function getInfo() {
2015
    return array(
2016
      'name' => 'Search and node access',
2017
      'description' => 'Tests search functionality with node access control.',
2018
      'group' => 'Search',
2019
    );
2020
  }
2021
2022
  function setUp() {
2023
    parent::setUp('search', 'node_access_test');
2024
    node_access_rebuild();
2025
2026
    // Create a test user and log in.
2027
    $this->test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search'));
2028
    $this->drupalLogin($this->test_user);
2029
  }
2030
2031
  /**
2032
   * Tests that search returns results with punctuation in the search phrase.
2033
   */
2034
  function testPhraseSearchPunctuation() {
2035
    $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy.")))));
2036
2037
    // Update the search index.
2038
    module_invoke_all('update_index');
2039
    search_update_totals();
2040
2041
    // Refresh variables after the treatment.
2042
    $this->refreshVariables();
2043
2044
    // Submit a phrase wrapped in double quotes to include the punctuation.
2045
    $edit = array('keys' => '"bunny\'s"');
2046
    $this->drupalPost('search/node', $edit, t('Search'));
2047
    $this->assertText($node->title);
2048
  }
2049
}
2050
2051
/**
2052
 * Tests searching with locale values set.
2053
 */
2054
class SearchSetLocaleTest extends DrupalWebTestCase {
2055
2056
  public static function getInfo() {
2057
    return array(
2058
      'name' => 'Search with numeric locale set',
2059
      'description' => 'Check that search works with numeric locale settings',
2060
      'group' => 'Search',
2061
    );
2062
  }
2063
2064
  function setUp() {
2065
    parent::setUp('search');
2066
2067
    // Create a simple node so something will be put in the index.
2068
    $info = array(
2069
      'body' => array(LANGUAGE_NONE => array(array('value' => 'Tapir'))),
2070
    );
2071
    $this->drupalCreateNode($info);
2072
2073
    // Run cron to index.
2074
    $this->cronRun();
2075
  }
2076
2077
  /**
2078
   * Verify that search works with a numeric locale set.
2079
   */
2080
  public function testSearchWithNumericLocale() {
2081
    // French decimal point is comma.
2082
    setlocale(LC_NUMERIC, 'fr_FR');
2083
2084
    // An exception will be thrown if a float in the wrong format occurs in the
2085
    // query to the database, so an assertion is not necessary here.
2086
    db_select('search_index', 'i')
2087
      ->extend('searchquery')
2088
      ->searchexpression('tapir', 'node')
2089
      ->execute();
2090
  }
2091
}