MDL-60913 search: add search area categories
[moodle.git] / course / tests / search_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  * Course global search unit tests.
19  *
20  * @package     core
21  * @category    phpunit
22  * @copyright   2016 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 global $CFG;
29 require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
31 /**
32  * Provides the unit tests for course global search.
33  *
34  * @package     core
35  * @category    phpunit
36  * @copyright   2016 David Monllao {@link http://www.davidmonllao.com}
37  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class course_search_testcase extends advanced_testcase {
41     /**
42      * @var string Area id
43      */
44     protected $mycoursesareaid = null;
46     /**
47      * @var string Area id for sections
48      */
49     protected $sectionareaid = null;
51     public function setUp() {
52         $this->resetAfterTest(true);
53         set_config('enableglobalsearch', true);
55         $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
56         $this->sectionareaid = \core_search\manager::generate_areaid('core_course', 'section');
58         // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
59         $search = testable_core_search::instance();
60     }
62     /**
63      * Indexing my courses contents.
64      *
65      * @return void
66      */
67     public function test_mycourses_indexing() {
69         // Returns the instance as long as the area is supported.
70         $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
71         $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
73         $user1 = self::getDataGenerator()->create_user();
74         $user2 = self::getDataGenerator()->create_user();
76         $course1 = self::getDataGenerator()->create_course();
77         $course2 = self::getDataGenerator()->create_course();
79         $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
80         $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
82         $record = new stdClass();
83         $record->course = $course1->id;
85         // All records.
86         $recordset = $searcharea->get_recordset_by_timestamp(0);
87         $this->assertTrue($recordset->valid());
88         $nrecords = 0;
89         foreach ($recordset as $record) {
90             $this->assertInstanceOf('stdClass', $record);
91             $doc = $searcharea->get_document($record);
92             $this->assertInstanceOf('\core_search\document', $doc);
93             $nrecords++;
94         }
95         // If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
96         $recordset->close();
97         $this->assertEquals(3, $nrecords);
99         // The +2 is to prevent race conditions.
100         $recordset = $searcharea->get_recordset_by_timestamp(time() + 2);
102         // No new records.
103         $this->assertFalse($recordset->valid());
104         $recordset->close();
105     }
107     /**
108      * Tests course indexing support for contexts.
109      */
110     public function test_mycourses_indexing_contexts() {
111         global $DB, $USER, $SITE;
113         $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
115         // Create some courses in categories, and a forum.
116         $generator = $this->getDataGenerator();
117         $cat1 = $generator->create_category();
118         $course1 = $generator->create_course(['category' => $cat1->id]);
119         $cat2 = $generator->create_category(['parent' => $cat1->id]);
120         $course2 = $generator->create_course(['category' => $cat2->id]);
121         $cat3 = $generator->create_category();
122         $course3 = $generator->create_course(['category' => $cat3->id]);
123         $forum = $generator->create_module('forum', ['course' => $course1->id]);
124         $DB->set_field('course', 'timemodified', 0, ['id' => $SITE->id]);
125         $DB->set_field('course', 'timemodified', 1, ['id' => $course1->id]);
126         $DB->set_field('course', 'timemodified', 2, ['id' => $course2->id]);
127         $DB->set_field('course', 'timemodified', 3, ['id' => $course3->id]);
129         // Find the first block to use for a block context.
130         $blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
131         $blockcontext = context_block::instance($blockid);
133         // Check with block context - should be null.
134         $this->assertNull($searcharea->get_document_recordset(0, $blockcontext));
136         // Check with user context - should be null.
137         $this->setAdminUser();
138         $usercontext = context_user::instance($USER->id);
139         $this->assertNull($searcharea->get_document_recordset(0, $usercontext));
141         // Check with module context - should be null.
142         $modcontext = context_module::instance($forum->cmid);
143         $this->assertNull($searcharea->get_document_recordset(0, $modcontext));
145         // Check with course context - should return specified course if timestamp allows.
146         $coursecontext = context_course::instance($course3->id);
147         $results = self::recordset_to_ids($searcharea->get_document_recordset(3, $coursecontext));
148         $this->assertEquals([$course3->id], $results);
149         $results = self::recordset_to_ids($searcharea->get_document_recordset(4, $coursecontext));
150         $this->assertEquals([], $results);
152         // Check with category context - should return course in categories and subcategories.
153         $catcontext = context_coursecat::instance($cat1->id);
154         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
155         $this->assertEquals([$course1->id, $course2->id], $results);
156         $results = self::recordset_to_ids($searcharea->get_document_recordset(2, $catcontext));
157         $this->assertEquals([$course2->id], $results);
159         // Check with system context and null - should return all these courses + site course.
160         $systemcontext = context_system::instance();
161         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $systemcontext));
162         $this->assertEquals([$SITE->id, $course1->id, $course2->id, $course3->id], $results);
163         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, null));
164         $this->assertEquals([$SITE->id, $course1->id, $course2->id, $course3->id], $results);
165         $results = self::recordset_to_ids($searcharea->get_document_recordset(3, $systemcontext));
166         $this->assertEquals([$course3->id], $results);
167         $results = self::recordset_to_ids($searcharea->get_document_recordset(3, null));
168         $this->assertEquals([$course3->id], $results);
169     }
171     /**
172      * Utility function to convert recordset to array of IDs for testing.
173      *
174      * @param moodle_recordset $rs Recordset to convert (and close)
175      * @return array Array of IDs from records indexed by number (0, 1, 2, ...)
176      */
177     protected static function recordset_to_ids(moodle_recordset $rs) {
178         $results = [];
179         foreach ($rs as $rec) {
180             $results[] = $rec->id;
181         }
182         $rs->close();
183         return $results;
184     }
186     /**
187      * Document contents.
188      *
189      * @return void
190      */
191     public function test_mycourses_document() {
193         // Returns the instance as long as the area is supported.
194         $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
195         $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
197         $user = self::getDataGenerator()->create_user();
198         $course = self::getDataGenerator()->create_course();
199         $this->getDataGenerator()->enrol_user($user->id, $course->id, 'teacher');
201         $doc = $searcharea->get_document($course);
202         $this->assertInstanceOf('\core_search\document', $doc);
203         $this->assertEquals($course->id, $doc->get('itemid'));
204         $this->assertEquals($this->mycoursesareaid . '-' . $course->id, $doc->get('id'));
205         $this->assertEquals($course->id, $doc->get('courseid'));
206         $this->assertFalse($doc->is_set('userid'));
207         $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
208         $this->assertEquals($course->fullname, $doc->get('title'));
210         // Not nice. Applying \core_search\document::set line breaks clean up.
211         $summary = preg_replace("/\s+/u", " ", content_to_text($course->summary, $course->summaryformat));
212         $this->assertEquals($summary, $doc->get('content'));
213         $this->assertEquals($course->shortname, $doc->get('description1'));
214     }
216     /**
217      * Document accesses.
218      *
219      * @return void
220      */
221     public function test_mycourses_access() {
223         // Returns the instance as long as the area is supported.
224         $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
226         $user1 = self::getDataGenerator()->create_user();
227         $user2 = self::getDataGenerator()->create_user();
229         $course1 = self::getDataGenerator()->create_course();
230         $course2 = self::getDataGenerator()->create_course(array('visible' => 0));
231         $course3 = self::getDataGenerator()->create_course();
233         $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
234         $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
235         $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'teacher');
236         $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
238         $this->setUser($user1);
239         $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
240         $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course2->id));
241         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
242         $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123));
244         $this->setUser($user2);
245         $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
246         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
247         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
248     }
250     /**
251      * Indexing section contents.
252      */
253     public function test_section_indexing() {
254         global $DB, $USER;
256         // Returns the instance as long as the area is supported.
257         $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
258         $this->assertInstanceOf('\core_course\search\section', $searcharea);
260         // Create some courses in categories, and a forum.
261         $generator = $this->getDataGenerator();
262         $cat1 = $generator->create_category();
263         $cat2 = $generator->create_category(['parent' => $cat1->id]);
264         $course1 = $generator->create_course(['category' => $cat1->id]);
265         $course2 = $generator->create_course(['category' => $cat2->id]);
266         $forum = $generator->create_module('forum', ['course' => $course1->id]);
268         // Edit 2 sections on course 1 and one on course 2.
269         $existing = $DB->get_record('course_sections', ['course' => $course1->id, 'section' => 2]);
270         $course1section2id = $existing->id;
271         $new = clone($existing);
272         $new->name = 'Frogs';
273         course_update_section($course1->id, $existing, $new);
275         $existing = $DB->get_record('course_sections', ['course' => $course1->id, 'section' => 3]);
276         $course1section3id = $existing->id;
277         $new = clone($existing);
278         $new->summary = 'Frogs';
279         $new->summaryformat = FORMAT_HTML;
280         course_update_section($course1->id, $existing, $new);
282         $existing = $DB->get_record('course_sections', ['course' => $course2->id, 'section' => 1]);
283         $course2section1id = $existing->id;
284         $new = clone($existing);
285         $new->summary = 'Frogs';
286         $new->summaryformat = FORMAT_HTML;
287         course_update_section($course2->id, $existing, $new);
289         // Bodge timemodified into a particular order.
290         $DB->set_field('course_sections', 'timemodified', 1, ['id' => $course1section3id]);
291         $DB->set_field('course_sections', 'timemodified', 2, ['id' => $course1section2id]);
292         $DB->set_field('course_sections', 'timemodified', 3, ['id' => $course2section1id]);
294         // All records.
295         $results = self::recordset_to_ids($searcharea->get_document_recordset(0));
296         $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
298         // Records after time 2.
299         $results = self::recordset_to_ids($searcharea->get_document_recordset(2));
300         $this->assertEquals([$course1section2id, $course2section1id], $results);
302         // Records after time 10 (there aren't any).
303         $results = self::recordset_to_ids($searcharea->get_document_recordset(10));
304         $this->assertEquals([], $results);
306         // Find the first block to use for a block context.
307         $blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
308         $blockcontext = context_block::instance($blockid);
310         // Check with block context - should be null.
311         $this->assertNull($searcharea->get_document_recordset(0, $blockcontext));
313         // Check with user context - should be null.
314         $this->setAdminUser();
315         $usercontext = context_user::instance($USER->id);
316         $this->assertNull($searcharea->get_document_recordset(0, $usercontext));
318         // Check with module context - should be null.
319         $modcontext = context_module::instance($forum->cmid);
320         $this->assertNull($searcharea->get_document_recordset(0, $modcontext));
322         // Check with course context - should return specific course entries.
323         $coursecontext = context_course::instance($course1->id);
324         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $coursecontext));
325         $this->assertEquals([$course1section3id, $course1section2id], $results);
326         $results = self::recordset_to_ids($searcharea->get_document_recordset(2, $coursecontext));
327         $this->assertEquals([$course1section2id], $results);
329         // Check with category context - should return course in categories and subcategories.
330         $catcontext = context_coursecat::instance($cat1->id);
331         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
332         $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
333         $catcontext = context_coursecat::instance($cat2->id);
334         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
335         $this->assertEquals([$course2section1id], $results);
337         // Check with system context - should return everything (same as null, tested first).
338         $systemcontext = context_system::instance();
339         $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $systemcontext));
340         $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
341     }
343     /**
344      * Document contents for sections.
345      */
346     public function test_section_document() {
347         global $DB;
349         $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
351         // Create a course.
352         $generator = $this->getDataGenerator();
353         $course = $generator->create_course();
355         // Test with default title.
356         $sectionrec = (object)['id' => 123, 'course' => $course->id,
357                 'section' => 3, 'timemodified' => 456,
358                 'summary' => 'Kermit', 'summaryformat' => FORMAT_HTML];
359         $doc = $searcharea->get_document($sectionrec);
360         $this->assertInstanceOf('\core_search\document', $doc);
361         $this->assertEquals(123, $doc->get('itemid'));
362         $this->assertEquals($this->sectionareaid . '-123', $doc->get('id'));
363         $this->assertEquals($course->id, $doc->get('courseid'));
364         $this->assertFalse($doc->is_set('userid'));
365         $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
366         $this->assertEquals('Topic 3', $doc->get('title'));
367         $this->assertEquals('Kermit', $doc->get('content'));
369         // Test with user-set title.
370         $DB->set_field('course_sections', 'name', 'Frogs',
371                 ['course' => $course->id, 'section' => 3]);
372         rebuild_course_cache($course->id, true);
373         $doc = $searcharea->get_document($sectionrec);
374         $this->assertEquals('Frogs', $doc->get('title'));
375     }
377     /**
378      * Document access for sections.
379      */
380     public function test_section_access() {
381         global $DB;
383         $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
385         // Create a course.
386         $generator = $this->getDataGenerator();
387         $course = $generator->create_course();
389         // Create 2 users - student and manager. Initially, student is not even enrolled.
390         $student = $generator->create_user();
391         $manager = $generator->create_user();
392         $generator->enrol_user($manager->id, $course->id, 'manager');
394         // Two sections have content - one is hidden.
395         $DB->set_field('course_sections', 'name', 'Frogs',
396                 ['course' => $course->id, 'section' => 1]);
397         $DB->set_field('course_sections', 'name', 'Toads',
398                 ['course' => $course->id, 'section' => 2]);
399         $DB->set_field('course_sections', 'visible', '0',
400                 ['course' => $course->id, 'section' => 2]);
402         // Make the modified time be in order of sections.
403         $DB->execute('UPDATE {course_sections} SET timemodified = section');
405         // Get the two document objects.
406         $rs = $searcharea->get_document_recordset();
407         $documents = [];
408         $index = 0;
409         foreach ($rs as $rec) {
410             $documents[$index++] = $searcharea->get_document($rec);
411         }
412         $this->assertCount(2, $documents);
414         // Log in as admin and check access.
415         $this->setAdminUser();
416         $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
417                 $searcharea->check_access($documents[0]->get('itemid')));
418         $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
419                 $searcharea->check_access($documents[1]->get('itemid')));
421         // Log in as manager and check access.
422         $this->setUser($manager);
423         $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
424                 $searcharea->check_access($documents[0]->get('itemid')));
425         $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
426                 $searcharea->check_access($documents[1]->get('itemid')));
428         // Log in as student and check access - none yet.
429         $this->setUser($student);
430         $this->assertEquals(\core_search\manager::ACCESS_DENIED,
431                 $searcharea->check_access($documents[0]->get('itemid')));
432         $this->assertEquals(\core_search\manager::ACCESS_DENIED,
433                 $searcharea->check_access($documents[1]->get('itemid')));
435         // Enrol student - now they should get access but not to the hidden one.
436         $generator->enrol_user($student->id, $course->id, 'student');
437         $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
438                 $searcharea->check_access($documents[0]->get('itemid')));
439         $this->assertEquals(\core_search\manager::ACCESS_DENIED,
440                 $searcharea->check_access($documents[1]->get('itemid')));
442         // Delete the course and check it returns deleted.
443         delete_course($course, false);
444         $this->assertEquals(\core_search\manager::ACCESS_DELETED,
445                 $searcharea->check_access($documents[0]->get('itemid')));
446         $this->assertEquals(\core_search\manager::ACCESS_DELETED,
447                 $searcharea->check_access($documents[1]->get('itemid')));
448     }
450     /**
451      * Test document icon for mycourse area.
452      */
453     public function test_get_doc_icon_for_mycourse_area() {
454         $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
456         $document = $this->getMockBuilder('\core_search\document')
457             ->disableOriginalConstructor()
458             ->getMock();
460         $result = $searcharea->get_doc_icon($document);
462         $this->assertEquals('i/course', $result->get_name());
463         $this->assertEquals('moodle', $result->get_component());
464     }
466     /**
467      * Test document icon for section area.
468      */
469     public function test_get_doc_icon_for_section_area() {
470         $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
472         $document = $this->getMockBuilder('\core_search\document')
473             ->disableOriginalConstructor()
474             ->getMock();
476         $result = $searcharea->get_doc_icon($document);
478         $this->assertEquals('i/section', $result->get_name());
479         $this->assertEquals('moodle', $result->get_component());
480     }
482     /**
483      * Test assigned search categories.
484      */
485     public function test_get_category_names() {
486         $coursessearcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
487         $sectionsearcharea = \core_search\manager::get_search_area($this->sectionareaid);
489         $this->assertEquals(['core-courses'], $coursessearcharea->get_category_names());
490         $this->assertEquals(['core-course-content'], $sectionsearcharea->get_category_names());
491     }