MDL-60913 search: add search area categories
[moodle.git] / search / tests / manager_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  * Search manager unit tests.
19  *
20  * @package     core_search
21  * @category    phpunit
22  * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
23  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once(__DIR__ . '/fixtures/testable_core_search.php');
29 require_once(__DIR__ . '/fixtures/mock_search_area.php');
31 /**
32  * Unit tests for search manager.
33  *
34  * @package     core_search
35  * @category    phpunit
36  * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
37  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class search_manager_testcase extends advanced_testcase {
41     protected $forumpostareaid = null;
42     protected $mycoursesareaid = null;
44     public function setUp() {
45         $this->forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
46         $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
47     }
49     protected function tearDown() {
50         // Stop it from faking time in the search manager (if set by test).
51         testable_core_search::fake_current_time();
52         parent::tearDown();
53     }
55     public function test_search_enabled() {
57         $this->resetAfterTest();
59         // Disabled by default.
60         $this->assertFalse(\core_search\manager::is_global_search_enabled());
62         set_config('enableglobalsearch', true);
63         $this->assertTrue(\core_search\manager::is_global_search_enabled());
65         set_config('enableglobalsearch', false);
66         $this->assertFalse(\core_search\manager::is_global_search_enabled());
67     }
69     public function test_search_areas() {
70         global $CFG;
72         $this->resetAfterTest();
74         set_config('enableglobalsearch', true);
76         $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
78         $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
79         $this->assertInstanceOf('\core_search\base', $searcharea);
81         $this->assertFalse(\core_search\manager::get_search_area($fakeareaid));
83         $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list());
84         $this->assertArrayNotHasKey($fakeareaid, \core_search\manager::get_search_areas_list());
86         // Enabled by default once global search is enabled.
87         $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
89         list($componentname, $varname) = $searcharea->get_config_var_name();
90         set_config($varname . '_enabled', 0, $componentname);
91         \core_search\manager::clear_static();
93         $this->assertArrayNotHasKey('mod_forum', \core_search\manager::get_search_areas_list(true));
95         set_config($varname . '_enabled', 1, $componentname);
97         // Although the result is wrong, we want to check that \core_search\manager::get_search_areas_list returns cached results.
98         $this->assertArrayNotHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
100         // Now we check the real result.
101         \core_search\manager::clear_static();
102         $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
103     }
105     public function test_search_config() {
107         $this->resetAfterTest();
109         $search = testable_core_search::instance();
111         // We should test both plugin types and core subsystems. No core subsystems available yet.
112         $searcharea = $search->get_search_area($this->forumpostareaid);
114         list($componentname, $varname) = $searcharea->get_config_var_name();
116         // Just with a couple of vars should be enough.
117         $start = time() - 100;
118         $end = time();
119         set_config($varname . '_indexingstart', $start, $componentname);
120         set_config($varname . '_indexingend', $end, $componentname);
122         $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
123         $this->assertEquals($start, $configs[$this->forumpostareaid]->indexingstart);
124         $this->assertEquals($end, $configs[$this->forumpostareaid]->indexingend);
125         $this->assertEquals(false, $configs[$this->forumpostareaid]->partial);
127         try {
128             $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
129             $search->reset_config($fakeareaid);
130             $this->fail('An exception should be triggered if the provided search area does not exist.');
131         } catch (moodle_exception $ex) {
132             $this->assertContains($fakeareaid . ' search area is not available.', $ex->getMessage());
133         }
135         // We clean it all but enabled components.
136         $search->reset_config($this->forumpostareaid);
137         $config = $searcharea->get_config();
138         $this->assertEquals(1, $config[$varname . '_enabled']);
139         $this->assertEquals(0, $config[$varname . '_indexingstart']);
140         $this->assertEquals(0, $config[$varname . '_indexingend']);
141         $this->assertEquals(0, $config[$varname . '_lastindexrun']);
142         $this->assertEquals(0, $config[$varname . '_partial']);
143         // No caching.
144         $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
145         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
146         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
148         set_config($varname . '_indexingstart', $start, $componentname);
149         set_config($varname . '_indexingend', $end, $componentname);
151         // All components config should be reset.
152         $search->reset_config();
153         $this->assertEquals(0, get_config($componentname, $varname . '_indexingstart'));
154         $this->assertEquals(0, get_config($componentname, $varname . '_indexingend'));
155         $this->assertEquals(0, get_config($componentname, $varname . '_lastindexrun'));
156         // No caching.
157         $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
158         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
159         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
160     }
162     /**
163      * Tests the get_last_indexing_duration method in the base area class.
164      */
165     public function test_get_last_indexing_duration() {
166         $this->resetAfterTest();
168         $search = testable_core_search::instance();
170         $searcharea = $search->get_search_area($this->forumpostareaid);
172         // When never indexed, the duration is false.
173         $this->assertSame(false, $searcharea->get_last_indexing_duration());
175         // Set the start/end times.
176         list($componentname, $varname) = $searcharea->get_config_var_name();
177         $start = time() - 100;
178         $end = time();
179         set_config($varname . '_indexingstart', $start, $componentname);
180         set_config($varname . '_indexingend', $end, $componentname);
182         // The duration should now be 100.
183         $this->assertSame(100, $searcharea->get_last_indexing_duration());
184     }
186     /**
187      * Tests that partial indexing works correctly.
188      */
189     public function test_partial_indexing() {
190         global $USER;
192         $this->resetAfterTest();
193         $this->setAdminUser();
195         // Create a course and a forum.
196         $generator = $this->getDataGenerator();
197         $course = $generator->create_course();
198         $forum = $generator->create_module('forum', ['course' => $course->id]);
200         // Index everything up to current. Ensure the course is older than current second so it
201         // definitely doesn't get indexed again next time.
202         $this->waitForSecond();
203         $search = testable_core_search::instance();
204         $search->index(false, 0);
206         $searcharea = $search->get_search_area($this->forumpostareaid);
207         list($componentname, $varname) = $searcharea->get_config_var_name();
208         $this->assertFalse(get_config($componentname, $varname . '_partial'));
210         // Add 3 discussions to the forum.
211         $now = time();
212         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
213                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now,
214                 'name' => 'Frog']);
215         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
216                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
217                 'name' => 'Toad']);
218         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
219                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
220                 'name' => 'Zombie']);
221         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
222                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
223                 'name' => 'Werewolf']);
224         time_sleep_until($now + 3);
226         // Clear the count of added documents.
227         $search->get_engine()->get_and_clear_added_documents();
229         // Make the search engine delay while indexing each document.
230         $search->get_engine()->set_add_delay(1.2);
232         // Use fake time, starting from now.
233         testable_core_search::fake_current_time(time());
235         // Index with a limit of 2 seconds - it should index 2 of the documents (after the second
236         // one, it will have taken 2.4 seconds so it will stop).
237         $search->index(false, 2);
238         $added = $search->get_engine()->get_and_clear_added_documents();
239         $this->assertCount(2, $added);
240         $this->assertEquals('Frog', $added[0]->get('title'));
241         $this->assertEquals('Toad', $added[1]->get('title'));
242         $this->assertEquals(1, get_config($componentname, $varname . '_partial'));
243         // Whilst 2.4 seconds of "time" have elapsed, the indexing duration is
244         // measured in seconds, so should be 2.
245         $this->assertEquals(2, $searcharea->get_last_indexing_duration());
247         // Add a label.
248         $generator->create_module('label', ['course' => $course->id, 'intro' => 'Vampire']);
250         // Wait to next second (so as to not reindex the label more than once, as it will now
251         // be timed before the indexing run).
252         $this->waitForSecond();
253         testable_core_search::fake_current_time(time());
255         // Next index with 1 second limit should do the label and not the forum - the logic is,
256         // if it spent ages indexing an area last time, do that one last on next run.
257         $search->index(false, 1);
258         $added = $search->get_engine()->get_and_clear_added_documents();
259         $this->assertCount(1, $added);
260         $this->assertEquals('Vampire', $added[0]->get('title'));
262         // Index again with a 3 second limit - it will redo last post for safety (because of other
263         // things possibly having the same time second), and then do the remaining one. (Note:
264         // because it always does more than one second worth of items, it would actually index 2
265         // posts even if the limit were less than 2, we are testing it does 3 posts to make sure
266         // the time limiting is actually working with the specified time.)
267         $search->index(false, 3);
268         $added = $search->get_engine()->get_and_clear_added_documents();
269         $this->assertCount(3, $added);
270         $this->assertEquals('Toad', $added[0]->get('title'));
271         $remainingtitles = [$added[1]->get('title'), $added[2]->get('title')];
272         sort($remainingtitles);
273         $this->assertEquals(['Werewolf', 'Zombie'], $remainingtitles);
274         $this->assertFalse(get_config($componentname, $varname . '_partial'));
276         // Index again - there should be nothing to index this time.
277         $search->index(false, 2);
278         $added = $search->get_engine()->get_and_clear_added_documents();
279         $this->assertCount(0, $added);
280         $this->assertFalse(get_config($componentname, $varname . '_partial'));
281     }
283     /**
284      * Tests the progress display while indexing.
285      *
286      * This tests the different logic about displaying progress for slow/fast and
287      * complete/incomplete processing.
288      */
289     public function test_index_progress() {
290         $this->resetAfterTest();
291         $generator = $this->getDataGenerator();
293         // Set up the fake search area.
294         $search = testable_core_search::instance();
295         $area = new \core_mocksearch\search\mock_search_area();
296         $search->add_search_area('whatever', $area);
297         $searchgenerator = $generator->get_plugin_generator('core_search');
298         $searchgenerator->setUp();
300         // Add records with specific time modified values.
301         $time = strtotime('2017-11-01 01:00');
302         for ($i = 0; $i < 8; $i ++) {
303             $searchgenerator->create_record((object)['timemodified' => $time]);
304             $time += 60;
305         }
307         // Simulate slow progress on indexing and initial query.
308         $now = strtotime('2017-11-11 01:00');
309         \testable_core_search::fake_current_time($now);
310         $area->set_indexing_delay(10.123);
311         $search->get_engine()->set_add_delay(15.789);
313         // Run search indexing and check output.
314         $progress = new progress_trace_buffer(new text_progress_trace(), false);
315         $search->index(false, 75, $progress);
316         $out = $progress->get_buffer();
317         $progress->reset_buffer();
319         // Check for the standard text.
320         $this->assertContains('Processing area: Mock search area', $out);
321         $this->assertContains('Stopping indexing due to time limit', $out);
323         // Check for initial query performance indication.
324         $this->assertContains('Initial query took 10.1 seconds.', $out);
326         // Check for the two (approximately) every-30-seconds messages.
327         $this->assertContains('01:00:41: Done to 1/11/17, 01:01', $out);
328         $this->assertContains('01:01:13: Done to 1/11/17, 01:03', $out);
330         // Check for the 'not complete' indicator showing when it was done until.
331         $this->assertContains('Processed 5 records containing 5 documents, in 89.1 seconds ' .
332                 '(not complete; done to 1/11/17, 01:04)', $out);
334         // Make the initial query delay less than 5 seconds, so it won't appear. Make the documents
335         // quicker, so that the 30-second delay won't be needed.
336         $area->set_indexing_delay(4.9);
337         $search->get_engine()->set_add_delay(1);
339         // Run search indexing (still partial) and check output.
340         $progress = new progress_trace_buffer(new text_progress_trace(), false);
341         $search->index(false, 5, $progress);
342         $out = $progress->get_buffer();
343         $progress->reset_buffer();
345         $this->assertContains('Processing area: Mock search area', $out);
346         $this->assertContains('Stopping indexing due to time limit', $out);
347         $this->assertNotContains('Initial query took', $out);
348         $this->assertNotContains(': Done to', $out);
349         $this->assertContains('Processed 2 records containing 2 documents, in 6.9 seconds ' .
350                 '(not complete; done to 1/11/17, 01:05).', $out);
352         // Run the remaining items to complete it.
353         $progress = new progress_trace_buffer(new text_progress_trace(), false);
354         $search->index(false, 100, $progress);
355         $out = $progress->get_buffer();
356         $progress->reset_buffer();
358         $this->assertContains('Processing area: Mock search area', $out);
359         $this->assertNotContains('Stopping indexing due to time limit', $out);
360         $this->assertNotContains('Initial query took', $out);
361         $this->assertNotContains(': Done to', $out);
362         $this->assertContains('Processed 3 records containing 3 documents, in 7.9 seconds.', $out);
364         $searchgenerator->tearDown();
365     }
367     /**
368      * Tests that documents with modified time in the future are NOT indexed (as this would cause
369      * a problem by preventing it from indexing other documents modified between now and the future
370      * date).
371      */
372     public function test_future_documents() {
373         $this->resetAfterTest();
375         // Create a course and a forum.
376         $generator = $this->getDataGenerator();
377         $course = $generator->create_course();
378         $forum = $generator->create_module('forum', ['course' => $course->id]);
380         // Index everything up to current. Ensure the course is older than current second so it
381         // definitely doesn't get indexed again next time.
382         $this->waitForSecond();
383         $search = testable_core_search::instance();
384         $search->index(false, 0);
385         $search->get_engine()->get_and_clear_added_documents();
387         // Add 2 discussions to the forum, one of which happend just now, but the other is
388         // incorrectly set to the future.
389         $now = time();
390         $userid = get_admin()->id;
391         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
392                 'forum' => $forum->id, 'userid' => $userid, 'timemodified' => $now,
393                 'name' => 'Frog']);
394         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
395                 'forum' => $forum->id, 'userid' => $userid, 'timemodified' => $now + 100,
396                 'name' => 'Toad']);
398         // Wait for a second so we're not actually on the same second as the forum post (there's a
399         // 1 second overlap between indexing; it would get indexed in both checks below otherwise).
400         $this->waitForSecond();
402         // Index.
403         $search->index(false);
404         $added = $search->get_engine()->get_and_clear_added_documents();
405         $this->assertCount(1, $added);
406         $this->assertEquals('Frog', $added[0]->get('title'));
408         // Check latest time - it should be the same as $now, not the + 100.
409         $searcharea = $search->get_search_area($this->forumpostareaid);
410         list($componentname, $varname) = $searcharea->get_config_var_name();
411         $this->assertEquals($now, get_config($componentname, $varname . '_lastindexrun'));
413         // Index again - there should be nothing to index this time.
414         $search->index(false);
415         $added = $search->get_engine()->get_and_clear_added_documents();
416         $this->assertCount(0, $added);
417     }
419     /**
420      * Tests that indexing a specified context works correctly.
421      */
422     public function test_context_indexing() {
423         global $USER;
425         $this->resetAfterTest();
426         $this->setAdminUser();
428         // Create a course and two forums and a page.
429         $generator = $this->getDataGenerator();
430         $course = $generator->create_course();
431         $now = time();
432         $forum1 = $generator->create_module('forum', ['course' => $course->id]);
433         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
434                 'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now,
435                 'name' => 'Frog']);
436         $this->waitForSecond();
437         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
438                 'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
439                 'name' => 'Zombie']);
440         $forum2 = $generator->create_module('forum', ['course' => $course->id]);
441         $this->waitForSecond();
442         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
443                 'forum' => $forum2->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
444                 'name' => 'Toad']);
445         $generator->create_module('page', ['course' => $course->id]);
446         $generator->create_module('forum', ['course' => $course->id]);
448         // Index forum 1 only.
449         $search = testable_core_search::instance();
450         $buffer = new progress_trace_buffer(new text_progress_trace(), false);
451         $result = $search->index_context(\context_module::instance($forum1->cmid), '', 0, $buffer);
452         $this->assertTrue($result->complete);
453         $log = $buffer->get_buffer();
454         $buffer->reset_buffer();
456         // Confirm that output only processed 1 forum activity and 2 posts.
457         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 1 "));
458         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
460         // Confirm that some areas for different types of context were skipped.
461         $this->assertNotFalse(strpos($log, "area: Users\n  Skipping"));
462         $this->assertNotFalse(strpos($log, "area: My courses\n  Skipping"));
464         // Confirm that another module area had no results.
465         $this->assertNotFalse(strpos($log, "area: Page\n  No documents"));
467         // Index whole course.
468         $result = $search->index_context(\context_course::instance($course->id), '', 0, $buffer);
469         $this->assertTrue($result->complete);
470         $log = $buffer->get_buffer();
471         $buffer->reset_buffer();
473         // Confirm that output processed 3 forum activities and 3 posts.
474         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 3 "));
475         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
477         // The course area was also included this time.
478         $this->assertNotFalse(strpos($log, "area: My courses\n  Processed 1 "));
480         // Confirm that another module area had results too.
481         $this->assertNotFalse(strpos($log, "area: Page\n  Processed 1 "));
483         // Index whole course, but only forum posts.
484         $result = $search->index_context(\context_course::instance($course->id), 'mod_forum-post',
485                 0, $buffer);
486         $this->assertTrue($result->complete);
487         $log = $buffer->get_buffer();
488         $buffer->reset_buffer();
490         // Confirm that output processed 3 posts but not forum activities.
491         $this->assertFalse(strpos($log, "area: Forum - activity information"));
492         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
494         // Set time limit and retry index of whole course, taking 3 tries to complete it.
495         $search->get_engine()->set_add_delay(0.4);
496         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer);
497         $log = $buffer->get_buffer();
498         $buffer->reset_buffer();
499         $this->assertFalse($result->complete);
500         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
502         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
503                 $result->startfromarea, $result->startfromtime);
504         $log = $buffer->get_buffer();
505         $buffer->reset_buffer();
506         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
507         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
508         $this->assertFalse($result->complete);
510         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
511                 $result->startfromarea, $result->startfromtime);
512         $log = $buffer->get_buffer();
513         $buffer->reset_buffer();
514         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
515         $this->assertTrue($result->complete);
516     }
518     /**
519      * Adding this test here as get_areas_user_accesses process is the same, results just depend on the context level.
520      *
521      * @return void
522      */
523     public function test_search_user_accesses() {
524         global $DB;
526         $this->resetAfterTest();
528         $frontpage = $DB->get_record('course', array('id' => SITEID));
529         $frontpagectx = context_course::instance($frontpage->id);
530         $course1 = $this->getDataGenerator()->create_course();
531         $course1ctx = context_course::instance($course1->id);
532         $course2 = $this->getDataGenerator()->create_course();
533         $course2ctx = context_course::instance($course2->id);
534         $teacher = $this->getDataGenerator()->create_user();
535         $teacherctx = context_user::instance($teacher->id);
536         $student = $this->getDataGenerator()->create_user();
537         $studentctx = context_user::instance($student->id);
538         $noaccess = $this->getDataGenerator()->create_user();
539         $noaccessctx = context_user::instance($noaccess->id);
540         $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, 'teacher');
541         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
543         $frontpageforum = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
544         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
545         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
546         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
547         $frontpageforumcontext = context_module::instance($frontpageforum->cmid);
548         $context1 = context_module::instance($forum1->cmid);
549         $context2 = context_module::instance($forum2->cmid);
550         $context3 = context_module::instance($forum3->cmid);
552         $search = testable_core_search::instance();
553         $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
554         $search->add_core_search_areas();
555         $search->add_search_area($mockareaid, new core_mocksearch\search\mock_search_area());
557         $this->setAdminUser();
558         $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
560         $sitectx = \context_course::instance(SITEID);
562         // Can access the frontpage ones.
563         $this->setUser($noaccess);
564         $contexts = $search->get_areas_user_accesses()->usercontexts;
565         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id), $contexts[$this->forumpostareaid]);
566         $this->assertEquals(array($sitectx->id => $sitectx->id), $contexts[$this->mycoursesareaid]);
567         $mockctxs = array($noaccessctx->id => $noaccessctx->id, $frontpagectx->id => $frontpagectx->id);
568         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
570         $this->setUser($teacher);
571         $contexts = $search->get_areas_user_accesses()->usercontexts;
572         $frontpageandcourse1 = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
573             $context2->id => $context2->id);
574         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
575         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
576             $contexts[$this->mycoursesareaid]);
577         $mockctxs = array($teacherctx->id => $teacherctx->id,
578                 $frontpagectx->id => $frontpagectx->id, $course1ctx->id => $course1ctx->id);
579         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
581         $this->setUser($student);
582         $contexts = $search->get_areas_user_accesses()->usercontexts;
583         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
584         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
585             $contexts[$this->mycoursesareaid]);
586         $mockctxs = array($studentctx->id => $studentctx->id,
587                 $frontpagectx->id => $frontpagectx->id, $course1ctx->id => $course1ctx->id);
588         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
590         // Hide the activity.
591         set_coursemodule_visible($forum2->cmid, 0);
592         $contexts = $search->get_areas_user_accesses()->usercontexts;
593         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id),
594             $contexts[$this->forumpostareaid]);
596         // Now test course limited searches.
597         set_coursemodule_visible($forum2->cmid, 1);
598         $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student');
599         $contexts = $search->get_areas_user_accesses()->usercontexts;
600         $allcontexts = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
601             $context2->id => $context2->id, $context3->id => $context3->id);
602         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
603         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id,
604             $course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
606         $contexts = $search->get_areas_user_accesses(array($course1->id, $course2->id))->usercontexts;
607         $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id, $context3->id => $context3->id);
608         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
609         $this->assertEquals(array($course1ctx->id => $course1ctx->id,
610             $course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
612         $contexts = $search->get_areas_user_accesses(array($course2->id))->usercontexts;
613         $allcontexts = array($context3->id => $context3->id);
614         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
615         $this->assertEquals(array($course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
617         $contexts = $search->get_areas_user_accesses(array($course1->id))->usercontexts;
618         $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id);
619         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
620         $this->assertEquals(array($course1ctx->id => $course1ctx->id), $contexts[$this->mycoursesareaid]);
622         // Test context limited search with no course limit.
623         $contexts = $search->get_areas_user_accesses(false,
624                 [$frontpageforumcontext->id, $course2ctx->id])->usercontexts;
625         $this->assertEquals([$frontpageforumcontext->id => $frontpageforumcontext->id],
626                 $contexts[$this->forumpostareaid]);
627         $this->assertEquals([$course2ctx->id => $course2ctx->id],
628                 $contexts[$this->mycoursesareaid]);
630         // Test context limited search with course limit.
631         $contexts = $search->get_areas_user_accesses([$course1->id, $course2->id],
632                 [$frontpageforumcontext->id, $course2ctx->id])->usercontexts;
633         $this->assertArrayNotHasKey($this->forumpostareaid, $contexts);
634         $this->assertEquals([$course2ctx->id => $course2ctx->id],
635                 $contexts[$this->mycoursesareaid]);
637         // Single context and course.
638         $contexts = $search->get_areas_user_accesses([$course1->id], [$context1->id])->usercontexts;
639         $this->assertEquals([$context1->id => $context1->id], $contexts[$this->forumpostareaid]);
640         $this->assertArrayNotHasKey($this->mycoursesareaid, $contexts);
642         // For admins, this is still limited only if we specify the things, so it should be same.
643         $this->setAdminUser();
644         $contexts = $search->get_areas_user_accesses([$course1->id], [$context1->id])->usercontexts;
645         $this->assertEquals([$context1->id => $context1->id], $contexts[$this->forumpostareaid]);
646         $this->assertArrayNotHasKey($this->mycoursesareaid, $contexts);
647     }
649     /**
650      * Tests the block support in get_search_user_accesses.
651      *
652      * @return void
653      */
654     public function test_search_user_accesses_blocks() {
655         global $DB;
657         $this->resetAfterTest();
658         $this->setAdminUser();
660         // Create course and add HTML block.
661         $generator = $this->getDataGenerator();
662         $course1 = $generator->create_course();
663         $context1 = \context_course::instance($course1->id);
664         $page = new \moodle_page();
665         $page->set_context($context1);
666         $page->set_course($course1);
667         $page->set_pagelayout('standard');
668         $page->set_pagetype('course-view');
669         $page->blocks->load_blocks();
670         $page->blocks->add_block_at_end_of_default_region('html');
672         // Create another course with HTML blocks only in some weird page or a module page (not
673         // yet supported, so both these blocks will be ignored).
674         $course2 = $generator->create_course();
675         $context2 = \context_course::instance($course2->id);
676         $page = new \moodle_page();
677         $page->set_context($context2);
678         $page->set_course($course2);
679         $page->set_pagelayout('standard');
680         $page->set_pagetype('bogus-page');
681         $page->blocks->load_blocks();
682         $page->blocks->add_block_at_end_of_default_region('html');
684         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
685         $forumcontext = context_module::instance($forum->cmid);
686         $page = new \moodle_page();
687         $page->set_context($forumcontext);
688         $page->set_course($course2);
689         $page->set_pagelayout('standard');
690         $page->set_pagetype('mod-forum-view');
691         $page->blocks->load_blocks();
692         $page->blocks->add_block_at_end_of_default_region('html');
694         // The third course has 2 HTML blocks.
695         $course3 = $generator->create_course();
696         $context3 = \context_course::instance($course3->id);
697         $page = new \moodle_page();
698         $page->set_context($context3);
699         $page->set_course($course3);
700         $page->set_pagelayout('standard');
701         $page->set_pagetype('course-view');
702         $page->blocks->load_blocks();
703         $page->blocks->add_block_at_end_of_default_region('html');
704         $page->blocks->add_block_at_end_of_default_region('html');
706         // Student 1 belongs to all 3 courses.
707         $student1 = $generator->create_user();
708         $generator->enrol_user($student1->id, $course1->id, 'student');
709         $generator->enrol_user($student1->id, $course2->id, 'student');
710         $generator->enrol_user($student1->id, $course3->id, 'student');
712         // Student 2 belongs only to course 2.
713         $student2 = $generator->create_user();
714         $generator->enrol_user($student2->id, $course2->id, 'student');
716         // And the third student is only in course 3.
717         $student3 = $generator->create_user();
718         $generator->enrol_user($student3->id, $course3->id, 'student');
720         $search = testable_core_search::instance();
721         $search->add_core_search_areas();
723         // Admin gets 'true' result to function regardless of blocks.
724         $this->setAdminUser();
725         $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
727         // Student 1 gets all 3 block contexts.
728         $this->setUser($student1);
729         $contexts = $search->get_areas_user_accesses()->usercontexts;
730         $this->assertArrayHasKey('block_html-content', $contexts);
731         $this->assertCount(3, $contexts['block_html-content']);
733         // Student 2 does not get any blocks.
734         $this->setUser($student2);
735         $contexts = $search->get_areas_user_accesses()->usercontexts;
736         $this->assertArrayNotHasKey('block_html-content', $contexts);
738         // Student 3 gets only two of them.
739         $this->setUser($student3);
740         $contexts = $search->get_areas_user_accesses()->usercontexts;
741         $this->assertArrayHasKey('block_html-content', $contexts);
742         $this->assertCount(2, $contexts['block_html-content']);
744         // A course limited search for student 1 is the same as the student 3 search.
745         $this->setUser($student1);
746         $limitedcontexts = $search->get_areas_user_accesses([$course3->id])->usercontexts;
747         $this->assertEquals($contexts['block_html-content'], $limitedcontexts['block_html-content']);
749         // Get block context ids for the blocks that appear.
750         global $DB;
751         $blockcontextids = $DB->get_fieldset_sql('
752             SELECT x.id
753               FROM {block_instances} bi
754               JOIN {context} x ON x.instanceid = bi.id AND x.contextlevel = ?
755              WHERE (parentcontextid = ? OR parentcontextid = ?)
756                    AND blockname = ?
757           ORDER BY bi.id', [CONTEXT_BLOCK, $context1->id, $context3->id, 'html']);
759         // Context limited search (no course).
760         $contexts = $search->get_areas_user_accesses(false,
761                 [$blockcontextids[0], $blockcontextids[2]])->usercontexts;
762         $this->assertCount(2, $contexts['block_html-content']);
764         // Context limited search (with course 3).
765         $contexts = $search->get_areas_user_accesses([$course2->id, $course3->id],
766                 [$blockcontextids[0], $blockcontextids[2]])->usercontexts;
767         $this->assertCount(1, $contexts['block_html-content']);
768     }
770     /**
771      * Test get_areas_user_accesses with regard to the 'all available courses' config option.
772      *
773      * @return void
774      */
775     public function test_search_user_accesses_allavailable() {
776         global $DB, $CFG;
778         $this->resetAfterTest();
780         // Front page, including a forum.
781         $frontpage = $DB->get_record('course', array('id' => SITEID));
782         $forumfront = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
783         $forumfrontctx = context_module::instance($forumfront->cmid);
785         // Course 1 does not allow guest access.
786         $course1 = $this->getDataGenerator()->create_course((object)array(
787                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
788                 'enrol_guest_password_0' => ''));
789         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
790         $forum1ctx = context_module::instance($forum1->cmid);
792         // Course 2 does not allow guest but is accessible by all users.
793         $course2 = $this->getDataGenerator()->create_course((object)array(
794                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
795                 'enrol_guest_password_0' => ''));
796         $course2ctx = context_course::instance($course2->id);
797         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
798         $forum2ctx = context_module::instance($forum2->cmid);
799         assign_capability('moodle/course:view', CAP_ALLOW, $CFG->defaultuserroleid, $course2ctx->id);
801         // Course 3 allows guest access without password.
802         $course3 = $this->getDataGenerator()->create_course((object)array(
803                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
804                 'enrol_guest_password_0' => ''));
805         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
806         $forum3ctx = context_module::instance($forum3->cmid);
808         // Student user is enrolled in course 1.
809         $student = $this->getDataGenerator()->create_user();
810         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
812         // No access user is just a user with no permissions.
813         $noaccess = $this->getDataGenerator()->create_user();
815         // First test without the all available option.
816         $search = testable_core_search::instance();
818         // Admin user can access everything.
819         $this->setAdminUser();
820         $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
822         // No-access user can access only the front page forum.
823         $this->setUser($noaccess);
824         $contexts = $search->get_areas_user_accesses()->usercontexts;
825         $this->assertEquals([$forumfrontctx->id], array_keys($contexts[$this->forumpostareaid]));
827         // Student can access the front page forum plus the enrolled one.
828         $this->setUser($student);
829         $contexts = $search->get_areas_user_accesses()->usercontexts;
830         $this->assertEquals([$forum1ctx->id, $forumfrontctx->id],
831                 array_keys($contexts[$this->forumpostareaid]));
833         // Now turn on the all available option.
834         set_config('searchallavailablecourses', 1);
836         // Admin user can access everything.
837         $this->setAdminUser();
838         $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
840         // No-access user can access the front page forum and course 2, 3.
841         $this->setUser($noaccess);
842         $contexts = $search->get_areas_user_accesses()->usercontexts;
843         $this->assertEquals([$forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
844                 array_keys($contexts[$this->forumpostareaid]));
846         // Student can access the front page forum plus the enrolled one plus courses 2, 3.
847         $this->setUser($student);
848         $contexts = $search->get_areas_user_accesses()->usercontexts;
849         $this->assertEquals([$forum1ctx->id, $forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
850                 array_keys($contexts[$this->forumpostareaid]));
851     }
853     /**
854      * Tests group-related aspects of the get_areas_user_accesses function.
855      */
856     public function test_search_user_accesses_groups() {
857         global $DB;
859         $this->resetAfterTest();
860         $this->setAdminUser();
862         // Create 2 courses each with 2 groups and 2 forums (separate/visible groups).
863         $generator = $this->getDataGenerator();
864         $course1 = $generator->create_course();
865         $course2 = $generator->create_course();
866         $group1 = $generator->create_group(['courseid' => $course1->id]);
867         $group2 = $generator->create_group(['courseid' => $course1->id]);
868         $group3 = $generator->create_group(['courseid' => $course2->id]);
869         $group4 = $generator->create_group(['courseid' => $course2->id]);
870         $forum1s = $generator->create_module('forum', ['course' => $course1->id, 'groupmode' => SEPARATEGROUPS]);
871         $id1s = context_module::instance($forum1s->cmid)->id;
872         $forum1v = $generator->create_module('forum', ['course' => $course1->id, 'groupmode' => VISIBLEGROUPS]);
873         $id1v = context_module::instance($forum1v->cmid)->id;
874         $forum2s = $generator->create_module('forum', ['course' => $course2->id, 'groupmode' => SEPARATEGROUPS]);
875         $id2s = context_module::instance($forum2s->cmid)->id;
876         $forum2n = $generator->create_module('forum', ['course' => $course2->id, 'groupmode' => NOGROUPS]);
877         $id2n = context_module::instance($forum2n->cmid)->id;
879         // Get search instance.
880         $search = testable_core_search::instance();
881         $search->add_core_search_areas();
883         // User 1 is a manager in one course and a student in the other one. They belong to
884         // all of the groups 1, 2, 3, and 4.
885         $user1 = $generator->create_user();
886         $generator->enrol_user($user1->id, $course1->id, 'manager');
887         $generator->enrol_user($user1->id, $course2->id, 'student');
888         groups_add_member($group1, $user1);
889         groups_add_member($group2, $user1);
890         groups_add_member($group3, $user1);
891         groups_add_member($group4, $user1);
893         $this->setUser($user1);
894         $accessinfo = $search->get_areas_user_accesses();
895         $contexts = $accessinfo->usercontexts;
897         // Double-check all the forum contexts.
898         $postcontexts = $contexts['mod_forum-post'];
899         sort($postcontexts);
900         $this->assertEquals([$id1s, $id1v, $id2s, $id2n], $postcontexts);
902         // Only the context in the second course (no accessallgroups) is restricted.
903         $restrictedcontexts = $accessinfo->separategroupscontexts;
904         sort($restrictedcontexts);
905         $this->assertEquals([$id2s], $restrictedcontexts);
907         // Only the groups from the second course (no accessallgroups) are included.
908         $groupids = $accessinfo->usergroups;
909         sort($groupids);
910         $this->assertEquals([$group3->id, $group4->id], $groupids);
912         // User 2 is a student in each course and belongs to groups 2 and 4.
913         $user2 = $generator->create_user();
914         $generator->enrol_user($user2->id, $course1->id, 'student');
915         $generator->enrol_user($user2->id, $course2->id, 'student');
916         groups_add_member($group2, $user2);
917         groups_add_member($group4, $user2);
919         $this->setUser($user2);
920         $accessinfo = $search->get_areas_user_accesses();
921         $contexts = $accessinfo->usercontexts;
923         // Double-check all the forum contexts.
924         $postcontexts = $contexts['mod_forum-post'];
925         sort($postcontexts);
926         $this->assertEquals([$id1s, $id1v, $id2s, $id2n], $postcontexts);
928         // Both separate groups forums are restricted.
929         $restrictedcontexts = $accessinfo->separategroupscontexts;
930         sort($restrictedcontexts);
931         $this->assertEquals([$id1s, $id2s], $restrictedcontexts);
933         // Groups from both courses are included.
934         $groupids = $accessinfo->usergroups;
935         sort($groupids);
936         $this->assertEquals([$group2->id, $group4->id], $groupids);
938         // User 3 is a manager at system level.
939         $user3 = $generator->create_user();
940         role_assign($DB->get_field('role', 'id', ['shortname' => 'manager'], MUST_EXIST), $user3->id,
941                 \context_system::instance());
943         $this->setUser($user3);
944         $accessinfo = $search->get_areas_user_accesses();
946         // Nothing is restricted and no groups are relevant.
947         $this->assertEquals([], $accessinfo->separategroupscontexts);
948         $this->assertEquals([], $accessinfo->usergroups);
949     }
951     /**
952      * test_is_search_area
953      *
954      * @return void
955      */
956     public function test_is_search_area() {
958         $this->assertFalse(testable_core_search::is_search_area('\asd\asd'));
959         $this->assertFalse(testable_core_search::is_search_area('\mod_forum\search\posta'));
960         $this->assertFalse(testable_core_search::is_search_area('\core_search\base_mod'));
961         $this->assertTrue(testable_core_search::is_search_area('\mod_forum\search\post'));
962         $this->assertTrue(testable_core_search::is_search_area('\\mod_forum\\search\\post'));
963         $this->assertTrue(testable_core_search::is_search_area('mod_forum\\search\\post'));
964     }
966     /**
967      * Tests the request_index function used for reindexing certain contexts. This only tests
968      * adding things to the request list, it doesn't test that they are actually indexed by the
969      * scheduled task.
970      */
971     public function test_request_index() {
972         global $DB;
974         $this->resetAfterTest();
976         $course1 = $this->getDataGenerator()->create_course();
977         $course1ctx = context_course::instance($course1->id);
978         $course2 = $this->getDataGenerator()->create_course();
979         $course2ctx = context_course::instance($course2->id);
980         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
981         $forum1ctx = context_module::instance($forum1->cmid);
982         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
983         $forum2ctx = context_module::instance($forum2->cmid);
985         // Initially no requests.
986         $this->assertEquals(0, $DB->count_records('search_index_requests'));
988         // Request update for course 1, all areas.
989         \core_search\manager::request_index($course1ctx);
991         // Check all details of entry.
992         $results = array_values($DB->get_records('search_index_requests'));
993         $this->assertCount(1, $results);
994         $this->assertEquals($course1ctx->id, $results[0]->contextid);
995         $this->assertEquals('', $results[0]->searcharea);
996         $now = time();
997         $this->assertLessThanOrEqual($now, $results[0]->timerequested);
998         $this->assertGreaterThan($now - 10, $results[0]->timerequested);
999         $this->assertEquals('', $results[0]->partialarea);
1000         $this->assertEquals(0, $results[0]->partialtime);
1002         // Request forum 1, all areas; not added as covered by course 1.
1003         \core_search\manager::request_index($forum1ctx);
1004         $this->assertEquals(1, $DB->count_records('search_index_requests'));
1006         // Request forum 1, specific area; not added as covered by course 1 all areas.
1007         \core_search\manager::request_index($forum1ctx, 'forum-post');
1008         $this->assertEquals(1, $DB->count_records('search_index_requests'));
1010         // Request course 1 again, specific area; not added as covered by all areas.
1011         \core_search\manager::request_index($course1ctx, 'forum-post');
1012         $this->assertEquals(1, $DB->count_records('search_index_requests'));
1014         // Request course 1 again, all areas; not needed as covered already.
1015         \core_search\manager::request_index($course1ctx);
1016         $this->assertEquals(1, $DB->count_records('search_index_requests'));
1018         // Request course 2, specific area.
1019         \core_search\manager::request_index($course2ctx, 'label-activity');
1020         // Note: I'm ordering by ID for convenience - this is dangerous in real code (see MDL-43447)
1021         // but in a unit test it shouldn't matter as nobody is using clustered databases for unit
1022         // test.
1023         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1024         $this->assertCount(2, $results);
1025         $this->assertEquals($course1ctx->id, $results[0]->contextid);
1026         $this->assertEquals($course2ctx->id, $results[1]->contextid);
1027         $this->assertEquals('label-activity', $results[1]->searcharea);
1029         // Request forum 2, same specific area; not added.
1030         \core_search\manager::request_index($forum2ctx, 'label-activity');
1031         $this->assertEquals(2, $DB->count_records('search_index_requests'));
1033         // Request forum 2, different specific area; added.
1034         \core_search\manager::request_index($forum2ctx, 'forum-post');
1035         $this->assertEquals(3, $DB->count_records('search_index_requests'));
1037         // Request forum 2, all areas; also added. (Note: This could obviously remove the previous
1038         // one, but for simplicity, I didn't make it do that; also it could perhaps cause problems
1039         // if we had already begun processing the previous entry.)
1040         \core_search\manager::request_index($forum2ctx);
1041         $this->assertEquals(4, $DB->count_records('search_index_requests'));
1043         // Clear queue and do tests relating to priority.
1044         $DB->delete_records('search_index_requests');
1046         // Request forum 1, specific area, priority 100.
1047         \core_search\manager::request_index($forum1ctx, 'forum-post', 100);
1048         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1049         $this->assertCount(1, $results);
1050         $this->assertEquals(100, $results[0]->indexpriority);
1052         // Request forum 1, same area, lower priority; no change.
1053         \core_search\manager::request_index($forum1ctx, 'forum-post', 99);
1054         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1055         $this->assertCount(1, $results);
1056         $this->assertEquals(100, $results[0]->indexpriority);
1058         // Request forum 1, same area, higher priority; priority stored changes.
1059         \core_search\manager::request_index($forum1ctx, 'forum-post', 101);
1060         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1061         $this->assertCount(1, $results);
1062         $this->assertEquals(101, $results[0]->indexpriority);
1064         // Request forum 1, all areas, lower priority; adds second entry.
1065         \core_search\manager::request_index($forum1ctx, '', 100);
1066         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1067         $this->assertCount(2, $results);
1068         $this->assertEquals(100, $results[1]->indexpriority);
1070         // Request course 1, all areas, lower priority; adds third entry.
1071         \core_search\manager::request_index($course1ctx, '', 99);
1072         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1073         $this->assertCount(3, $results);
1074         $this->assertEquals(99, $results[2]->indexpriority);
1075     }
1077     /**
1078      * Tests the process_index_requests function.
1079      */
1080     public function test_process_index_requests() {
1081         global $DB;
1083         $this->resetAfterTest();
1085         $search = testable_core_search::instance();
1087         // When there are no index requests, nothing gets logged.
1088         $progress = new progress_trace_buffer(new text_progress_trace(), false);
1089         $search->process_index_requests(0.0, $progress);
1090         $out = $progress->get_buffer();
1091         $progress->reset_buffer();
1092         $this->assertEquals('', $out);
1094         // Set up the course with 3 forums.
1095         $generator = $this->getDataGenerator();
1096         $course = $generator->create_course(['fullname' => 'TCourse']);
1097         $forum1 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum1']);
1098         $forum2 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum2']);
1099         $forum3 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum3']);
1101         // Hack the forums so they have different creation times.
1102         $now = time();
1103         $DB->set_field('forum', 'timemodified', $now - 30, ['id' => $forum1->id]);
1104         $DB->set_field('forum', 'timemodified', $now - 20, ['id' => $forum2->id]);
1105         $DB->set_field('forum', 'timemodified', $now - 10, ['id' => $forum3->id]);
1106         $forum2time = $now - 20;
1108         // Make 2 index requests.
1109         testable_core_search::fake_current_time($now - 3);
1110         $search::request_index(context_course::instance($course->id), 'mod_label-activity');
1111         testable_core_search::fake_current_time($now - 2);
1112         $search::request_index(context_module::instance($forum1->cmid));
1114         // Run with no time limit.
1115         $search->process_index_requests(0.0, $progress);
1116         $out = $progress->get_buffer();
1117         $progress->reset_buffer();
1119         // Check that it's done both areas.
1120         $this->assertContains(
1121                 'Indexing requested context: Course: TCourse (search area: mod_label-activity)',
1122                 $out);
1123         $this->assertContains(
1124                 'Completed requested context: Course: TCourse (search area: mod_label-activity)',
1125                 $out);
1126         $this->assertContains('Indexing requested context: Forum: TForum1', $out);
1127         $this->assertContains('Completed requested context: Forum: TForum1', $out);
1129         // Check the requests database table is now empty.
1130         $this->assertEquals(0, $DB->count_records('search_index_requests'));
1132         // Request indexing the course a couple of times.
1133         testable_core_search::fake_current_time($now - 3);
1134         $search::request_index(context_course::instance($course->id), 'mod_forum-activity');
1135         testable_core_search::fake_current_time($now - 2);
1136         $search::request_index(context_course::instance($course->id), 'mod_forum-post');
1138         // Do the processing again with a time limit and indexing delay. The time limit is too
1139         // small; because of the way the logic works, this means it will index 2 activities.
1140         $search->get_engine()->set_add_delay(0.2);
1141         $search->process_index_requests(0.1, $progress);
1142         $out = $progress->get_buffer();
1143         $progress->reset_buffer();
1145         // Confirm the right wrapper information was logged.
1146         $this->assertContains(
1147                 'Indexing requested context: Course: TCourse (search area: mod_forum-activity)',
1148                 $out);
1149         $this->assertContains('Stopping indexing due to time limit', $out);
1150         $this->assertContains(
1151                 'Ending requested context: Course: TCourse (search area: mod_forum-activity)',
1152                 $out);
1154         // Check the database table has been updated with progress.
1155         $records = array_values($DB->get_records('search_index_requests', null, 'searcharea'));
1156         $this->assertEquals('mod_forum-activity', $records[0]->partialarea);
1157         $this->assertEquals($forum2time, $records[0]->partialtime);
1159         // Run again and confirm it now finishes.
1160         $search->process_index_requests(2.0, $progress);
1161         $out = $progress->get_buffer();
1162         $progress->reset_buffer();
1163         $this->assertContains(
1164                 'Completed requested context: Course: TCourse (search area: mod_forum-activity)',
1165                 $out);
1166         $this->assertContains(
1167                 'Completed requested context: Course: TCourse (search area: mod_forum-post)',
1168                 $out);
1170         // Confirm table is now empty.
1171         $this->assertEquals(0, $DB->count_records('search_index_requests'));
1173         // Make 2 requests - first one is low priority.
1174         testable_core_search::fake_current_time($now - 3);
1175         $search::request_index(context_module::instance($forum1->cmid), 'mod_forum-activity',
1176                 \core_search\manager::INDEX_PRIORITY_REINDEXING);
1177         testable_core_search::fake_current_time($now - 2);
1178         $search::request_index(context_module::instance($forum2->cmid), 'mod_forum-activity');
1180         // Process with short time limit and confirm it does the second one first.
1181         $search->process_index_requests(0.1, $progress);
1182         $out = $progress->get_buffer();
1183         $progress->reset_buffer();
1184         $this->assertContains(
1185                 'Completed requested context: Forum: TForum2 (search area: mod_forum-activity)',
1186                 $out);
1187         $search->process_index_requests(0.1, $progress);
1188         $out = $progress->get_buffer();
1189         $progress->reset_buffer();
1190         $this->assertContains(
1191                 'Completed requested context: Forum: TForum1 (search area: mod_forum-activity)',
1192                 $out);
1194         // Make a request for a course context...
1195         $course = $generator->create_course();
1196         $context = context_course::instance($course->id);
1197         $search::request_index($context);
1199         // ...but then delete it (note: delete_course spews output, so we throw it away).
1200         ob_start();
1201         delete_course($course);
1202         ob_end_clean();
1204         // Process requests - it should only note the deleted context.
1205         $search->process_index_requests(10, $progress);
1206         $out = $progress->get_buffer();
1207         $progress->reset_buffer();
1208         $this->assertContains('Skipped deleted context: ' . $context->id, $out);
1210         // Confirm request table is now empty.
1211         $this->assertEquals(0, $DB->count_records('search_index_requests'));
1212     }
1214     /**
1215      * Test search area categories.
1216      */
1217     public function test_get_search_area_categories() {
1218         $categories = \core_search\manager::get_search_area_categories();
1220         $this->assertTrue(is_array($categories));
1221         $this->assertTrue(count($categories) >= 4); // We always should have 4 core categories.
1222         $this->assertArrayHasKey('core-all', $categories);
1223         $this->assertArrayHasKey('core-course-content', $categories);
1224         $this->assertArrayHasKey('core-courses', $categories);
1225         $this->assertArrayHasKey('core-users', $categories);
1227         foreach ($categories as $category) {
1228             $this->assertInstanceOf('\core_search\area_category', $category);
1229         }
1230     }
1232     /**
1233      * Test that we can find out if search area categories functionality is enabled.
1234      */
1235     public function test_is_search_area_categories_enabled() {
1236         $this->resetAfterTest();
1238         $this->assertFalse(\core_search\manager::is_search_area_categories_enabled());
1239         set_config('searchenablecategories', 1);
1240         $this->assertTrue(\core_search\manager::is_search_area_categories_enabled());
1241         set_config('searchenablecategories', 0);
1242         $this->assertFalse(\core_search\manager::is_search_area_categories_enabled());
1243     }
1245     /**
1246      * Test that we can find out if hiding all results category is enabled.
1247      */
1248     public function test_should_hide_all_results_category() {
1249         $this->resetAfterTest();
1251         $this->assertEquals(0, \core_search\manager::should_hide_all_results_category());
1252         set_config('searchhideallcategory', 1);
1253         $this->assertEquals(1, \core_search\manager::should_hide_all_results_category());
1254         set_config('searchhideallcategory', 0);
1255         $this->assertEquals(0, \core_search\manager::should_hide_all_results_category());
1256     }
1258     /**
1259      * Test that we can get default search category name.
1260      */
1261     public function test_get_default_area_category_name() {
1262         $this->resetAfterTest();
1264         $expected = 'core-all';
1265         $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
1267         set_config('searchhideallcategory', 1);
1268         $expected = 'core-course-content';
1269         $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
1271         set_config('searchhideallcategory', 0);
1272         $expected = 'core-all';
1273         $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
1274     }
1276     /**
1277      * Test that we can get correct search area category by its name.
1278      */
1279     public function test_get_search_area_category_by_name() {
1280         $this->resetAfterTest();
1282         $testcategory = \core_search\manager::get_search_area_category_by_name('test_random_name');
1283         $this->assertEquals('core-all', $testcategory->get_name());
1285         $testcategory = \core_search\manager::get_search_area_category_by_name('core-courses');
1286         $this->assertEquals('core-courses', $testcategory->get_name());
1288         set_config('searchhideallcategory', 1);
1289         $testcategory = \core_search\manager::get_search_area_category_by_name('test_random_name');
1290         $this->assertEquals('core-course-content', $testcategory->get_name());
1291     }