Merge branch 'MDL-55755_master' of git://github.com/dmonllao/moodle
[moodle.git] / search / engine / solr / tests / engine_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Solr earch engine base unit tests.
19  *
20  * Required params:
21  * - define('TEST_SEARCH_SOLR_HOSTNAME', '127.0.0.1');
22  * - define('TEST_SEARCH_SOLR_PORT', '8983');
23  * - define('TEST_SEARCH_SOLR_INDEXNAME', 'unittest');
24  *
25  * Optional params:
26  * - define('TEST_SEARCH_SOLR_USERNAME', '');
27  * - define('TEST_SEARCH_SOLR_PASSWORD', '');
28  * - define('TEST_SEARCH_SOLR_SSLCERT', '');
29  * - define('TEST_SEARCH_SOLR_SSLKEY', '');
30  * - define('TEST_SEARCH_SOLR_KEYPASSWORD', '');
31  * - define('TEST_SEARCH_SOLR_CAINFOCERT', '');
32  *
33  * @package     core_search
34  * @category    phpunit
35  * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
36  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
39 defined('MOODLE_INTERNAL') || die();
41 global $CFG;
42 require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
43 require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
44 require_once($CFG->dirroot . '/search/engine/solr/tests/fixtures/testable_engine.php');
46 /**
47  * Solr search engine base unit tests.
48  *
49  * @package     core_search
50  * @category    phpunit
51  * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
52  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53  */
54 class search_solr_engine_testcase extends advanced_testcase {
56     /**
57      * @var \core_search::manager
58      */
59     protected $search = null;
61     /**
62      * @var Instace of core_search_generator.
63      */
64     protected $generator = null;
66     /**
67      * @var Instace of testable_engine.
68      */
69     protected $engine = null;
71     public function setUp() {
72         $this->resetAfterTest();
73         set_config('enableglobalsearch', true);
75         if (!function_exists('solr_get_version')) {
76             $this->markTestSkipped('Solr extension is not loaded.');
77         }
79         if (!defined('TEST_SEARCH_SOLR_HOSTNAME') || !defined('TEST_SEARCH_SOLR_INDEXNAME') ||
80                 !defined('TEST_SEARCH_SOLR_PORT')) {
81             $this->markTestSkipped('Solr extension test server not set.');
82         }
84         set_config('server_hostname', TEST_SEARCH_SOLR_HOSTNAME, 'search_solr');
85         set_config('server_port', TEST_SEARCH_SOLR_PORT, 'search_solr');
86         set_config('indexname', TEST_SEARCH_SOLR_INDEXNAME, 'search_solr');
88         if (defined('TEST_SEARCH_SOLR_USERNAME')) {
89             set_config('server_username', TEST_SEARCH_SOLR_USERNAME, 'search_solr');
90         }
92         if (defined('TEST_SEARCH_SOLR_PASSWORD')) {
93             set_config('server_password', TEST_SEARCH_SOLR_PASSWORD, 'search_solr');
94         }
96         if (defined('TEST_SEARCH_SOLR_SSLCERT')) {
97             set_config('secure', true, 'search_solr');
98             set_config('ssl_cert', TEST_SEARCH_SOLR_SSLCERT, 'search_solr');
99         }
101         if (defined('TEST_SEARCH_SOLR_SSLKEY')) {
102             set_config('ssl_key', TEST_SEARCH_SOLR_SSLKEY, 'search_solr');
103         }
105         if (defined('TEST_SEARCH_SOLR_KEYPASSWORD')) {
106             set_config('ssl_keypassword', TEST_SEARCH_SOLR_KEYPASSWORD, 'search_solr');
107         }
109         if (defined('TEST_SEARCH_SOLR_CAINFOCERT')) {
110             set_config('ssl_cainfo', TEST_SEARCH_SOLR_CAINFOCERT, 'search_solr');
111         }
113         set_config('fileindexing', 1, 'search_solr');
115         // We are only test indexing small string files, so setting this as low as we can.
116         set_config('maxindexfilekb', 1, 'search_solr');
118         $this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
119         $this->generator->setup();
121         // Inject search solr engine into the testable core search as we need to add the mock
122         // search component to it.
123         $this->engine = new \search_solr\testable_engine();
124         $this->search = testable_core_search::instance($this->engine);
125         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
126         $this->search->add_search_area($areaid, new core_mocksearch\search\mock_search_area());
128         $this->setAdminUser();
130         // Cleanup before doing anything on it as the index it is out of this test control.
131         $this->search->delete_index();
133         // Add moodle fields if they don't exist.
134         $schema = new \search_solr\schema();
135         $schema->setup(false);
136     }
138     public function tearDown() {
139         // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
140         if ($this->generator) {
141             // Moodle DML freaks out if we don't teardown the temp table after each run.
142             $this->generator->teardown();
143             $this->generator = null;
144         }
145     }
147     /**
148      * Simple data provider to allow tests to be run with file indexing on and off.
149      */
150     public function file_indexing_provider() {
151         return array(
152             'file-indexing-on' => array(1),
153             'file-indexing-off' => array(0)
154         );
155     }
157     public function test_connection() {
158         $this->assertTrue($this->engine->is_server_ready());
159     }
161     /**
162      * @dataProvider file_indexing_provider
163      */
164     public function test_index($fileindexing) {
165         global $DB;
167         $this->engine->test_set_config('fileindexing', $fileindexing);
169         $record = new \stdClass();
170         $record->timemodified = time() - 1;
171         $this->generator->create_record($record);
173         // Data gets into the search engine.
174         $this->assertTrue($this->search->index());
176         // Not anymore as everything was already added.
177         sleep(1);
178         $this->assertFalse($this->search->index());
180         $this->generator->create_record();
182         // Indexing again once there is new data.
183         $this->assertTrue($this->search->index());
184     }
186     /**
187      * Better keep this not very strict about which or how many results are returned as may depend on solr engine config.
188      *
189      * @dataProvider file_indexing_provider
190      *
191      * @return void
192      */
193     public function test_search($fileindexing) {
194         global $USER, $DB;
196         $this->engine->test_set_config('fileindexing', $fileindexing);
198         $this->generator->create_record();
199         $record = new \stdClass();
200         $record->title = "Special title";
201         $this->generator->create_record($record);
203         $this->search->index();
205         $querydata = new stdClass();
206         $querydata->q = 'message';
207         $results = $this->search->search($querydata);
208         $this->assertCount(2, $results);
210         // Based on core_mocksearch\search\indexer.
211         $this->assertEquals($USER->id, $results[0]->get('userid'));
212         $this->assertEquals(\context_system::instance()->id, $results[0]->get('contextid'));
214         // Do a test to make sure we aren't searching non-query fields, like areaid.
215         $querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
216         $this->assertCount(0, $this->search->search($querydata));
217         $querydata->q = 'message';
219         sleep(1);
220         $beforeadding = time();
221         sleep(1);
222         $this->generator->create_record();
223         $this->search->index();
225         // Timestart.
226         $querydata->timestart = $beforeadding;
227         $this->assertCount(1, $this->search->search($querydata));
229         // Timeend.
230         unset($querydata->timestart);
231         $querydata->timeend = $beforeadding;
232         $this->assertCount(2, $this->search->search($querydata));
234         // Title.
235         unset($querydata->timeend);
236         $querydata->title = 'Special title';
237         $this->assertCount(1, $this->search->search($querydata));
239         // Course IDs.
240         unset($querydata->title);
241         $querydata->courseids = array(SITEID + 1);
242         $this->assertCount(0, $this->search->search($querydata));
244         $querydata->courseids = array(SITEID);
245         $this->assertCount(3, $this->search->search($querydata));
247         // Now try some area-id combinations.
248         unset($querydata->courseids);
249         $forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
250         $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
252         $querydata->areaids = array($forumpostareaid);
253         $this->assertCount(0, $this->search->search($querydata));
255         $querydata->areaids = array($forumpostareaid, $mockareaid);
256         $this->assertCount(3, $this->search->search($querydata));
258         $querydata->areaids = array($mockareaid);
259         $this->assertCount(3, $this->search->search($querydata));
261         $querydata->areaids = array();
262         $this->assertCount(3, $this->search->search($querydata));
264         // Check that index contents get updated.
265         $this->generator->delete_all();
266         $this->search->index(true);
267         unset($querydata->title);
268         $querydata->q = '*';
269         $this->assertCount(0, $this->search->search($querydata));
270     }
272     /**
273      * @dataProvider file_indexing_provider
274      */
275     public function test_delete($fileindexing) {
276         $this->engine->test_set_config('fileindexing', $fileindexing);
278         $this->generator->create_record();
279         $this->generator->create_record();
280         $this->search->index();
282         $querydata = new stdClass();
283         $querydata->q = 'message';
285         $this->assertCount(2, $this->search->search($querydata));
287         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
288         $this->search->delete_index($areaid);
289         $this->assertCount(0, $this->search->search($querydata));
290     }
292     /**
293      * @dataProvider file_indexing_provider
294      */
295     public function test_alloweduserid($fileindexing) {
296         $this->engine->test_set_config('fileindexing', $fileindexing);
298         $area = new core_mocksearch\search\mock_search_area();
300         $record = $this->generator->create_record();
302         // Get the doc and insert the default doc.
303         $doc = $area->get_document($record);
304         $this->engine->add_document($doc);
306         $users = array();
307         $users[] = $this->getDataGenerator()->create_user();
308         $users[] = $this->getDataGenerator()->create_user();
309         $users[] = $this->getDataGenerator()->create_user();
311         // Add a record that only user 100 can see.
312         $originalid = $doc->get('id');
314         // Now add a custom doc for each user.
315         foreach ($users as $user) {
316             $doc = $area->get_document($record);
317             $doc->set('id', $originalid.'-'.$user->id);
318             $doc->set('owneruserid', $user->id);
319             $this->engine->add_document($doc);
320         }
322         $this->engine->area_index_complete($area->get_area_id());
324         $querydata = new stdClass();
325         $querydata->q = 'message';
326         $querydata->title = $doc->get('title');
328         // We are going to go through each user and see if they get the original and the owned doc.
329         foreach ($users as $user) {
330             $this->setUser($user);
332             $results = $this->search->search($querydata);
333             $this->assertCount(2, $results);
335             $owned = 0;
336             $notowned = 0;
338             // We don't know what order we will get the results in, so we are doing this.
339             foreach ($results as $result) {
340                 $owneruserid = $result->get('owneruserid');
341                 if (empty($owneruserid)) {
342                     $notowned++;
343                     $this->assertEquals(0, $owneruserid);
344                     $this->assertEquals($originalid, $result->get('id'));
345                 } else {
346                     $owned++;
347                     $this->assertEquals($user->id, $owneruserid);
348                     $this->assertEquals($originalid.'-'.$user->id, $result->get('id'));
349                 }
350             }
352             $this->assertEquals(1, $owned);
353             $this->assertEquals(1, $notowned);
354         }
356         // Now test a user with no owned results.
357         $otheruser = $this->getDataGenerator()->create_user();
358         $this->setUser($otheruser);
360         $results = $this->search->search($querydata);
361         $this->assertCount(1, $results);
363         $this->assertEquals(0, $results[0]->get('owneruserid'));
364         $this->assertEquals($originalid, $results[0]->get('id'));
365     }
367     /**
368      * @dataProvider file_indexing_provider
369      */
370     public function test_highlight($fileindexing) {
371         global $PAGE;
373         $this->engine->test_set_config('fileindexing', $fileindexing);
375         $this->generator->create_record();
376         $this->search->index();
378         $querydata = new stdClass();
379         $querydata->q = 'message';
381         $results = $this->search->search($querydata);
382         $this->assertCount(1, $results);
384         $result = reset($results);
386         $regex = '|'.\search_solr\engine::HIGHLIGHT_START.'message'.\search_solr\engine::HIGHLIGHT_END.'|';
387         $this->assertRegExp($regex, $result->get('content'));
389         $searchrenderer = $PAGE->get_renderer('core_search');
390         $exported = $result->export_for_template($searchrenderer);
392         $regex = '|<span class="highlight">message</span>|';
393         $this->assertRegExp($regex, $exported['content']);
394     }
396     public function test_index_file() {
397         // Very simple test.
398         $file = $this->generator->create_file();
400         $record = new \stdClass();
401         $record->attachfileids = array($file->get_id());
402         $this->generator->create_record($record);
404         $this->search->index();
405         $querydata = new stdClass();
406         $querydata->q = '"File contents"';
408         $this->assertCount(1, $this->search->search($querydata));
409     }
411     public function test_reindexing_files() {
412         // Get area to work with.
413         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
414         $area = \core_search\manager::get_search_area($areaid);
416         $record = $this->generator->create_record();
418         $doc = $area->get_document($record);
420         // Now we are going to make some files.
421         $fs = get_file_storage();
422         $syscontext = \context_system::instance();
424         $files = array();
426         $filerecord = new \stdClass();
427         // We make enough so that we pass the 500 files threashold. That is the boundary when getting files.
428         $boundary = 500;
429         $top = (int)($boundary * 1.1);
430         for ($i = 0; $i < $top; $i++) {
431             $filerecord->filename  = 'searchfile'.$i;
432             $filerecord->content = 'Some FileContents'.$i;
433             $file = $this->generator->create_file($filerecord);
434             $doc->add_stored_file($file);
435             $files[] = $file;
436         }
438         // Add the doc with lots of files, then commit.
439         $this->engine->add_document($doc, true);
440         $this->engine->area_index_complete($area->get_area_id());
442         // Indexes we are going to check. 0 means we will delete, 1 means we will keep.
443         $checkfiles = array(
444             0 => 0,                        // Check the begining of the set.
445             1 => 1,
446             2 => 0,
447             ($top - 3) => 0,               // Check the end of the set.
448             ($top - 2) => 1,
449             ($top - 1) => 0,
450             ($boundary - 2) => 0,          // Check at the boundary between fetch groups.
451             ($boundary - 1) => 0,
452             $boundary => 0,
453             ($boundary + 1) => 0,
454             ((int)($boundary * 0.5)) => 1, // Make sure we keep some middle ones.
455             ((int)($boundary * 1.05)) => 1
456         );
458         $querydata = new stdClass();
460         // First, check that all the files are currently there.
461         foreach ($checkfiles as $key => $unused) {
462             $querydata->q = 'FileContents'.$key;
463             $this->assertCount(1, $this->search->search($querydata));
464             $querydata->q = 'searchfile'.$key;
465             $this->assertCount(1, $this->search->search($querydata));
466         }
468         // Remove the files we want removed from the files array.
469         foreach ($checkfiles as $key => $keep) {
470             if (!$keep) {
471                 unset($files[$key]);
472             }
473         }
475         // And make us a new file to add.
476         $filerecord->filename  = 'searchfileNew';
477         $filerecord->content  = 'Some FileContentsNew';
478         $files[] = $this->generator->create_file($filerecord);
479         $checkfiles['New'] = 1;
481         $doc = $area->get_document($record);
482         foreach($files as $file) {
483             $doc->add_stored_file($file);
484         }
486         // Reindex the document with the changed files.
487         $this->engine->add_document($doc, true);
488         $this->engine->area_index_complete($area->get_area_id());
490         // Go through our check array, and see if the file is there or not.
491         foreach ($checkfiles as $key => $keep) {
492             $querydata->q = 'FileContents'.$key;
493             $this->assertCount($keep, $this->search->search($querydata));
494             $querydata->q = 'searchfile'.$key;
495             $this->assertCount($keep, $this->search->search($querydata));
496         }
498         // Now check that we get one result when we search from something in all of them.
499         $querydata->q = 'Some';
500         $this->assertCount(1, $this->search->search($querydata));
501     }
503     /**
504      * Test indexing a file we don't consider indexable.
505      */
506     public function test_index_filtered_file() {
507         // Get area to work with.
508         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
509         $area = \core_search\manager::get_search_area($areaid);
511         // Get a single record to make a doc from.
512         $record = $this->generator->create_record();
514         $doc = $area->get_document($record);
516         // Now we are going to make some files.
517         $fs = get_file_storage();
518         $syscontext = \context_system::instance();
520         // We need to make a file greater than 1kB in size, which is the lowest filter size.
521         $filerecord = new \stdClass();
522         $filerecord->filename = 'largefile';
523         $filerecord->content = 'Some LargeFindContent to find.';
524         for ($i = 0; $i < 200; $i++) {
525             $filerecord->content .= ' The quick brown fox jumps over the lazy dog.';
526         }
528         $this->assertGreaterThan(1024, strlen($filerecord->content));
530         $file = $this->generator->create_file($filerecord);
531         $doc->add_stored_file($file);
533         $filerecord->filename = 'smallfile';
534         $filerecord->content = 'Some SmallFindContent to find.';
535         $file = $this->generator->create_file($filerecord);
536         $doc->add_stored_file($file);
538         $this->engine->add_document($doc, true);
539         $this->engine->area_index_complete($area->get_area_id());
541         $querydata = new stdClass();
542         // We shouldn't be able to find the large file contents.
543         $querydata->q = 'LargeFindContent';
544         $this->assertCount(0, $this->search->search($querydata));
546         // But we should be able to find the filename.
547         $querydata->q = 'largefile';
548         $this->assertCount(1, $this->search->search($querydata));
550         // We should be able to find the small file contents.
551         $querydata->q = 'SmallFindContent';
552         $this->assertCount(1, $this->search->search($querydata));
554         // And we should be able to find the filename.
555         $querydata->q = 'smallfile';
556         $this->assertCount(1, $this->search->search($querydata));
557     }
559     public function test_delete_by_id() {
560         // First get files in the index.
561         $file = $this->generator->create_file();
562         $record = new \stdClass();
563         $record->attachfileids = array($file->get_id());
564         $this->generator->create_record($record);
565         $this->generator->create_record($record);
566         $this->search->index();
568         $querydata = new stdClass();
570         // Then search to make sure they are there.
571         $querydata->q = '"File contents"';
572         $results = $this->search->search($querydata);
573         $this->assertCount(2, $results);
575         $first = reset($results);
576         $deleteid = $first->get('id');
578         $this->engine->delete_by_id($deleteid);
580         // Check that we don't get a result for it anymore.
581         $results = $this->search->search($querydata);
582         $this->assertCount(1, $results);
583         $result = reset($results);
584         $this->assertNotEquals($deleteid, $result->get('id'));
585     }
587     /**
588      * Test that expected results are returned, even with low check_access success rate.
589      *
590      * @dataProvider file_indexing_provider
591      */
592     public function test_solr_filling($fileindexing) {
593         $this->engine->test_set_config('fileindexing', $fileindexing);
595         $user1 = self::getDataGenerator()->create_user();
596         $user2 = self::getDataGenerator()->create_user();
598         // We are going to create a bunch of records that user 1 can see with 2 keywords.
599         // Then we are going to create a bunch for user 2 with only 1 of the keywords.
600         // If user 2 searches for both keywords, solr will return all of the user 1 results, then the user 2 results.
601         // This is because the user 1 results will match 2 keywords, while the others will match only 1.
603         $record = new \stdClass();
605         // First create a bunch of records for user 1 to see.
606         $record->denyuserids = array($user2->id);
607         $record->content = 'Something1 Something2';
608         $maxresults = (int)(\core_search\manager::MAX_RESULTS * .75);
609         for ($i = 0; $i < $maxresults; $i++) {
610             $this->generator->create_record($record);
611         }
613         // Then create a bunch of records for user 2 to see.
614         $record->denyuserids = array($user1->id);
615         $record->content = 'Something1';
616         for ($i = 0; $i < $maxresults; $i++) {
617             $this->generator->create_record($record);
618         }
620         $this->search->index();
622         // Check that user 1 sees all their results.
623         $this->setUser($user1);
624         $querydata = new stdClass();
625         $querydata->q = 'Something1 Something2';
626         $results = $this->search->search($querydata);
627         $this->assertCount($maxresults, $results);
629         // Check that user 2 will see theirs, even though they may be crouded out.
630         $this->setUser($user2);
631         $results = $this->search->search($querydata);
632         $this->assertCount($maxresults, $results);
633     }
635     /**
636      * Create 40 docs, that will be return from Solr in 10 hidden, 10 visible, 10 hidden, 10 visible if you query for:
637      * Something1 Something2 Something3 Something4, with the specified user set.
638      */
639     protected function setup_user_hidden_docs($user) {
640         // These results will come first, and will not be visible by the user.
641         $record = new \stdClass();
642         $record->denyuserids = array($user->id);
643         $record->content = 'Something1 Something2 Something3 Something4';
644         for ($i = 0; $i < 10; $i++) {
645             $this->generator->create_record($record);
646         }
648         // These results will come second, and will  be visible by the user.
649         unset($record->denyuserids);
650         $record->content = 'Something1 Something2 Something3';
651         for ($i = 0; $i < 10; $i++) {
652             $this->generator->create_record($record);
653         }
655         // These results will come third, and will not be visible by the user.
656         $record->denyuserids = array($user->id);
657         $record->content = 'Something1 Something2';
658         for ($i = 0; $i < 10; $i++) {
659             $this->generator->create_record($record);
660         }
662         // These results will come fourth, and will be visible by the user.
663         unset($record->denyuserids);
664         $record->content = 'Something1 ';
665         for ($i = 0; $i < 10; $i++) {
666             $this->generator->create_record($record);
667         }
668     }
670     /**
671      * Test that counts are what we expect.
672      *
673      * @dataProvider file_indexing_provider
674      */
675     public function test_get_query_total_count($fileindexing) {
676         $this->engine->test_set_config('fileindexing', $fileindexing);
678         $user = self::getDataGenerator()->create_user();
679         $this->setup_user_hidden_docs($user);
680         $this->search->index();
682         $this->setUser($user);
683         $querydata = new stdClass();
684         $querydata->q = 'Something1 Something2 Something3 Something4';
686         // In this first set, it should have determined the first 10 of 40 are bad, so there could be up to 30 left.
687         $results = $this->engine->execute_query($querydata, true, 5);
688         $this->assertEquals(30, $this->engine->get_query_total_count());
689         $this->assertCount(5, $results);
691         // To get to 15, it has to process the first 10 that are bad, 10 that are good, 10 that are bad, then 5 that are good.
692         // So we now know 20 are bad out of 40.
693         $results = $this->engine->execute_query($querydata, true, 15);
694         $this->assertEquals(20, $this->engine->get_query_total_count());
695         $this->assertCount(15, $results);
697         // Try to get more then all, make sure we still see 20 count and 20 returned.
698         $results = $this->engine->execute_query($querydata, true, 30);
699         $this->assertEquals(20, $this->engine->get_query_total_count());
700         $this->assertCount(20, $results);
701     }
703     /**
704      * Test that paged results are what we expect.
705      *
706      * @dataProvider file_indexing_provider
707      */
708     public function test_manager_paged_search($fileindexing) {
709         $this->engine->test_set_config('fileindexing', $fileindexing);
711         $user = self::getDataGenerator()->create_user();
712         $this->setup_user_hidden_docs($user);
713         $this->search->index();
715         // Check that user 1 sees all their results.
716         $this->setUser($user);
717         $querydata = new stdClass();
718         $querydata->q = 'Something1 Something2 Something3 Something4';
720         // On this first page, it should have determined the first 10 of 40 are bad, so there could be up to 30 left.
721         $results = $this->search->paged_search($querydata, 0);
722         $this->assertEquals(30, $results->totalcount);
723         $this->assertCount(10, $results->results);
724         $this->assertEquals(0, $results->actualpage);
726         // On the second page, it should have found the next 10 bad ones, so we no know there are only 20 total.
727         $results = $this->search->paged_search($querydata, 1);
728         $this->assertEquals(20, $results->totalcount);
729         $this->assertCount(10, $results->results);
730         $this->assertEquals(1, $results->actualpage);
732         // Try to get an additional page - we should get back page 1 results, since that is the last page with valid results.
733         $results = $this->search->paged_search($querydata, 2);
734         $this->assertEquals(20, $results->totalcount);
735         $this->assertCount(10, $results->results);
736         $this->assertEquals(1, $results->actualpage);
737     }