Merge branch 'MDL-55356-master' of https://github.com/sammarshallou/moodle
[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 documents with modified time in the future are NOT indexed (as this would cause
266      * a problem by preventing it from indexing other documents modified between now and the future
267      * date).
268      */
269     public function test_future_documents() {
270         $this->resetAfterTest();
272         // Create a course and a forum.
273         $generator = $this->getDataGenerator();
274         $course = $generator->create_course();
275         $forum = $generator->create_module('forum', ['course' => $course->id]);
277         // Index everything up to current. Ensure the course is older than current second so it
278         // definitely doesn't get indexed again next time.
279         $this->waitForSecond();
280         $search = testable_core_search::instance();
281         $search->index(false, 0);
282         $search->get_engine()->get_and_clear_added_documents();
284         // Add 2 discussions to the forum, one of which happend just now, but the other is
285         // incorrectly set to the future.
286         $now = time();
287         $userid = get_admin()->id;
288         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
289                 'forum' => $forum->id, 'userid' => $userid, 'timemodified' => $now,
290                 'name' => 'Frog']);
291         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
292                 'forum' => $forum->id, 'userid' => $userid, 'timemodified' => $now + 100,
293                 'name' => 'Toad']);
295         // Wait for a second so we're not actually on the same second as the forum post (there's a
296         // 1 second overlap between indexing; it would get indexed in both checks below otherwise).
297         $this->waitForSecond();
299         // Index.
300         $search->index(false);
301         $added = $search->get_engine()->get_and_clear_added_documents();
302         $this->assertCount(1, $added);
303         $this->assertEquals('Frog', $added[0]->get('title'));
305         // Check latest time - it should be the same as $now, not the + 100.
306         $searcharea = $search->get_search_area($this->forumpostareaid);
307         list($componentname, $varname) = $searcharea->get_config_var_name();
308         $this->assertEquals($now, get_config($componentname, $varname . '_lastindexrun'));
310         // Index again - there should be nothing to index this time.
311         $search->index(false);
312         $added = $search->get_engine()->get_and_clear_added_documents();
313         $this->assertCount(0, $added);
314     }
316     /**
317      * Tests that indexing a specified context works correctly.
318      */
319     public function test_context_indexing() {
320         global $USER;
322         $this->resetAfterTest();
323         $this->setAdminUser();
325         // Create a course and two forums and a page.
326         $generator = $this->getDataGenerator();
327         $course = $generator->create_course();
328         $now = time();
329         $forum1 = $generator->create_module('forum', ['course' => $course->id]);
330         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
331                 'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now,
332                 'name' => 'Frog']);
333         $this->waitForSecond();
334         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
335                 'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
336                 'name' => 'Zombie']);
337         $forum2 = $generator->create_module('forum', ['course' => $course->id]);
338         $this->waitForSecond();
339         $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
340                 'forum' => $forum2->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
341                 'name' => 'Toad']);
342         $generator->create_module('page', ['course' => $course->id]);
343         $generator->create_module('forum', ['course' => $course->id]);
345         // Index forum 1 only.
346         $search = testable_core_search::instance();
347         $buffer = new progress_trace_buffer(new text_progress_trace(), false);
348         $result = $search->index_context(\context_module::instance($forum1->cmid), '', 0, $buffer);
349         $this->assertTrue($result->complete);
350         $log = $buffer->get_buffer();
351         $buffer->reset_buffer();
353         // Confirm that output only processed 1 forum activity and 2 posts.
354         var_dump(strpos($log, "area: Forum - activity information\n  Processed 1 "));
355         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 1 "));
356         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
358         // Confirm that some areas for different types of context were skipped.
359         $this->assertNotFalse(strpos($log, "area: Users\n  Skipping"));
360         $this->assertNotFalse(strpos($log, "area: My courses\n  Skipping"));
362         // Confirm that another module area had no results.
363         $this->assertNotFalse(strpos($log, "area: Page\n  No documents"));
365         // Index whole course.
366         $result = $search->index_context(\context_course::instance($course->id), '', 0, $buffer);
367         $this->assertTrue($result->complete);
368         $log = $buffer->get_buffer();
369         $buffer->reset_buffer();
371         // Confirm that output processed 3 forum activities and 3 posts.
372         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 3 "));
373         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
375         // The course area was also included this time.
376         $this->assertNotFalse(strpos($log, "area: My courses\n  Processed 1 "));
378         // Confirm that another module area had results too.
379         $this->assertNotFalse(strpos($log, "area: Page\n  Processed 1 "));
381         // Index whole course, but only forum posts.
382         $result = $search->index_context(\context_course::instance($course->id), 'mod_forum-post',
383                 0, $buffer);
384         $this->assertTrue($result->complete);
385         $log = $buffer->get_buffer();
386         $buffer->reset_buffer();
388         // Confirm that output processed 3 posts but not forum activities.
389         $this->assertFalse(strpos($log, "area: Forum - activity information"));
390         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
392         // Set time limit and retry index of whole course, taking 3 tries to complete it.
393         $search->get_engine()->set_add_delay(0.4);
394         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer);
395         $log = $buffer->get_buffer();
396         $buffer->reset_buffer();
397         $this->assertFalse($result->complete);
398         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
400         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
401                 $result->startfromarea, $result->startfromtime);
402         $log = $buffer->get_buffer();
403         $buffer->reset_buffer();
404         $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
405         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
406         $this->assertFalse($result->complete);
408         $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
409                 $result->startfromarea, $result->startfromtime);
410         $log = $buffer->get_buffer();
411         $buffer->reset_buffer();
412         $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
413         $this->assertTrue($result->complete);
415     /**
416      * Adding this test here as get_areas_user_accesses process is the same, results just depend on the context level.
417      *
418      * @return void
419      */
420     public function test_search_user_accesses() {
421         global $DB;
423         $this->resetAfterTest();
425         $frontpage = $DB->get_record('course', array('id' => SITEID));
426         $course1 = $this->getDataGenerator()->create_course();
427         $course1ctx = context_course::instance($course1->id);
428         $course2 = $this->getDataGenerator()->create_course();
429         $course2ctx = context_course::instance($course2->id);
430         $teacher = $this->getDataGenerator()->create_user();
431         $teacherctx = context_user::instance($teacher->id);
432         $student = $this->getDataGenerator()->create_user();
433         $studentctx = context_user::instance($student->id);
434         $noaccess = $this->getDataGenerator()->create_user();
435         $noaccessctx = context_user::instance($noaccess->id);
436         $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, 'teacher');
437         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
439         $frontpageforum = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
440         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
441         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
442         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
443         $frontpageforumcontext = context_module::instance($frontpageforum->cmid);
444         $context1 = context_module::instance($forum1->cmid);
445         $context2 = context_module::instance($forum2->cmid);
446         $context3 = context_module::instance($forum3->cmid);
448         $search = testable_core_search::instance();
449         $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
450         $search->add_core_search_areas();
451         $search->add_search_area($mockareaid, new core_mocksearch\search\mock_search_area());
453         $this->setAdminUser();
454         $this->assertTrue($search->get_areas_user_accesses());
456         $sitectx = \context_course::instance(SITEID);
457         $systemctxid = \context_system::instance()->id;
459         // Can access the frontpage ones.
460         $this->setUser($noaccess);
461         $contexts = $search->get_areas_user_accesses();
462         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id), $contexts[$this->forumpostareaid]);
463         $this->assertEquals(array($sitectx->id => $sitectx->id), $contexts[$this->mycoursesareaid]);
464         $mockctxs = array($noaccessctx->id => $noaccessctx->id, $systemctxid => $systemctxid);
465         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
467         $this->setUser($teacher);
468         $contexts = $search->get_areas_user_accesses();
469         $frontpageandcourse1 = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
470             $context2->id => $context2->id);
471         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
472         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
473             $contexts[$this->mycoursesareaid]);
474         $mockctxs = array($teacherctx->id => $teacherctx->id, $systemctxid => $systemctxid);
475         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
477         $this->setUser($student);
478         $contexts = $search->get_areas_user_accesses();
479         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
480         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
481             $contexts[$this->mycoursesareaid]);
482         $mockctxs = array($studentctx->id => $studentctx->id, $systemctxid => $systemctxid);
483         $this->assertEquals($mockctxs, $contexts[$mockareaid]);
485         // Hide the activity.
486         set_coursemodule_visible($forum2->cmid, 0);
487         $contexts = $search->get_areas_user_accesses();
488         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id),
489             $contexts[$this->forumpostareaid]);
491         // Now test course limited searches.
492         set_coursemodule_visible($forum2->cmid, 1);
493         $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student');
494         $contexts = $search->get_areas_user_accesses();
495         $allcontexts = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
496             $context2->id => $context2->id, $context3->id => $context3->id);
497         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
498         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id,
499             $course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
501         $contexts = $search->get_areas_user_accesses(array($course1->id, $course2->id));
502         $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id, $context3->id => $context3->id);
503         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
504         $this->assertEquals(array($course1ctx->id => $course1ctx->id,
505             $course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
507         $contexts = $search->get_areas_user_accesses(array($course2->id));
508         $allcontexts = array($context3->id => $context3->id);
509         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
510         $this->assertEquals(array($course2ctx->id => $course2ctx->id), $contexts[$this->mycoursesareaid]);
512         $contexts = $search->get_areas_user_accesses(array($course1->id));
513         $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id);
514         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
515         $this->assertEquals(array($course1ctx->id => $course1ctx->id), $contexts[$this->mycoursesareaid]);
516     }
518     /**
519      * Tests the block support in get_search_user_accesses.
520      *
521      * @return void
522      */
523     public function test_search_user_accesses_blocks() {
524         $this->resetAfterTest();
525         $this->setAdminUser();
527         // Create course and add HTML block.
528         $generator = $this->getDataGenerator();
529         $course1 = $generator->create_course();
530         $context1 = \context_course::instance($course1->id);
531         $page = new \moodle_page();
532         $page->set_context($context1);
533         $page->set_course($course1);
534         $page->set_pagelayout('standard');
535         $page->set_pagetype('course-view');
536         $page->blocks->load_blocks();
537         $page->blocks->add_block_at_end_of_default_region('html');
539         // Create another course with HTML blocks only in some weird page or a module page (not
540         // yet supported, so both these blocks will be ignored).
541         $course2 = $generator->create_course();
542         $context2 = \context_course::instance($course2->id);
543         $page = new \moodle_page();
544         $page->set_context($context2);
545         $page->set_course($course2);
546         $page->set_pagelayout('standard');
547         $page->set_pagetype('bogus-page');
548         $page->blocks->load_blocks();
549         $page->blocks->add_block_at_end_of_default_region('html');
551         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
552         $forumcontext = context_module::instance($forum->cmid);
553         $page = new \moodle_page();
554         $page->set_context($forumcontext);
555         $page->set_course($course2);
556         $page->set_pagelayout('standard');
557         $page->set_pagetype('mod-forum-view');
558         $page->blocks->load_blocks();
559         $page->blocks->add_block_at_end_of_default_region('html');
561         // The third course has 2 HTML blocks.
562         $course3 = $generator->create_course();
563         $context3 = \context_course::instance($course3->id);
564         $page = new \moodle_page();
565         $page->set_context($context3);
566         $page->set_course($course3);
567         $page->set_pagelayout('standard');
568         $page->set_pagetype('course-view');
569         $page->blocks->load_blocks();
570         $page->blocks->add_block_at_end_of_default_region('html');
571         $page->blocks->add_block_at_end_of_default_region('html');
573         // Student 1 belongs to all 3 courses.
574         $student1 = $generator->create_user();
575         $generator->enrol_user($student1->id, $course1->id, 'student');
576         $generator->enrol_user($student1->id, $course2->id, 'student');
577         $generator->enrol_user($student1->id, $course3->id, 'student');
579         // Student 2 belongs only to course 2.
580         $student2 = $generator->create_user();
581         $generator->enrol_user($student2->id, $course2->id, 'student');
583         // And the third student is only in course 3.
584         $student3 = $generator->create_user();
585         $generator->enrol_user($student3->id, $course3->id, 'student');
587         $search = testable_core_search::instance();
588         $search->add_core_search_areas();
590         // Admin gets 'true' result to function regardless of blocks.
591         $this->setAdminUser();
592         $this->assertTrue($search->get_areas_user_accesses());
594         // Student 1 gets all 3 block contexts.
595         $this->setUser($student1);
596         $contexts = $search->get_areas_user_accesses();
597         $this->assertArrayHasKey('block_html-content', $contexts);
598         $this->assertCount(3, $contexts['block_html-content']);
600         // Student 2 does not get any blocks.
601         $this->setUser($student2);
602         $contexts = $search->get_areas_user_accesses();
603         $this->assertArrayNotHasKey('block_html-content', $contexts);
605         // Student 3 gets only two of them.
606         $this->setUser($student3);
607         $contexts = $search->get_areas_user_accesses();
608         $this->assertArrayHasKey('block_html-content', $contexts);
609         $this->assertCount(2, $contexts['block_html-content']);
611         // A course limited search for student 1 is the same as the student 3 search.
612         $this->setUser($student1);
613         $limitedcontexts = $search->get_areas_user_accesses([$course3->id]);
614         $this->assertEquals($contexts['block_html-content'], $limitedcontexts['block_html-content']);
615     }
617     /**
618      * Test get_areas_user_accesses with regard to the 'all available courses' config option.
619      *
620      * @return void
621      */
622     public function test_search_user_accesses_allavailable() {
623         global $DB, $CFG;
625         $this->resetAfterTest();
627         // Front page, including a forum.
628         $frontpage = $DB->get_record('course', array('id' => SITEID));
629         $forumfront = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
630         $forumfrontctx = context_module::instance($forumfront->cmid);
632         // Course 1 does not allow guest access.
633         $course1 = $this->getDataGenerator()->create_course((object)array(
634                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
635                 'enrol_guest_password_0' => ''));
636         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
637         $forum1ctx = context_module::instance($forum1->cmid);
639         // Course 2 does not allow guest but is accessible by all users.
640         $course2 = $this->getDataGenerator()->create_course((object)array(
641                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
642                 'enrol_guest_password_0' => ''));
643         $course2ctx = context_course::instance($course2->id);
644         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
645         $forum2ctx = context_module::instance($forum2->cmid);
646         assign_capability('moodle/course:view', CAP_ALLOW, $CFG->defaultuserroleid, $course2ctx->id);
648         // Course 3 allows guest access without password.
649         $course3 = $this->getDataGenerator()->create_course((object)array(
650                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
651                 'enrol_guest_password_0' => ''));
652         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
653         $forum3ctx = context_module::instance($forum3->cmid);
655         // Student user is enrolled in course 1.
656         $student = $this->getDataGenerator()->create_user();
657         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
659         // No access user is just a user with no permissions.
660         $noaccess = $this->getDataGenerator()->create_user();
662         // First test without the all available option.
663         $search = testable_core_search::instance();
665         // Admin user can access everything.
666         $this->setAdminUser();
667         $this->assertTrue($search->get_areas_user_accesses());
669         // No-access user can access only the front page forum.
670         $this->setUser($noaccess);
671         $contexts = $search->get_areas_user_accesses();
672         $this->assertEquals([$forumfrontctx->id], array_keys($contexts[$this->forumpostareaid]));
674         // Student can access the front page forum plus the enrolled one.
675         $this->setUser($student);
676         $contexts = $search->get_areas_user_accesses();
677         $this->assertEquals([$forum1ctx->id, $forumfrontctx->id],
678                 array_keys($contexts[$this->forumpostareaid]));
680         // Now turn on the all available option.
681         set_config('searchallavailablecourses', 1);
683         // Admin user can access everything.
684         $this->setAdminUser();
685         $this->assertTrue($search->get_areas_user_accesses());
687         // No-access user can access the front page forum and course 2, 3.
688         $this->setUser($noaccess);
689         $contexts = $search->get_areas_user_accesses();
690         $this->assertEquals([$forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
691                 array_keys($contexts[$this->forumpostareaid]));
693         // Student can access the front page forum plus the enrolled one plus courses 2, 3.
694         $this->setUser($student);
695         $contexts = $search->get_areas_user_accesses();
696         $this->assertEquals([$forum1ctx->id, $forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
697                 array_keys($contexts[$this->forumpostareaid]));
698     }
700     /**
701      * test_is_search_area
702      *
703      * @return void
704      */
705     public function test_is_search_area() {
707         $this->assertFalse(testable_core_search::is_search_area('\asd\asd'));
708         $this->assertFalse(testable_core_search::is_search_area('\mod_forum\search\posta'));
709         $this->assertFalse(testable_core_search::is_search_area('\core_search\base_mod'));
710         $this->assertTrue(testable_core_search::is_search_area('\mod_forum\search\post'));
711         $this->assertTrue(testable_core_search::is_search_area('\\mod_forum\\search\\post'));
712         $this->assertTrue(testable_core_search::is_search_area('mod_forum\\search\\post'));
713     }
715     /**
716      * Tests the request_index function used for reindexing certain contexts. This only tests
717      * adding things to the request list, it doesn't test that they are actually indexed by the
718      * scheduled task.
719      */
720     public function test_request_index() {
721         global $DB;
723         $this->resetAfterTest();
725         $course1 = $this->getDataGenerator()->create_course();
726         $course1ctx = context_course::instance($course1->id);
727         $course2 = $this->getDataGenerator()->create_course();
728         $course2ctx = context_course::instance($course2->id);
729         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
730         $forum1ctx = context_module::instance($forum1->cmid);
731         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
732         $forum2ctx = context_module::instance($forum2->cmid);
734         // Initially no requests.
735         $this->assertEquals(0, $DB->count_records('search_index_requests'));
737         // Request update for course 1, all areas.
738         \core_search\manager::request_index($course1ctx);
740         // Check all details of entry.
741         $results = array_values($DB->get_records('search_index_requests'));
742         $this->assertCount(1, $results);
743         $this->assertEquals($course1ctx->id, $results[0]->contextid);
744         $this->assertEquals('', $results[0]->searcharea);
745         $now = time();
746         $this->assertLessThanOrEqual($now, $results[0]->timerequested);
747         $this->assertGreaterThan($now - 10, $results[0]->timerequested);
748         $this->assertEquals('', $results[0]->partialarea);
749         $this->assertEquals(0, $results[0]->partialtime);
751         // Request forum 1, all areas; not added as covered by course 1.
752         \core_search\manager::request_index($forum1ctx);
753         $this->assertEquals(1, $DB->count_records('search_index_requests'));
755         // Request forum 1, specific area; not added as covered by course 1 all areas.
756         \core_search\manager::request_index($forum1ctx, 'forum-post');
757         $this->assertEquals(1, $DB->count_records('search_index_requests'));
759         // Request course 1 again, specific area; not added as covered by all areas.
760         \core_search\manager::request_index($course1ctx, 'forum-post');
761         $this->assertEquals(1, $DB->count_records('search_index_requests'));
763         // Request course 1 again, all areas; not needed as covered already.
764         \core_search\manager::request_index($course1ctx);
765         $this->assertEquals(1, $DB->count_records('search_index_requests'));
767         // Request course 2, specific area.
768         \core_search\manager::request_index($course2ctx, 'label-activity');
769         // Note: I'm ordering by ID for convenience - this is dangerous in real code (see MDL-43447)
770         // but in a unit test it shouldn't matter as nobody is using clustered databases for unit
771         // test.
772         $results = array_values($DB->get_records('search_index_requests', null, 'id'));
773         $this->assertCount(2, $results);
774         $this->assertEquals($course1ctx->id, $results[0]->contextid);
775         $this->assertEquals($course2ctx->id, $results[1]->contextid);
776         $this->assertEquals('label-activity', $results[1]->searcharea);
778         // Request forum 2, same specific area; not added.
779         \core_search\manager::request_index($forum2ctx, 'label-activity');
780         $this->assertEquals(2, $DB->count_records('search_index_requests'));
782         // Request forum 2, different specific area; added.
783         \core_search\manager::request_index($forum2ctx, 'forum-post');
784         $this->assertEquals(3, $DB->count_records('search_index_requests'));
786         // Request forum 2, all areas; also added. (Note: This could obviously remove the previous
787         // one, but for simplicity, I didn't make it do that; also it could perhaps cause problems
788         // if we had already begun processing the previous entry.)
789         \core_search\manager::request_index($forum2ctx);
790         $this->assertEquals(4, $DB->count_records('search_index_requests'));
791     }
793     /**
794      * Tests the process_index_requests function.
795      */
796     public function test_process_index_requests() {
797         global $DB;
799         $this->resetAfterTest();
801         $search = testable_core_search::instance();
803         // When there are no index requests, nothing gets logged.
804         $progress = new progress_trace_buffer(new text_progress_trace(), false);
805         $search->process_index_requests(0.0, $progress);
806         $out = $progress->get_buffer();
807         $progress->reset_buffer();
808         $this->assertEquals('', $out);
810         // Set up the course with 3 forums.
811         $generator = $this->getDataGenerator();
812         $course = $generator->create_course(['fullname' => 'TCourse']);
813         $forum1 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum1']);
814         $forum2 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum2']);
815         $forum3 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum3']);
817         // Hack the forums so they have different creation times.
818         $now = time();
819         $DB->set_field('forum', 'timemodified', $now - 3, ['id' => $forum1->id]);
820         $DB->set_field('forum', 'timemodified', $now - 2, ['id' => $forum2->id]);
821         $DB->set_field('forum', 'timemodified', $now - 1, ['id' => $forum3->id]);
822         $forum2time = $now - 2;
824         // Make 2 index requests.
825         $search::request_index(context_course::instance($course->id), 'mod_label-activity');
826         $this->waitForSecond();
827         $search::request_index(context_module::instance($forum1->cmid));
829         // Run with no time limit.
830         $search->process_index_requests(0.0, $progress);
831         $out = $progress->get_buffer();
832         $progress->reset_buffer();
834         // Check that it's done both areas.
835         $this->assertContains(
836                 'Indexing requested context: Course: TCourse (search area: mod_label-activity)',
837                 $out);
838         $this->assertContains(
839                 'Completed requested context: Course: TCourse (search area: mod_label-activity)',
840                 $out);
841         $this->assertContains('Indexing requested context: Forum: TForum1', $out);
842         $this->assertContains('Completed requested context: Forum: TForum1', $out);
844         // Check the requests database table is now empty.
845         $this->assertEquals(0, $DB->count_records('search_index_requests'));
847         // Request indexing the course a couple of times.
848         $search::request_index(context_course::instance($course->id), 'mod_forum-activity');
849         $search::request_index(context_course::instance($course->id), 'mod_forum-post');
851         // Do the processing again with a time limit and indexing delay. The time limit is too
852         // small; because of the way the logic works, this means it will index 2 activities.
853         $search->get_engine()->set_add_delay(0.2);
854         $search->process_index_requests(0.1, $progress);
855         $out = $progress->get_buffer();
856         $progress->reset_buffer();
858         // Confirm the right wrapper information was logged.
859         $this->assertContains(
860                 'Indexing requested context: Course: TCourse (search area: mod_forum-activity)',
861                 $out);
862         $this->assertContains('Stopping indexing due to time limit', $out);
863         $this->assertContains(
864                 'Ending requested context: Course: TCourse (search area: mod_forum-activity)',
865                 $out);
867         // Check the database table has been updated with progress.
868         $records = array_values($DB->get_records('search_index_requests', null, 'searcharea'));
869         $this->assertEquals('mod_forum-activity', $records[0]->partialarea);
870         $this->assertEquals($forum2time, $records[0]->partialtime);
872         // Run again and confirm it now finishes.
873         $search->process_index_requests(0.1, $progress);
874         $out = $progress->get_buffer();
875         $progress->reset_buffer();
876         $this->assertContains(
877                 'Completed requested context: Course: TCourse (search area: mod_forum-activity)',
878                 $out);
879         $this->assertContains(
880                 'Completed requested context: Course: TCourse (search area: mod_forum-post)',
881                 $out);
883         // Confirm table is now empty.
884         $this->assertEquals(0, $DB->count_records('search_index_requests'));
885     }