MDL-55356 core_search: Make indexing task/CLI do context requests
[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     public function test_search_enabled() {
51         $this->resetAfterTest();
53         // Disabled by default.
54         $this->assertFalse(\core_search\manager::is_global_search_enabled());
56         set_config('enableglobalsearch', true);
57         $this->assertTrue(\core_search\manager::is_global_search_enabled());
59         set_config('enableglobalsearch', false);
60         $this->assertFalse(\core_search\manager::is_global_search_enabled());
61     }
63     public function test_search_areas() {
64         global $CFG;
66         $this->resetAfterTest();
68         set_config('enableglobalsearch', true);
70         $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
72         $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
73         $this->assertInstanceOf('\core_search\base', $searcharea);
75         $this->assertFalse(\core_search\manager::get_search_area($fakeareaid));
77         $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list());
78         $this->assertArrayNotHasKey($fakeareaid, \core_search\manager::get_search_areas_list());
80         // Enabled by default once global search is enabled.
81         $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
83         list($componentname, $varname) = $searcharea->get_config_var_name();
84         set_config($varname . '_enabled', 0, $componentname);
85         \core_search\manager::clear_static();
87         $this->assertArrayNotHasKey('mod_forum', \core_search\manager::get_search_areas_list(true));
89         set_config($varname . '_enabled', 1, $componentname);
91         // Although the result is wrong, we want to check that \core_search\manager::get_search_areas_list returns cached results.
92         $this->assertArrayNotHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
94         // Now we check the real result.
95         \core_search\manager::clear_static();
96         $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
97     }
99     public function test_search_config() {
101         $this->resetAfterTest();
103         $search = testable_core_search::instance();
105         // We should test both plugin types and core subsystems. No core subsystems available yet.
106         $searcharea = $search->get_search_area($this->forumpostareaid);
108         list($componentname, $varname) = $searcharea->get_config_var_name();
110         // Just with a couple of vars should be enough.
111         $start = time() - 100;
112         $end = time();
113         set_config($varname . '_indexingstart', $start, $componentname);
114         set_config($varname . '_indexingend', $end, $componentname);
116         $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
117         $this->assertEquals($start, $configs[$this->forumpostareaid]->indexingstart);
118         $this->assertEquals($end, $configs[$this->forumpostareaid]->indexingend);
119         $this->assertEquals(false, $configs[$this->forumpostareaid]->partial);
121         try {
122             $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
123             $search->reset_config($fakeareaid);
124             $this->fail('An exception should be triggered if the provided search area does not exist.');
125         } catch (moodle_exception $ex) {
126             $this->assertContains($fakeareaid . ' search area is not available.', $ex->getMessage());
127         }
129         // We clean it all but enabled components.
130         $search->reset_config($this->forumpostareaid);
131         $config = $searcharea->get_config();
132         $this->assertEquals(1, $config[$varname . '_enabled']);
133         $this->assertEquals(0, $config[$varname . '_indexingstart']);
134         $this->assertEquals(0, $config[$varname . '_indexingend']);
135         $this->assertEquals(0, $config[$varname . '_lastindexrun']);
136         $this->assertEquals(0, $config[$varname . '_partial']);
137         // No caching.
138         $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
139         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
140         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
142         set_config($varname . '_indexingstart', $start, $componentname);
143         set_config($varname . '_indexingend', $end, $componentname);
145         // All components config should be reset.
146         $search->reset_config();
147         $this->assertEquals(0, get_config($componentname, $varname . '_indexingstart'));
148         $this->assertEquals(0, get_config($componentname, $varname . '_indexingend'));
149         $this->assertEquals(0, get_config($componentname, $varname . '_lastindexrun'));
150         // No caching.
151         $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
152         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
153         $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
154     }
156     /**
157      * Tests the get_last_indexing_duration method in the base area class.
158      */
159     public function test_get_last_indexing_duration() {
160         $this->resetAfterTest();
162         $search = testable_core_search::instance();
164         $searcharea = $search->get_search_area($this->forumpostareaid);
166         // When never indexed, the duration is false.
167         $this->assertSame(false, $searcharea->get_last_indexing_duration());
169         // Set the start/end times.
170         list($componentname, $varname) = $searcharea->get_config_var_name();
171         $start = time() - 100;
172         $end = time();
173         set_config($varname . '_indexingstart', $start, $componentname);
174         set_config($varname . '_indexingend', $end, $componentname);
176         // The duration should now be 100.
177         $this->assertSame(100, $searcharea->get_last_indexing_duration());
178     }
180     /**
181      * Tests that partial indexing works correctly.
182      */
183     public function test_partial_indexing() {
184         global $USER;
186         $this->resetAfterTest();
187         $this->setAdminUser();
189         // Create a course and a forum.
190         $generator = $this->getDataGenerator();
191         $course = $generator->create_course();
192         $forum = $generator->create_module('forum', ['course' => $course->id]);
194         // Index everything up to current. Ensure the course is older than current second so it
195         // definitely doesn't get indexed again next time.
196         $this->waitForSecond();
197         $search = testable_core_search::instance();
198         $search->index(false, 0);
200         $searcharea = $search->get_search_area($this->forumpostareaid);
201         list($componentname, $varname) = $searcharea->get_config_var_name();
202         $this->assertFalse(get_config($componentname, $varname . '_partial'));
204         // Add 3 discussions to the forum.
205         $now = time();
206         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
207                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now,
208                 'name' => 'Frog']);
209         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
210                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
211                 'name' => 'Toad']);
212         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
213                 'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
214                 'name' => 'Zombie']);
215         time_sleep_until($now + 3);
217         // Clear the count of added documents.
218         $search->get_engine()->get_and_clear_added_documents();
220         // Make the search engine delay while indexing each document.
221         $search->get_engine()->set_add_delay(1.2);
223         // Index with a limit of 2 seconds - it should index 2 of the documents (after the second
224         // one, it will have taken 2.4 seconds so it will stop).
225         $search->index(false, 2);
226         $added = $search->get_engine()->get_and_clear_added_documents();
227         $this->assertCount(2, $added);
228         $this->assertEquals('Frog', $added[0]->get('title'));
229         $this->assertEquals('Toad', $added[1]->get('title'));
230         $this->assertEquals(1, get_config($componentname, $varname . '_partial'));
232         // Add a label.
233         $generator->create_module('label', ['course' => $course->id, 'intro' => 'Vampire']);
235         // Wait to next second (so as to not reindex the label more than once, as it will now
236         // be timed before the indexing run).
237         $this->waitForSecond();
239         // Next index with 1 second limit should do the label and not the forum - the logic is,
240         // if it spent ages indexing an area last time, do that one last on next run.
241         $search->index(false, 1);
242         $added = $search->get_engine()->get_and_clear_added_documents();
243         $this->assertCount(1, $added);
244         $this->assertEquals('Vampire', $added[0]->get('title'));
246         // Index again with a 2 second limit - it will redo last post for safety (because of other
247         // things possibly having the same time second), and then do the remaining one. (Note:
248         // because it always does more than one second worth of items, it would actually index 2
249         // posts even if the limit were less than 2.)
250         $search->index(false, 2);
251         $added = $search->get_engine()->get_and_clear_added_documents();
252         $this->assertCount(2, $added);
253         $this->assertEquals('Toad', $added[0]->get('title'));
254         $this->assertEquals('Zombie', $added[1]->get('title'));
255         $this->assertFalse(get_config($componentname, $varname . '_partial'));
257         // Index again - there should be nothing to index this time.
258         $search->index(false, 2);
259         $added = $search->get_engine()->get_and_clear_added_documents();
260         $this->assertCount(0, $added);
261         $this->assertFalse(get_config($componentname, $varname . '_partial'));
262     }
264     /**
265      * Tests that indexing a specified context works correctly.
266      */
267     public function test_context_indexing() {
268         global $USER;
270         $this->resetAfterTest();
271         $this->setAdminUser();
273         // Create a course and two forums and a page.
274         $generator = $this->getDataGenerator();
275         $course = $generator->create_course();
276         $now = time();
277         $forum1 = $generator->create_module('forum', ['course' => $course->id]);
278         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
279                 'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now,
280                 'name' => 'Frog']);
281         $this->waitForSecond();
282         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
283                 'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
284                 'name' => 'Zombie']);
285         $forum2 = $generator->create_module('forum', ['course' => $course->id]);
286         $this->waitForSecond();
287         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
288                 'forum' => $forum2->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
289                 'name' => 'Toad']);
290         $generator->create_module('page', ['course' => $course->id]);
291         $generator->create_module('forum', ['course' => $course->id]);
293         // Index forum 1 only.
294         $search = testable_core_search::instance();
295         $buffer = new progress_trace_buffer(new text_progress_trace(), false);
296         $result = $search->index_context(\context_module::instance($forum1->cmid), '', 0, $buffer);
297         $this->assertTrue($result->complete);
298         $log = $buffer->get_buffer();
299         $buffer->reset_buffer();
301         // Confirm that output only processed 1 forum activity and 2 posts.
302         var_dump(strpos($log, "area: Forum - activity information\n  Processed 1 "));
303         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 1 "));
304         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
306         // Confirm that some areas for different types of context were skipped.
307         $this->assertNotFalse(strpos($log, "area: Users\n  Skipping"));
308         $this->assertNotFalse(strpos($log, "area: My courses\n  Skipping"));
310         // Confirm that another module area had no results.
311         $this->assertNotFalse(strpos($log, "area: Page\n  No documents"));
313         // Index whole course.
314         $result = $search->index_context(\context_course::instance($course->id), '', 0, $buffer);
315         $this->assertTrue($result->complete);
316         $log = $buffer->get_buffer();
317         $buffer->reset_buffer();
319         // Confirm that output processed 3 forum activities and 3 posts.
320         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 3 "));
321         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
323         // The course area was also included this time.
324         $this->assertNotFalse(strpos($log, "area: My courses\n  Processed 1 "));
326         // Confirm that another module area had results too.
327         $this->assertNotFalse(strpos($log, "area: Page\n  Processed 1 "));
329         // Index whole course, but only forum posts.
330         $result = $search->index_context(\context_course::instance($course->id), 'mod_forum-post',
331                 0, $buffer);
332         $this->assertTrue($result->complete);
333         $log = $buffer->get_buffer();
334         $buffer->reset_buffer();
336         // Confirm that output processed 3 posts but not forum activities.
337         $this->assertFalse(strpos($log, "area: Forum - activity information"));
338         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
340         // Set time limit and retry index of whole course, taking 3 tries to complete it.
341         $search->get_engine()->set_add_delay(0.4);
342         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer);
343         $log = $buffer->get_buffer();
344         $buffer->reset_buffer();
345         $this->assertFalse($result->complete);
346         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
348         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
349                 $result->startfromarea, $result->startfromtime);
350         $log = $buffer->get_buffer();
351         $buffer->reset_buffer();
352         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
353         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
354         $this->assertFalse($result->complete);
356         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
357                 $result->startfromarea, $result->startfromtime);
358         $log = $buffer->get_buffer();
359         $buffer->reset_buffer();
360         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
361         $this->assertTrue($result->complete);
362     }
364     /**
365      * Adding this test here as get_areas_user_accesses process is the same, results just depend on the context level.
366      *
367      * @return void
368      */
369     public function test_search_user_accesses() {
370         global $DB;
372         $this->resetAfterTest();
374         $frontpage = $DB->get_record('course', array('id' => SITEID));
375         $course1 = $this->getDataGenerator()->create_course();
376         $course1ctx = context_course::instance($course1->id);
377         $course2 = $this->getDataGenerator()->create_course();
378         $course2ctx = context_course::instance($course2->id);
379         $teacher = $this->getDataGenerator()->create_user();
380         $teacherctx = context_user::instance($teacher->id);
381         $student = $this->getDataGenerator()->create_user();
382         $studentctx = context_user::instance($student->id);
383         $noaccess = $this->getDataGenerator()->create_user();
384         $noaccessctx = context_user::instance($noaccess->id);
385         $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, 'teacher');
386         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
388         $frontpageforum = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
389         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
390         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
391         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
392         $frontpageforumcontext = context_module::instance($frontpageforum->cmid);
393         $context1 = context_module::instance($forum1->cmid);
394         $context2 = context_module::instance($forum2->cmid);
395         $context3 = context_module::instance($forum3->cmid);
397         $search = testable_core_search::instance();
398         $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
399         $search->add_core_search_areas();
400         $search->add_search_area($mockareaid, new core_mocksearch\search\mock_search_area());
402         $this->setAdminUser();
403         $this->assertTrue($search->get_areas_user_accesses());
405         $sitectx = \context_course::instance(SITEID);
406         $systemctxid = \context_system::instance()->id;
408         // Can access the frontpage ones.
409         $this->setUser($noaccess);
410         $contexts = $search->get_areas_user_accesses();
411         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id), $contexts[$this->forumpostareaid]);
412         $this->assertEquals(array($sitectx->id => $sitectx->id), $contexts[$this->mycoursesareaid]);
413         $mockctxs = array($noaccessctx->id => $noaccessctx->id, $systemctxid => $systemctxid);
414         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
416         $this->setUser($teacher);
417         $contexts = $search->get_areas_user_accesses();
418         $frontpageandcourse1 = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
419             $context2->id => $context2->id);
420         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
421         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
422             $contexts[$this->mycoursesareaid]);
423         $mockctxs = array($teacherctx->id => $teacherctx->id, $systemctxid => $systemctxid);
424         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
426         $this->setUser($student);
427         $contexts = $search->get_areas_user_accesses();
428         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
429         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
430             $contexts[$this->mycoursesareaid]);
431         $mockctxs = array($studentctx->id => $studentctx->id, $systemctxid => $systemctxid);
432         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
434         // Hide the activity.
435         set_coursemodule_visible($forum2->cmid, 0);
436         $contexts = $search->get_areas_user_accesses();
437         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id),
438             $contexts[$this->forumpostareaid]);
440         // Now test course limited searches.
441         set_coursemodule_visible($forum2->cmid, 1);
442         $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student');
443         $contexts = $search->get_areas_user_accesses();
444         $allcontexts = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
445             $context2->id => $context2->id, $context3->id => $context3->id);
446         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
447         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id,
448             $course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
450         $contexts = $search->get_areas_user_accesses(array($course1->id, $course2->id));
451         $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id, $context3->id => $context3->id);
452         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
453         $this->assertEquals(array($course1ctx->id => $course1ctx->id,
454             $course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
456         $contexts = $search->get_areas_user_accesses(array($course2->id));
457         $allcontexts = array($context3->id => $context3->id);
458         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
459         $this->assertEquals(array($course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
461         $contexts = $search->get_areas_user_accesses(array($course1->id));
462         $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id);
463         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
464         $this->assertEquals(array($course1ctx->id => $course1ctx->id), $contexts[$this->mycoursesareaid]);
465     }
467     /**
468      * Tests the block support in get_search_user_accesses.
469      *
470      * @return void
471      */
472     public function test_search_user_accesses_blocks() {
473         $this->resetAfterTest();
474         $this->setAdminUser();
476         // Create course and add HTML block.
477         $generator = $this->getDataGenerator();
478         $course1 = $generator->create_course();
479         $context1 = \context_course::instance($course1->id);
480         $page = new \moodle_page();
481         $page->set_context($context1);
482         $page->set_course($course1);
483         $page->set_pagelayout('standard');
484         $page->set_pagetype('course-view');
485         $page->blocks->load_blocks();
486         $page->blocks->add_block_at_end_of_default_region('html');
488         // Create another course with HTML blocks only in some weird page or a module page (not
489         // yet supported, so both these blocks will be ignored).
490         $course2 = $generator->create_course();
491         $context2 = \context_course::instance($course2->id);
492         $page = new \moodle_page();
493         $page->set_context($context2);
494         $page->set_course($course2);
495         $page->set_pagelayout('standard');
496         $page->set_pagetype('bogus-page');
497         $page->blocks->load_blocks();
498         $page->blocks->add_block_at_end_of_default_region('html');
500         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
501         $forumcontext = context_module::instance($forum->cmid);
502         $page = new \moodle_page();
503         $page->set_context($forumcontext);
504         $page->set_course($course2);
505         $page->set_pagelayout('standard');
506         $page->set_pagetype('mod-forum-view');
507         $page->blocks->load_blocks();
508         $page->blocks->add_block_at_end_of_default_region('html');
510         // The third course has 2 HTML blocks.
511         $course3 = $generator->create_course();
512         $context3 = \context_course::instance($course3->id);
513         $page = new \moodle_page();
514         $page->set_context($context3);
515         $page->set_course($course3);
516         $page->set_pagelayout('standard');
517         $page->set_pagetype('course-view');
518         $page->blocks->load_blocks();
519         $page->blocks->add_block_at_end_of_default_region('html');
520         $page->blocks->add_block_at_end_of_default_region('html');
522         // Student 1 belongs to all 3 courses.
523         $student1 = $generator->create_user();
524         $generator->enrol_user($student1->id, $course1->id, 'student');
525         $generator->enrol_user($student1->id, $course2->id, 'student');
526         $generator->enrol_user($student1->id, $course3->id, 'student');
528         // Student 2 belongs only to course 2.
529         $student2 = $generator->create_user();
530         $generator->enrol_user($student2->id, $course2->id, 'student');
532         // And the third student is only in course 3.
533         $student3 = $generator->create_user();
534         $generator->enrol_user($student3->id, $course3->id, 'student');
536         $search = testable_core_search::instance();
537         $search->add_core_search_areas();
539         // Admin gets 'true' result to function regardless of blocks.
540         $this->setAdminUser();
541         $this->assertTrue($search->get_areas_user_accesses());
543         // Student 1 gets all 3 block contexts.
544         $this->setUser($student1);
545         $contexts = $search->get_areas_user_accesses();
546         $this->assertArrayHasKey('block_html-content', $contexts);
547         $this->assertCount(3, $contexts['block_html-content']);
549         // Student 2 does not get any blocks.
550         $this->setUser($student2);
551         $contexts = $search->get_areas_user_accesses();
552         $this->assertArrayNotHasKey('block_html-content', $contexts);
554         // Student 3 gets only two of them.
555         $this->setUser($student3);
556         $contexts = $search->get_areas_user_accesses();
557         $this->assertArrayHasKey('block_html-content', $contexts);
558         $this->assertCount(2, $contexts['block_html-content']);
560         // A course limited search for student 1 is the same as the student 3 search.
561         $this->setUser($student1);
562         $limitedcontexts = $search->get_areas_user_accesses([$course3->id]);
563         $this->assertEquals($contexts['block_html-content'], $limitedcontexts['block_html-content']);
564     }
566     /**
567      * Test get_areas_user_accesses with regard to the 'all available courses' config option.
568      *
569      * @return void
570      */
571     public function test_search_user_accesses_allavailable() {
572         global $DB, $CFG;
574         $this->resetAfterTest();
576         // Front page, including a forum.
577         $frontpage = $DB->get_record('course', array('id' => SITEID));
578         $forumfront = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
579         $forumfrontctx = context_module::instance($forumfront->cmid);
581         // Course 1 does not allow guest access.
582         $course1 = $this->getDataGenerator()->create_course((object)array(
583                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
584                 'enrol_guest_password_0' => ''));
585         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
586         $forum1ctx = context_module::instance($forum1->cmid);
588         // Course 2 does not allow guest but is accessible by all users.
589         $course2 = $this->getDataGenerator()->create_course((object)array(
590                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
591                 'enrol_guest_password_0' => ''));
592         $course2ctx = context_course::instance($course2->id);
593         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
594         $forum2ctx = context_module::instance($forum2->cmid);
595         assign_capability('moodle/course:view', CAP_ALLOW, $CFG->defaultuserroleid, $course2ctx->id);
597         // Course 3 allows guest access without password.
598         $course3 = $this->getDataGenerator()->create_course((object)array(
599                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
600                 'enrol_guest_password_0' => ''));
601         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
602         $forum3ctx = context_module::instance($forum3->cmid);
604         // Student user is enrolled in course 1.
605         $student = $this->getDataGenerator()->create_user();
606         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
608         // No access user is just a user with no permissions.
609         $noaccess = $this->getDataGenerator()->create_user();
611         // First test without the all available option.
612         $search = testable_core_search::instance();
614         // Admin user can access everything.
615         $this->setAdminUser();
616         $this->assertTrue($search->get_areas_user_accesses());
618         // No-access user can access only the front page forum.
619         $this->setUser($noaccess);
620         $contexts = $search->get_areas_user_accesses();
621         $this->assertEquals([$forumfrontctx->id], array_keys($contexts[$this->forumpostareaid]));
623         // Student can access the front page forum plus the enrolled one.
624         $this->setUser($student);
625         $contexts = $search->get_areas_user_accesses();
626         $this->assertEquals([$forum1ctx->id, $forumfrontctx->id],
627                 array_keys($contexts[$this->forumpostareaid]));
629         // Now turn on the all available option.
630         set_config('searchallavailablecourses', 1);
632         // Admin user can access everything.
633         $this->setAdminUser();
634         $this->assertTrue($search->get_areas_user_accesses());
636         // No-access user can access the front page forum and course 2, 3.
637         $this->setUser($noaccess);
638         $contexts = $search->get_areas_user_accesses();
639         $this->assertEquals([$forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
640                 array_keys($contexts[$this->forumpostareaid]));
642         // Student can access the front page forum plus the enrolled one plus courses 2, 3.
643         $this->setUser($student);
644         $contexts = $search->get_areas_user_accesses();
645         $this->assertEquals([$forum1ctx->id, $forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
646                 array_keys($contexts[$this->forumpostareaid]));
647     }
649     /**
650      * test_is_search_area
651      *
652      * @return void
653      */
654     public function test_is_search_area() {
656         $this->assertFalse(testable_core_search::is_search_area('\asd\asd'));
657         $this->assertFalse(testable_core_search::is_search_area('\mod_forum\search\posta'));
658         $this->assertFalse(testable_core_search::is_search_area('\core_search\base_mod'));
659         $this->assertTrue(testable_core_search::is_search_area('\mod_forum\search\post'));
660         $this->assertTrue(testable_core_search::is_search_area('\\mod_forum\\search\\post'));
661         $this->assertTrue(testable_core_search::is_search_area('mod_forum\\search\\post'));
662     }
664     /**
665      * Tests the request_index function used for reindexing certain contexts. This only tests
666      * adding things to the request list, it doesn't test that they are actually indexed by the
667      * scheduled task.
668      */
669     public function test_request_index() {
670         global $DB;
672         $this->resetAfterTest();
674         $course1 = $this->getDataGenerator()->create_course();
675         $course1ctx = context_course::instance($course1->id);
676         $course2 = $this->getDataGenerator()->create_course();
677         $course2ctx = context_course::instance($course2->id);
678         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
679         $forum1ctx = context_module::instance($forum1->cmid);
680         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
681         $forum2ctx = context_module::instance($forum2->cmid);
683         // Initially no requests.
684         $this->assertEquals(0, $DB->count_records('search_index_requests'));
686         // Request update for course 1, all areas.
687         \core_search\manager::request_index($course1ctx);
689         // Check all details of entry.
690         $results = array_values($DB->get_records('search_index_requests'));
691         $this->assertCount(1, $results);
692         $this->assertEquals($course1ctx->id, $results[0]->contextid);
693         $this->assertEquals('', $results[0]->searcharea);
694         $now = time();
695         $this->assertLessThanOrEqual($now, $results[0]->timerequested);
696         $this->assertGreaterThan($now - 10, $results[0]->timerequested);
697         $this->assertEquals('', $results[0]->partialarea);
698         $this->assertEquals(0, $results[0]->partialtime);
700         // Request forum 1, all areas; not added as covered by course 1.
701         \core_search\manager::request_index($forum1ctx);
702         $this->assertEquals(1, $DB->count_records('search_index_requests'));
704         // Request forum 1, specific area; not added as covered by course 1 all areas.
705         \core_search\manager::request_index($forum1ctx, 'forum-post');
706         $this->assertEquals(1, $DB->count_records('search_index_requests'));
708         // Request course 1 again, specific area; not added as covered by all areas.
709         \core_search\manager::request_index($course1ctx, 'forum-post');
710         $this->assertEquals(1, $DB->count_records('search_index_requests'));
712         // Request course 1 again, all areas; not needed as covered already.
713         \core_search\manager::request_index($course1ctx);
714         $this->assertEquals(1, $DB->count_records('search_index_requests'));
716         // Request course 2, specific area.
717         \core_search\manager::request_index($course2ctx, 'label-activity');
718         // Note: I'm ordering by ID for convenience - this is dangerous in real code (see MDL-43447)
719         // but in a unit test it shouldn't matter as nobody is using clustered databases for unit
720         // test.
721         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
722         $this->assertCount(2, $results);
723         $this->assertEquals($course1ctx->id, $results[0]->contextid);
724         $this->assertEquals($course2ctx->id, $results[1]->contextid);
725         $this->assertEquals('label-activity', $results[1]->searcharea);
727         // Request forum 2, same specific area; not added.
728         \core_search\manager::request_index($forum2ctx, 'label-activity');
729         $this->assertEquals(2, $DB->count_records('search_index_requests'));
731         // Request forum 2, different specific area; added.
732         \core_search\manager::request_index($forum2ctx, 'forum-post');
733         $this->assertEquals(3, $DB->count_records('search_index_requests'));
735         // Request forum 2, all areas; also added. (Note: This could obviously remove the previous
736         // one, but for simplicity, I didn't make it do that; also it could perhaps cause problems
737         // if we had already begun processing the previous entry.)
738         \core_search\manager::request_index($forum2ctx);
739         $this->assertEquals(4, $DB->count_records('search_index_requests'));
740     }
742     /**
743      * Tests the process_index_requests function.
744      */
745     public function test_process_index_requests() {
746         global $DB;
748         $this->resetAfterTest();
750         $search = testable_core_search::instance();
752         // When there are no index requests, nothing gets logged.
753         $progress = new progress_trace_buffer(new text_progress_trace(), false);
754         $search->process_index_requests(0.0, $progress);
755         $out = $progress->get_buffer();
756         $progress->reset_buffer();
757         $this->assertEquals('', $out);
759         // Set up the course with 3 forums.
760         $generator = $this->getDataGenerator();
761         $course = $generator->create_course(['fullname' => 'TCourse']);
762         $forum1 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum1']);
763         $forum2 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum2']);
764         $forum3 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum3']);
766         // Hack the forums so they have different creation times.
767         $now = time();
768         $DB->set_field('forum', 'timemodified', $now - 3, ['id' => $forum1->id]);
769         $DB->set_field('forum', 'timemodified', $now - 2, ['id' => $forum2->id]);
770         $DB->set_field('forum', 'timemodified', $now - 1, ['id' => $forum3->id]);
771         $forum2time = $now - 2;
773         // Make 2 index requests.
774         $search::request_index(context_course::instance($course->id), 'mod_label-activity');
775         $this->waitForSecond();
776         $search::request_index(context_module::instance($forum1->cmid));
778         // Run with no time limit.
779         $search->process_index_requests(0.0, $progress);
780         $out = $progress->get_buffer();
781         $progress->reset_buffer();
783         // Check that it's done both areas.
784         $this->assertContains(
785                 'Indexing requested context: Course: TCourse (search area: mod_label-activity)',
786                 $out);
787         $this->assertContains(
788                 'Completed requested context: Course: TCourse (search area: mod_label-activity)',
789                 $out);
790         $this->assertContains('Indexing requested context: Forum: TForum1', $out);
791         $this->assertContains('Completed requested context: Forum: TForum1', $out);
793         // Check the requests database table is now empty.
794         $this->assertEquals(0, $DB->count_records('search_index_requests'));
796         // Request indexing the course a couple of times.
797         $search::request_index(context_course::instance($course->id), 'mod_forum-activity');
798         $search::request_index(context_course::instance($course->id), 'mod_forum-post');
800         // Do the processing again with a time limit and indexing delay. The time limit is too
801         // small; because of the way the logic works, this means it will index 2 activities.
802         $search->get_engine()->set_add_delay(0.2);
803         $search->process_index_requests(0.1, $progress);
804         $out = $progress->get_buffer();
805         $progress->reset_buffer();
807         // Confirm the right wrapper information was logged.
808         $this->assertContains(
809                 'Indexing requested context: Course: TCourse (search area: mod_forum-activity)',
810                 $out);
811         $this->assertContains('Stopping indexing due to time limit', $out);
812         $this->assertContains(
813                 'Ending requested context: Course: TCourse (search area: mod_forum-activity)',
814                 $out);
816         // Check the database table has been updated with progress.
817         $records = array_values($DB->get_records('search_index_requests', null, 'searcharea'));
818         $this->assertEquals('mod_forum-activity', $records[0]->partialarea);
819         $this->assertEquals($forum2time, $records[0]->partialtime);
821         // Run again and confirm it now finishes.
822         $search->process_index_requests(0.1, $progress);
823         $out = $progress->get_buffer();
824         $progress->reset_buffer();
825         $this->assertContains(
826                 'Completed requested context: Course: TCourse (search area: mod_forum-activity)',
827                 $out);
828         $this->assertContains(
829                 'Completed requested context: Course: TCourse (search area: mod_forum-post)',
830                 $out);
832         // Confirm table is now empty.
833         $this->assertEquals(0, $DB->count_records('search_index_requests'));
834     }