7a4bcde943d2bd94838284fa9279f999abbf9ae4
[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_course::instance(SITEID)->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_export_file_for_engine() {
397         // Get area to work with.
398         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
399         $area = \core_search\manager::get_search_area($areaid);
401         $record = $this->generator->create_record();
403         $doc = $area->get_document($record);
404         $filerecord = new stdClass();
405         $filerecord->timemodified  = 978310800;
406         $file = $this->generator->create_file($filerecord);
407         $doc->add_stored_file($file);
409         $filearray = $doc->export_file_for_engine($file);
411         $this->assertEquals(\core_search\manager::TYPE_FILE, $filearray['type']);
412         $this->assertEquals($file->get_id(), $filearray['solr_fileid']);
413         $this->assertEquals($file->get_contenthash(), $filearray['solr_filecontenthash']);
414         $this->assertEquals(\search_solr\document::INDEXED_FILE_TRUE, $filearray['solr_fileindexstatus']);
415         $this->assertEquals($file->get_filename(), $filearray['title']);
416         $this->assertEquals(978310800, \search_solr\document::import_time_from_engine($filearray['modified']));
417     }
419     public function test_index_file() {
420         // Very simple test.
421         $file = $this->generator->create_file();
423         $record = new \stdClass();
424         $record->attachfileids = array($file->get_id());
425         $this->generator->create_record($record);
427         $this->search->index();
428         $querydata = new stdClass();
429         $querydata->q = '"File contents"';
431         $this->assertCount(1, $this->search->search($querydata));
432     }
434     public function test_reindexing_files() {
435         // Get area to work with.
436         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
437         $area = \core_search\manager::get_search_area($areaid);
439         $record = $this->generator->create_record();
441         $doc = $area->get_document($record);
443         // Now we are going to make some files.
444         $fs = get_file_storage();
445         $syscontext = \context_system::instance();
447         $files = array();
449         $filerecord = new \stdClass();
450         // We make enough so that we pass the 500 files threashold. That is the boundary when getting files.
451         $boundary = 500;
452         $top = (int)($boundary * 1.1);
453         for ($i = 0; $i < $top; $i++) {
454             $filerecord->filename  = 'searchfile'.$i;
455             $filerecord->content = 'Some FileContents'.$i;
456             $file = $this->generator->create_file($filerecord);
457             $doc->add_stored_file($file);
458             $files[] = $file;
459         }
461         // Add the doc with lots of files, then commit.
462         $this->engine->add_document($doc, true);
463         $this->engine->area_index_complete($area->get_area_id());
465         // Indexes we are going to check. 0 means we will delete, 1 means we will keep.
466         $checkfiles = array(
467             0 => 0,                        // Check the begining of the set.
468             1 => 1,
469             2 => 0,
470             ($top - 3) => 0,               // Check the end of the set.
471             ($top - 2) => 1,
472             ($top - 1) => 0,
473             ($boundary - 2) => 0,          // Check at the boundary between fetch groups.
474             ($boundary - 1) => 0,
475             $boundary => 0,
476             ($boundary + 1) => 0,
477             ((int)($boundary * 0.5)) => 1, // Make sure we keep some middle ones.
478             ((int)($boundary * 1.05)) => 1
479         );
481         $querydata = new stdClass();
483         // First, check that all the files are currently there.
484         foreach ($checkfiles as $key => $unused) {
485             $querydata->q = 'FileContents'.$key;
486             $this->assertCount(1, $this->search->search($querydata));
487             $querydata->q = 'searchfile'.$key;
488             $this->assertCount(1, $this->search->search($querydata));
489         }
491         // Remove the files we want removed from the files array.
492         foreach ($checkfiles as $key => $keep) {
493             if (!$keep) {
494                 unset($files[$key]);
495             }
496         }
498         // And make us a new file to add.
499         $filerecord->filename  = 'searchfileNew';
500         $filerecord->content  = 'Some FileContentsNew';
501         $files[] = $this->generator->create_file($filerecord);
502         $checkfiles['New'] = 1;
504         $doc = $area->get_document($record);
505         foreach($files as $file) {
506             $doc->add_stored_file($file);
507         }
509         // Reindex the document with the changed files.
510         $this->engine->add_document($doc, true);
511         $this->engine->area_index_complete($area->get_area_id());
513         // Go through our check array, and see if the file is there or not.
514         foreach ($checkfiles as $key => $keep) {
515             $querydata->q = 'FileContents'.$key;
516             $this->assertCount($keep, $this->search->search($querydata));
517             $querydata->q = 'searchfile'.$key;
518             $this->assertCount($keep, $this->search->search($querydata));
519         }
521         // Now check that we get one result when we search from something in all of them.
522         $querydata->q = 'Some';
523         $this->assertCount(1, $this->search->search($querydata));
524     }
526     /**
527      * Test indexing a file we don't consider indexable.
528      */
529     public function test_index_filtered_file() {
530         // Get area to work with.
531         $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
532         $area = \core_search\manager::get_search_area($areaid);
534         // Get a single record to make a doc from.
535         $record = $this->generator->create_record();
537         $doc = $area->get_document($record);
539         // Now we are going to make some files.
540         $fs = get_file_storage();
541         $syscontext = \context_system::instance();
543         // We need to make a file greater than 1kB in size, which is the lowest filter size.
544         $filerecord = new \stdClass();
545         $filerecord->filename = 'largefile';
546         $filerecord->content = 'Some LargeFindContent to find.';
547         for ($i = 0; $i < 200; $i++) {
548             $filerecord->content .= ' The quick brown fox jumps over the lazy dog.';
549         }
551         $this->assertGreaterThan(1024, strlen($filerecord->content));
553         $file = $this->generator->create_file($filerecord);
554         $doc->add_stored_file($file);
556         $filerecord->filename = 'smallfile';
557         $filerecord->content = 'Some SmallFindContent to find.';
558         $file = $this->generator->create_file($filerecord);
559         $doc->add_stored_file($file);
561         $this->engine->add_document($doc, true);
562         $this->engine->area_index_complete($area->get_area_id());
564         $querydata = new stdClass();
565         // We shouldn't be able to find the large file contents.
566         $querydata->q = 'LargeFindContent';
567         $this->assertCount(0, $this->search->search($querydata));
569         // But we should be able to find the filename.
570         $querydata->q = 'largefile';
571         $this->assertCount(1, $this->search->search($querydata));
573         // We should be able to find the small file contents.
574         $querydata->q = 'SmallFindContent';
575         $this->assertCount(1, $this->search->search($querydata));
577         // And we should be able to find the filename.
578         $querydata->q = 'smallfile';
579         $this->assertCount(1, $this->search->search($querydata));
580     }
582     public function test_delete_by_id() {
583         // First get files in the index.
584         $file = $this->generator->create_file();
585         $record = new \stdClass();
586         $record->attachfileids = array($file->get_id());
587         $this->generator->create_record($record);
588         $this->generator->create_record($record);
589         $this->search->index();
591         $querydata = new stdClass();
593         // Then search to make sure they are there.
594         $querydata->q = '"File contents"';
595         $results = $this->search->search($querydata);
596         $this->assertCount(2, $results);
598         $first = reset($results);
599         $deleteid = $first->get('id');
601         $this->engine->delete_by_id($deleteid);
603         // Check that we don't get a result for it anymore.
604         $results = $this->search->search($querydata);
605         $this->assertCount(1, $results);
606         $result = reset($results);
607         $this->assertNotEquals($deleteid, $result->get('id'));
608     }
610     /**
611      * Test that expected results are returned, even with low check_access success rate.
612      *
613      * @dataProvider file_indexing_provider
614      */
615     public function test_solr_filling($fileindexing) {
616         $this->engine->test_set_config('fileindexing', $fileindexing);
618         $user1 = self::getDataGenerator()->create_user();
619         $user2 = self::getDataGenerator()->create_user();
621         // We are going to create a bunch of records that user 1 can see with 2 keywords.
622         // Then we are going to create a bunch for user 2 with only 1 of the keywords.
623         // If user 2 searches for both keywords, solr will return all of the user 1 results, then the user 2 results.
624         // This is because the user 1 results will match 2 keywords, while the others will match only 1.
626         $record = new \stdClass();
628         // First create a bunch of records for user 1 to see.
629         $record->denyuserids = array($user2->id);
630         $record->content = 'Something1 Something2';
631         $maxresults = (int)(\core_search\manager::MAX_RESULTS * .75);
632         for ($i = 0; $i < $maxresults; $i++) {
633             $this->generator->create_record($record);
634         }
636         // Then create a bunch of records for user 2 to see.
637         $record->denyuserids = array($user1->id);
638         $record->content = 'Something1';
639         for ($i = 0; $i < $maxresults; $i++) {
640             $this->generator->create_record($record);
641         }
643         $this->search->index();
645         // Check that user 1 sees all their results.
646         $this->setUser($user1);
647         $querydata = new stdClass();
648         $querydata->q = 'Something1 Something2';
649         $results = $this->search->search($querydata);
650         $this->assertCount($maxresults, $results);
652         // Check that user 2 will see theirs, even though they may be crouded out.
653         $this->setUser($user2);
654         $results = $this->search->search($querydata);
655         $this->assertCount($maxresults, $results);
656     }
658     /**
659      * Create 40 docs, that will be return from Solr in 10 hidden, 10 visible, 10 hidden, 10 visible if you query for:
660      * Something1 Something2 Something3 Something4, with the specified user set.
661      */
662     protected function setup_user_hidden_docs($user) {
663         // These results will come first, and will not be visible by the user.
664         $record = new \stdClass();
665         $record->denyuserids = array($user->id);
666         $record->content = 'Something1 Something2 Something3 Something4';
667         for ($i = 0; $i < 10; $i++) {
668             $this->generator->create_record($record);
669         }
671         // These results will come second, and will  be visible by the user.
672         unset($record->denyuserids);
673         $record->content = 'Something1 Something2 Something3';
674         for ($i = 0; $i < 10; $i++) {
675             $this->generator->create_record($record);
676         }
678         // These results will come third, and will not be visible by the user.
679         $record->denyuserids = array($user->id);
680         $record->content = 'Something1 Something2';
681         for ($i = 0; $i < 10; $i++) {
682             $this->generator->create_record($record);
683         }
685         // These results will come fourth, and will be visible by the user.
686         unset($record->denyuserids);
687         $record->content = 'Something1 ';
688         for ($i = 0; $i < 10; $i++) {
689             $this->generator->create_record($record);
690         }
691     }
693     /**
694      * Test that counts are what we expect.
695      *
696      * @dataProvider file_indexing_provider
697      */
698     public function test_get_query_total_count($fileindexing) {
699         $this->engine->test_set_config('fileindexing', $fileindexing);
701         $user = self::getDataGenerator()->create_user();
702         $this->setup_user_hidden_docs($user);
703         $this->search->index();
705         $this->setUser($user);
706         $querydata = new stdClass();
707         $querydata->q = 'Something1 Something2 Something3 Something4';
709         // In this first set, it should have determined the first 10 of 40 are bad, so there could be up to 30 left.
710         $results = $this->engine->execute_query($querydata, (object)['everything' => true], 5);
711         $this->assertEquals(30, $this->engine->get_query_total_count());
712         $this->assertCount(5, $results);
714         // 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.
715         // So we now know 20 are bad out of 40.
716         $results = $this->engine->execute_query($querydata, (object)['everything' => true], 15);
717         $this->assertEquals(20, $this->engine->get_query_total_count());
718         $this->assertCount(15, $results);
720         // Try to get more then all, make sure we still see 20 count and 20 returned.
721         $results = $this->engine->execute_query($querydata, (object)['everything' => true], 30);
722         $this->assertEquals(20, $this->engine->get_query_total_count());
723         $this->assertCount(20, $results);
724     }
726     /**
727      * Test that paged results are what we expect.
728      *
729      * @dataProvider file_indexing_provider
730      */
731     public function test_manager_paged_search($fileindexing) {
732         $this->engine->test_set_config('fileindexing', $fileindexing);
734         $user = self::getDataGenerator()->create_user();
735         $this->setup_user_hidden_docs($user);
736         $this->search->index();
738         // Check that user 1 sees all their results.
739         $this->setUser($user);
740         $querydata = new stdClass();
741         $querydata->q = 'Something1 Something2 Something3 Something4';
743         // On this first page, it should have determined the first 10 of 40 are bad, so there could be up to 30 left.
744         $results = $this->search->paged_search($querydata, 0);
745         $this->assertEquals(30, $results->totalcount);
746         $this->assertCount(10, $results->results);
747         $this->assertEquals(0, $results->actualpage);
749         // On the second page, it should have found the next 10 bad ones, so we no know there are only 20 total.
750         $results = $this->search->paged_search($querydata, 1);
751         $this->assertEquals(20, $results->totalcount);
752         $this->assertCount(10, $results->results);
753         $this->assertEquals(1, $results->actualpage);
755         // Try to get an additional page - we should get back page 1 results, since that is the last page with valid results.
756         $results = $this->search->paged_search($querydata, 2);
757         $this->assertEquals(20, $results->totalcount);
758         $this->assertCount(10, $results->results);
759         $this->assertEquals(1, $results->actualpage);
760     }
762     /**
763      * Tests searching for results restricted to context id.
764      */
765     public function test_context_restriction() {
766         // Use real search areas.
767         $this->search->clear_static();
768         $this->search->add_core_search_areas();
770         // Create 2 courses and some forums.
771         $generator = $this->getDataGenerator();
772         $course1 = $generator->create_course(['fullname' => 'Course 1', 'summary' => 'xyzzy']);
773         $contextc1 = \context_course::instance($course1->id);
774         $course1forum1 = $generator->create_module('forum', ['course' => $course1,
775                 'name' => 'C1F1', 'intro' => 'xyzzy']);
776         $contextc1f1 = \context_module::instance($course1forum1->cmid);
777         $course1forum2 = $generator->create_module('forum', ['course' => $course1,
778                 'name' => 'C1F2', 'intro' => 'xyzzy']);
779         $contextc1f2 = \context_module::instance($course1forum2->cmid);
780         $course2 = $generator->create_course(['fullname' => 'Course 2', 'summary' => 'xyzzy']);
781         $contextc2 = \context_course::instance($course1->id);
782         $course2forum = $generator->create_module('forum', ['course' => $course2,
783                 'name' => 'C2F', 'intro' => 'xyzzy']);
784         $contextc2f = \context_module::instance($course2forum->cmid);
786         // Index the courses and forums.
787         $this->search->index();
789         // Search as admin user should find everything.
790         $querydata = new stdClass();
791         $querydata->q = 'xyzzy';
792         $results = $this->search->search($querydata);
793         $this->assert_result_titles(
794                 ['Course 1', 'Course 2', 'C1F1', 'C1F2', 'C2F'], $results);
796         // Admin user manually restricts results by context id to include one course and one forum.
797         $querydata->contextids = [$contextc2f->id, $contextc1->id];
798         $results = $this->search->search($querydata);
799         $this->assert_result_titles(['Course 1', 'C2F'], $results);
801         // Student enrolled in only one course, same restriction, only has the available results.
802         $student2 = $generator->create_user();
803         $generator->enrol_user($student2->id, $course2->id, 'student');
804         $this->setUser($student2);
805         $results = $this->search->search($querydata);
806         $this->assert_result_titles(['C2F'], $results);
808         // Student enrolled in both courses, same restriction, same results as admin.
809         $student1 = $generator->create_user();
810         $generator->enrol_user($student1->id, $course1->id, 'student');
811         $generator->enrol_user($student1->id, $course2->id, 'student');
812         $this->setUser($student1);
813         $results = $this->search->search($querydata);
814         $this->assert_result_titles(['Course 1', 'C2F'], $results);
816         // Restrict both course and context.
817         $querydata->courseids = [$course2->id];
818         $results = $this->search->search($querydata);
819         $this->assert_result_titles(['C2F'], $results);
820         unset($querydata->courseids);
822         // Restrict both area and context.
823         $querydata->areaids = ['core_course-mycourse'];
824         $results = $this->search->search($querydata);
825         $this->assert_result_titles(['Course 1'], $results);
827         // Restrict area and context, incompatibly - this has no results (and doesn't do a query).
828         $querydata->contextids = [$contextc2f->id];
829         $results = $this->search->search($querydata);
830         $this->assert_result_titles([], $results);
831     }
833     /**
834      * Tests searching for results in groups, either by specified group ids or based on user
835      * access permissions.
836      */
837     public function test_groups() {
838         global $USER;
840         // Use real search areas.
841         $this->search->clear_static();
842         $this->search->add_core_search_areas();
844         // Create 2 courses and a selection of forums with different group mode.
845         $generator = $this->getDataGenerator();
846         $course1 = $generator->create_course(['fullname' => 'Course 1']);
847         $forum1nogroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => NOGROUPS]);
848         $forum1separategroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => SEPARATEGROUPS]);
849         $forum1visiblegroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => VISIBLEGROUPS]);
850         $course2 = $generator->create_course(['fullname' => 'Course 2']);
851         $forum2separategroups = $generator->create_module('forum', ['course' => $course2, 'groupmode' => SEPARATEGROUPS]);
853         // Create two groups on each course.
854         $group1a = $generator->create_group(['courseid' => $course1->id]);
855         $group1b = $generator->create_group(['courseid' => $course1->id]);
856         $group2a = $generator->create_group(['courseid' => $course2->id]);
857         $group2b = $generator->create_group(['courseid' => $course2->id]);
859         // Create search records in each activity and (where relevant) in each group.
860         $forumgenerator = $generator->get_plugin_generator('mod_forum');
861         $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
862                 'forum' => $forum1nogroups->id, 'name' => 'F1NG', 'message' => 'xyzzy']);
863         $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
864                 'forum' => $forum1separategroups->id, 'name' => 'F1SG-A',  'message' => 'xyzzy',
865                 'groupid' => $group1a->id]);
866         $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
867                 'forum' => $forum1separategroups->id, 'name' => 'F1SG-B', 'message' => 'xyzzy',
868                 'groupid' => $group1b->id]);
869         $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
870                 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-A', 'message' => 'xyzzy',
871                 'groupid' => $group1a->id]);
872         $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
873                 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-B', 'message' => 'xyzzy',
874                 'groupid' => $group1b->id]);
875         $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id,
876                 'forum' => $forum2separategroups->id, 'name' => 'F2SG-A', 'message' => 'xyzzy',
877                 'groupid' => $group2a->id]);
878         $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id,
879                 'forum' => $forum2separategroups->id, 'name' => 'F2SG-B', 'message' => 'xyzzy',
880                 'groupid' => $group2b->id]);
882         $this->search->index();
884         // Search as admin user should find everything.
885         $querydata = new stdClass();
886         $querydata->q = 'xyzzy';
887         $results = $this->search->search($querydata);
888         $this->assert_result_titles(
889                 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results);
891         // Admin user manually restricts results by groups.
892         $querydata->groupids = [$group1b->id, $group2a->id];
893         $results = $this->search->search($querydata);
894         $this->assert_result_titles(['F1SG-B', 'F1VG-B', 'F2SG-A'], $results);
896         // Student enrolled in both courses but no groups.
897         $student1 = $generator->create_user();
898         $generator->enrol_user($student1->id, $course1->id, 'student');
899         $generator->enrol_user($student1->id, $course2->id, 'student');
900         $this->setUser($student1);
902         unset($querydata->groupids);
903         $results = $this->search->search($querydata);
904         $this->assert_result_titles(['F1NG', 'F1VG-A', 'F1VG-B'], $results);
906         // Student enrolled in both courses and group A in both cases.
907         $student2 = $generator->create_user();
908         $generator->enrol_user($student2->id, $course1->id, 'student');
909         $generator->enrol_user($student2->id, $course2->id, 'student');
910         groups_add_member($group1a, $student2);
911         groups_add_member($group2a, $student2);
912         $this->setUser($student2);
914         $results = $this->search->search($querydata);
915         $this->assert_result_titles(['F1NG', 'F1SG-A', 'F1VG-A', 'F1VG-B', 'F2SG-A'], $results);
917         // Manually restrict results to group B in course 1.
918         $querydata->groupids = [$group1b->id];
919         $results = $this->search->search($querydata);
920         $this->assert_result_titles(['F1VG-B'], $results);
922         // Manually restrict results to group A in course 1.
923         $querydata->groupids = [$group1a->id];
924         $results = $this->search->search($querydata);
925         $this->assert_result_titles(['F1SG-A', 'F1VG-A'], $results);
927         // Manager enrolled in both courses (has access all groups).
928         $manager = $generator->create_user();
929         $generator->enrol_user($manager->id, $course1->id, 'manager');
930         $generator->enrol_user($manager->id, $course2->id, 'manager');
931         $this->setUser($manager);
932         unset($querydata->groupids);
933         $results = $this->search->search($querydata);
934         $this->assert_result_titles(
935                 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results);
936     }
938     /**
939      * Asserts that the returned documents have the expected titles (regardless of order).
940      *
941      * @param string[] $expected List of expected document titles
942      * @param \core_search\document[] $results List of returned documents
943      */
944     protected function assert_result_titles(array $expected, array $results) {
945         $titles = [];
946         foreach ($results as $result) {
947             $titles[] = $result->get('title');
948         }
949         sort($titles);
950         sort($expected);
951         $this->assertEquals($expected, $titles);
952     }