first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
/**
* Area category unit tests.
*
* @package core_search
* @copyright 2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class area_category_test extends \advanced_testcase {
/**
* A helper function to get a mocked search area.
* @param string $areaid
*
* @return \PHPUnit\Framework\MockObject\MockObject
*/
protected function get_mocked_area($areaid) {
$builder = $this->getMockBuilder('\core_search\base');
$builder->disableOriginalConstructor();
$builder->onlyMethods(array('get_area_id'));
$area = $builder->getMockForAbstractClass();
$area->method('get_area_id')->willReturn($areaid);
return $area;
}
/**
* A helper function to get a list of search areas.
*
* @return array
*/
protected function get_areas() {
$areas = [];
$areas[] = $this->get_mocked_area('area1');
$areas[] = 'String';
$areas[] = 1;
$areas[] = '12';
$areas[] = true;
$areas[] = false;
$areas[] = null;
$areas[] = [$this->get_mocked_area('area2')];
$areas[] = $this;
$areas[] = new \stdClass();
$areas[] = $this->get_mocked_area('area3');
$areas[] = $this->get_mocked_area('area4');
return $areas;
}
/**
* Test default values.
*/
public function test_default_values(): void {
$category = new \core_search\area_category('test_name', 'test_visiblename');
$this->assertEquals('test_name', $category->get_name());
$this->assertEquals('test_visiblename', $category->get_visiblename());
$this->assertEquals(0, $category->get_order());
$this->assertEquals([], $category->get_areas());
}
/**
* Test that all get functions work as expected.
*/
public function test_getters(): void {
$category = new \core_search\area_category('test_name', 'test_visiblename', 4, $this->get_areas());
$this->assertEquals('test_name', $category->get_name());
$this->assertEquals('test_visiblename', $category->get_visiblename());
$this->assertEquals(4, $category->get_order());
$this->assertTrue(is_array($category->get_areas()));
$this->assertCount(3, $category->get_areas());
$this->assertTrue(key_exists('area1', $category->get_areas()));
$this->assertTrue(key_exists('area3', $category->get_areas()));
$this->assertTrue(key_exists('area4', $category->get_areas()));
}
/**
* Test that a list of areas could be set correctly.
*/
public function test_list_of_areas_could_be_set(): void {
$category = new \core_search\area_category('test_name', 'test_visiblename');
$this->assertEquals([], $category->get_areas());
$category->set_areas($this->get_areas());
$this->assertTrue(is_array($category->get_areas()));
$this->assertCount(3, $category->get_areas());
$this->assertTrue(key_exists('area1', $category->get_areas()));
$this->assertTrue(key_exists('area3', $category->get_areas()));
$this->assertTrue(key_exists('area4', $category->get_areas()));
}
}
+391
View File
@@ -0,0 +1,391 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/fixtures/testable_core_search.php');
require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
/**
* Search engine base unit tests.
*
* @package core_search
* @copyright 2017 Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class base_activity_test extends \advanced_testcase {
/**
* @var \core_search::manager
*/
protected $search = null;
/**
* @var \core_search_generator Instace of core_search_generator.
*/
protected $generator = null;
/**
* @var Instace of testable_engine.
*/
protected $engine = null;
/** @var \context[] Array of test contexts */
protected $contexts;
/** @var \stdClass[] Array of test forum objects */
protected $forums;
public function setUp(): void {
global $DB;
$this->resetAfterTest();
set_config('enableglobalsearch', true);
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = \testable_core_search::instance();
$this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
$this->generator->setup();
$this->setAdminUser();
// Create course and 2 forums.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$this->contexts['c1'] = \context_course::instance($course->id);
$this->forums[1] = $generator->create_module('forum', ['course' => $course->id, 'name' => 'Forum 1',
'intro' => '<p>Intro 1</p>', 'introformat' => FORMAT_HTML]);
$this->contexts['f1'] = \context_module::instance($this->forums[1]->cmid);
$this->forums[2] = $generator->create_module('forum', ['course' => $course->id, 'name' => 'Forum 2',
'intro' => '<p>Intro 2</p>', 'introformat' => FORMAT_HTML]);
$this->contexts['f2'] = \context_module::instance($this->forums[2]->cmid);
// Create another 2 courses (in same category and in a new category) with one forum each.
$this->contexts['cc1'] = \context_coursecat::instance($course->category);
$course2 = $generator->create_course();
$this->contexts['c2'] = \context_course::instance($course2->id);
$this->forums[3] = $generator->create_module('forum', ['course' => $course2->id, 'name' => 'Forum 3',
'intro' => '<p>Intro 3</p>', 'introformat' => FORMAT_HTML]);
$this->contexts['f3'] = \context_module::instance($this->forums[3]->cmid);
$cat2 = $generator->create_category();
$this->contexts['cc2'] = \context_coursecat::instance($cat2->id);
$course3 = $generator->create_course(['category' => $cat2->id]);
$this->contexts['c3'] = \context_course::instance($course3->id);
$this->forums[4] = $generator->create_module('forum', ['course' => $course3->id, 'name' => 'Forum 4',
'intro' => '<p>Intro 4</p>', 'introformat' => FORMAT_HTML]);
$this->contexts['f4'] = \context_module::instance($this->forums[4]->cmid);
// Hack about with the time modified values.
foreach ($this->forums as $index => $forum) {
$DB->set_field('forum', 'timemodified', $index, ['id' => $forum->id]);
}
}
public function tearDown(): void {
// For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
if ($this->generator) {
// Moodle DML freaks out if we don't teardown the temp table after each run.
$this->generator->teardown();
$this->generator = null;
}
}
/**
* Test base activity get search fileareas
*/
public function test_get_search_fileareas_base(): void {
$builder = $this->getMockBuilder('\core_search\base_activity');
$builder->disableOriginalConstructor();
$stub = $builder->getMockForAbstractClass();
$result = $stub->get_search_fileareas();
$this->assertEquals(array('intro'), $result);
}
/**
* Test base attach files
*/
public function test_attach_files_base(): void {
$filearea = 'intro';
$component = 'mod_forum';
$module = 'forum';
$course = self::getDataGenerator()->create_course();
$activity = self::getDataGenerator()->create_module('forum', array('course' => $course->id));
$context = \context_module::instance($activity->cmid);
$contextid = $context->id;
// Create file to add.
$fs = get_file_storage();
$filerecord = array(
'contextid' => $contextid,
'component' => $component,
'filearea' => $filearea,
'itemid' => 0,
'filepath' => '/',
'filename' => 'testfile.txt');
$content = 'All the news that\'s fit to print';
$file = $fs->create_file_from_string($filerecord, $content);
// Construct the search document.
$rec = new \stdClass();
$rec->courseid = $course->id;
$area = new \core_mocksearch\search\mock_search_area();
$record = $this->generator->create_record($rec);
$document = $area->get_document($record);
$document->set('itemid', $activity->id);
// Create a mock from the abstract class,
// with required methods stubbed.
$builder = $this->getMockBuilder('\core_search\base_activity');
$builder->disableOriginalConstructor();
$builder->onlyMethods(array('get_module_name', 'get_component_name'));
$stub = $builder->getMockForAbstractClass();
$stub->method('get_module_name')->willReturn($module);
$stub->method('get_component_name')->willReturn($component);
// Attach file to our test document.
$stub->attach_files($document);
// Verify file is attached.
$files = $document->get_files();
$file = array_values($files)[0];
$this->assertEquals(1, count($files));
$this->assertEquals($content, $file->get_content());
}
/**
* Tests getting the recordset.
*/
public function test_get_document_recordset(): void {
global $USER, $DB;
// Get all the forums to index (no restriction).
$area = new \mod_forum\search\activity();
$results = self::recordset_to_indexed_array($area->get_document_recordset());
// Should return all forums.
$this->assertCount(4, $results);
// Each result should basically have the contents of the forum table. We'll just check
// the key fields for the first one and then the other ones by id only.
$this->assertEquals($this->forums[1]->id, $results[0]->id);
$this->assertEquals(1, $results[0]->timemodified);
$this->assertEquals($this->forums[1]->course, $results[0]->course);
$this->assertEquals('Forum 1', $results[0]->name);
$this->assertEquals('<p>Intro 1</p>', $results[0]->intro);
$this->assertEquals(FORMAT_HTML, $results[0]->introformat);
$allids = self::records_to_ids($this->forums);
$this->assertEquals($allids, self::records_to_ids($results));
// Repeat with a time restriction.
$results = self::recordset_to_indexed_array($area->get_document_recordset(3));
$this->assertEquals([$this->forums[3]->id, $this->forums[4]->id],
self::records_to_ids($results));
// Now use context restrictions. First, the whole site (no change).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, \context_system::instance()));
$this->assertEquals($allids, self::records_to_ids($results));
// Course 1 only.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $this->contexts['c1']));
$this->assertEquals([$this->forums[1]->id, $this->forums[2]->id],
self::records_to_ids($results));
// Course 2 only.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $this->contexts['c2']));
$this->assertEquals([$this->forums[3]->id], self::records_to_ids($results));
// Specific forum only.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $this->contexts['f4']));
$this->assertEquals([$this->forums[4]->id], self::records_to_ids($results));
// Category 1 context (courses 1 and 2).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $this->contexts['cc1']));
$this->assertEquals([$this->forums[1]->id, $this->forums[2]->id, $this->forums[3]->id],
self::records_to_ids($results));
// Category 2 context (course 3).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $this->contexts['cc2']));
$this->assertEquals([$this->forums[4]->id], self::records_to_ids($results));
// Combine context restriction (category 1) with timemodified.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
2, $this->contexts['cc1']));
$this->assertEquals([$this->forums[2]->id, $this->forums[3]->id],
self::records_to_ids($results));
// Find an arbitrary block on the system to get a block context.
$blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
$blockcontext = \context_block::instance($blockid);
// Block context (cannot return anything, so always null).
$this->assertNull($area->get_document_recordset(0, $blockcontext));
// User context (cannot return anything, so always null).
$usercontext = \context_user::instance($USER->id);
$this->assertNull($area->get_document_recordset(0, $usercontext));
}
/**
* Utility function to convert recordset to array for testing.
*
* @param \moodle_recordset $rs Recordset to convert
* @return array Array indexed by number (0, 1, 2, ...)
*/
protected static function recordset_to_indexed_array(\moodle_recordset $rs) {
$results = [];
foreach ($rs as $rec) {
$results[] = $rec;
}
$rs->close();
return $results;
}
/**
* Utility function to convert records to array of IDs.
*
* @param array $recs Records which should have an 'id' field
* @return array Array of ids
*/
protected static function records_to_ids(array $recs) {
$ids = [];
foreach ($recs as $rec) {
$ids[] = $rec->id;
}
return $ids;
}
/**
* Tests the get_doc_url function.
*/
public function test_get_doc_url(): void {
$area = new \mod_forum\search\activity();
$results = self::recordset_to_indexed_array($area->get_document_recordset());
for ($i = 0; $i < 4; $i++) {
$this->assertEquals(new \moodle_url('/mod/forum/view.php',
['id' => $this->forums[$i + 1]->cmid]),
$area->get_doc_url($area->get_document($results[$i])));
}
}
/**
* Tests the check_access function.
*/
public function test_check_access(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
// Create a test user who can access courses 1 and 2 (everything except forum 4).
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$generator->enrol_user($user->id, $this->forums[1]->course, 'student');
$generator->enrol_user($user->id, $this->forums[3]->course, 'student');
$this->setUser($user);
// Delete forum 2 and set forum 3 hidden.
course_delete_module($this->forums[2]->cmid);
set_coursemodule_visible($this->forums[3]->cmid, 0);
// Call check access on all the first three.
$area = new \mod_forum\search\activity();
$this->assertEquals(\core_search\manager::ACCESS_GRANTED, $area->check_access(
$this->forums[1]->id));
$this->assertEquals(\core_search\manager::ACCESS_DELETED, $area->check_access(
$this->forums[2]->id));
$this->assertEquals(\core_search\manager::ACCESS_DENIED, $area->check_access(
$this->forums[3]->id));
// Note: Do not check forum 4 which is in a course the user can't access; this will return
// ACCESS_GRANTED, but it does not matter because the search engine will not have included
// that context in the list to search. (This is because the $cm->uservisible access flag
// is only valid if the user is known to be able to access the course.)
}
/**
* Tests the module version of get_contexts_to_reindex, which is supposed to return all the
* activity contexts in order of date added.
*/
public function test_get_contexts_to_reindex(): void {
global $DB;
$this->resetAfterTest();
// Set up a course with two URLs and a Page.
$generator = $this->getDataGenerator();
$course = $generator->create_course(['fullname' => 'TCourse']);
$url1 = $generator->create_module('url', ['course' => $course->id, 'name' => 'TURL1']);
$url2 = $generator->create_module('url', ['course' => $course->id, 'name' => 'TURL2']);
$page = $generator->create_module('page', ['course' => $course->id, 'name' => 'TPage1']);
// Hack the items so they have different added times.
$now = time();
$DB->set_field('course_modules', 'added', $now - 3, ['id' => $url2->cmid]);
$DB->set_field('course_modules', 'added', $now - 2, ['id' => $url1->cmid]);
$DB->set_field('course_modules', 'added', $now - 1, ['id' => $page->cmid]);
// Check the URL contexts are in date order.
$urlarea = new \mod_url\search\activity();
$contexts = iterator_to_array($urlarea->get_contexts_to_reindex(), false);
$this->assertEquals([\context_module::instance($url1->cmid),
\context_module::instance($url2->cmid)], $contexts);
// Check the Page contexts.
$pagearea = new \mod_page\search\activity();
$contexts = iterator_to_array($pagearea->get_contexts_to_reindex(), false);
$this->assertEquals([\context_module::instance($page->cmid)], $contexts);
// Check another module area that has no instances.
$glossaryarea = new \mod_glossary\search\activity();
$contexts = iterator_to_array($glossaryarea->get_contexts_to_reindex(), false);
$this->assertEquals([], $contexts);
}
/**
* Test document icon.
*/
public function test_get_doc_icon(): void {
$baseactivity = $this->getMockBuilder('\core_search\base_activity')
->disableOriginalConstructor()
->onlyMethods(array('get_module_name'))
->getMockForAbstractClass();
$baseactivity->method('get_module_name')->willReturn('test_activity');
$document = $this->getMockBuilder('\core_search\document')
->disableOriginalConstructor()
->getMock();
$result = $baseactivity->get_doc_icon($document);
$this->assertEquals('monologo', $result->get_name());
$this->assertEquals('test_activity', $result->get_component());
}
}
+459
View File
@@ -0,0 +1,459 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/testable_core_search.php');
require_once(__DIR__ . '/fixtures/mock_block_area.php');
/**
* Unit tests for the base_block class.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class base_block_test extends \advanced_testcase {
/**
* Tests getting the name out of the class name.
*/
public function test_get_block_name(): void {
$area = new \block_mockblock\search\area();
$this->assertEquals('mockblock', $area->get_block_name());
}
/**
* Tests getting the recordset.
*/
public function test_get_document_recordset(): void {
global $DB, $USER;
$this->resetAfterTest();
$this->setAdminUser();
// Create course and activity module.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$page = $generator->create_module('page', ['course' => $course->id]);
$pagecontext = \context_module::instance($page->cmid);
// Create another 2 courses (in same category and in a new category).
$cat1context = \context_coursecat::instance($course->category);
$course2 = $generator->create_course();
$course2context = \context_course::instance($course2->id);
$cat2 = $generator->create_category();
$cat2context = \context_coursecat::instance($cat2->id);
$course3 = $generator->create_course(['category' => $cat2->id]);
$course3context = \context_course::instance($course3->id);
// Add blocks by hacking table (because it's not a real block type).
// 1. Block on course page.
$configdata = base64_encode(serialize((object) ['example' => 'content']));
$instance = (object)['blockname' => 'mockblock', 'parentcontextid' => $coursecontext->id,
'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*',
'defaultweight' => 0, 'timecreated' => 1, 'timemodified' => 1,
'configdata' => $configdata];
$block1id = $DB->insert_record('block_instances', $instance);
$block1context = \context_block::instance($block1id);
// 2. Block on activity page.
$instance->parentcontextid = $pagecontext->id;
$instance->pagetypepattern = 'mod-page-view';
$instance->timemodified = 2;
$block2id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block2id);
// 3. Block on site context.
$sitecourse = get_site();
$sitecontext = \context_course::instance($sitecourse->id);
$instance->parentcontextid = $sitecontext->id;
$instance->pagetypepattern = 'site-index';
$instance->timemodified = 3;
$block3id = $DB->insert_record('block_instances', $instance);
$block3context = \context_block::instance($block3id);
// 4. Block on course page but no data.
$instance->parentcontextid = $coursecontext->id;
$instance->pagetypepattern = 'course-view-*';
unset($instance->configdata);
$instance->timemodified = 4;
$block4id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block4id);
// 5. Block on course page but not this block.
$instance->blockname = 'mockotherblock';
$instance->configdata = $configdata;
$instance->timemodified = 5;
$block5id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block5id);
// 6. Block on course page with '*' page type.
$instance->blockname = 'mockblock';
$instance->pagetypepattern = '*';
$instance->timemodified = 6;
$block6id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block6id);
// 7. Block on course page with 'course-*' page type.
$instance->pagetypepattern = 'course-*';
$instance->timemodified = 7;
$block7id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block7id);
// 8. Block on course 2.
$instance->parentcontextid = $course2context->id;
$instance->timemodified = 8;
$block8id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block8id);
// 9. Block on course 3.
$instance->parentcontextid = $course3context->id;
$instance->timemodified = 9;
$block9id = $DB->insert_record('block_instances', $instance);
\context_block::instance($block9id);
// Get all the blocks.
$area = new \block_mockblock\search\area();
$results = self::recordset_to_indexed_array($area->get_document_recordset());
// Only blocks 1, 3, 6, 7, 8, 9 should be returned. Check all the fields for the first two.
$this->assertCount(6, $results);
$this->assertEquals($block1id, $results[0]->id);
$this->assertEquals(1, $results[0]->timemodified);
$this->assertEquals(1, $results[0]->timecreated);
$this->assertEquals($configdata, $results[0]->configdata);
$this->assertEquals($course->id, $results[0]->courseid);
$this->assertEquals($block1context->id, $results[0]->contextid);
$this->assertEquals($block3id, $results[1]->id);
$this->assertEquals(3, $results[1]->timemodified);
$this->assertEquals(1, $results[1]->timecreated);
$this->assertEquals($configdata, $results[1]->configdata);
$this->assertEquals($sitecourse->id, $results[1]->courseid);
$this->assertEquals($block3context->id, $results[1]->contextid);
// For the later ones, just check it got the right ones!
$this->assertEquals($block6id, $results[2]->id);
$this->assertEquals($block7id, $results[3]->id);
$this->assertEquals($block8id, $results[4]->id);
$this->assertEquals($block9id, $results[5]->id);
// Repeat with a time restriction.
$results = self::recordset_to_indexed_array($area->get_document_recordset(2));
// Only block 3, 6, 7, 8, and 9 are returned.
$this->assertEquals([$block3id, $block6id, $block7id, $block8id, $block9id],
self::records_to_ids($results));
// Now use context restrictions. First, the whole site (no change).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, \context_system::instance()));
$this->assertEquals([$block1id, $block3id, $block6id, $block7id, $block8id, $block9id],
self::records_to_ids($results));
// Course page only (leave out the one on site page and other courses).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $coursecontext));
$this->assertEquals([$block1id, $block6id, $block7id],
self::records_to_ids($results));
// Other course page only.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $course2context));
$this->assertEquals([$block8id], self::records_to_ids($results));
// Activity module only (no results).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $pagecontext));
$this->assertCount(0, $results);
// Specific block context.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $block3context));
$this->assertEquals([$block3id], self::records_to_ids($results));
// User context (no results).
$usercontext = \context_user::instance($USER->id);
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $usercontext));
$this->assertCount(0, $results);
// Category 1 context (courses 1 and 2).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $cat1context));
$this->assertEquals([$block1id, $block6id, $block7id, $block8id],
self::records_to_ids($results));
// Category 2 context (course 3).
$results = self::recordset_to_indexed_array($area->get_document_recordset(
0, $cat2context));
$this->assertEquals([$block9id], self::records_to_ids($results));
// Combine context restriction (category 1) with timemodified.
$results = self::recordset_to_indexed_array($area->get_document_recordset(
7, $cat1context));
$this->assertEquals([$block7id, $block8id], self::records_to_ids($results));
}
/**
* Utility function to convert recordset to array for testing.
*
* @param \moodle_recordset $rs Recordset to convert
* @return array Array indexed by number (0, 1, 2, ...)
*/
protected static function recordset_to_indexed_array(\moodle_recordset $rs) {
$results = [];
foreach ($rs as $rec) {
$results[] = $rec;
}
$rs->close();
return $results;
}
/**
* Utility function to convert records to array of IDs.
*
* @param array $recs Records which should have an 'id' field
* @return array Array of ids
*/
protected static function records_to_ids(array $recs) {
$ids = [];
foreach ($recs as $rec) {
$ids[] = $rec->id;
}
return $ids;
}
/**
* Tests the get_doc_url function.
*/
public function test_get_doc_url(): void {
global $DB;
$this->resetAfterTest();
// Create course and activity module.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$page = $generator->create_module('page', ['course' => $course->id]);
$pagecontext = \context_module::instance($page->cmid);
// Create block on course page.
$configdata = base64_encode(serialize(new \stdClass()));
$instance = (object)['blockname' => 'mockblock', 'parentcontextid' => $coursecontext->id,
'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*', 'defaultweight' => 0,
'timecreated' => 1, 'timemodified' => 1, 'configdata' => $configdata];
$blockid = $DB->insert_record('block_instances', $instance);
// Get document URL.
$area = new \block_mockblock\search\area();
$doc = $this->get_doc($course->id, $blockid);
$expected = new \moodle_url('/course/view.php', ['id' => $course->id], 'inst' . $blockid);
$this->assertEquals($expected, $area->get_doc_url($doc));
$this->assertEquals($expected, $area->get_context_url($doc));
// Repeat with block on site page.
$sitecourse = get_site();
$sitecontext = \context_course::instance($sitecourse->id);
$instance->pagetypepattern = 'site-index';
$instance->parentcontextid = $sitecontext->id;
$block2id = $DB->insert_record('block_instances', $instance);
// Get document URL.
$doc2 = $this->get_doc($course->id, $block2id);
$expected = new \moodle_url('/', ['redirect' => 0], 'inst' . $block2id);
$this->assertEquals($expected, $area->get_doc_url($doc2));
$this->assertEquals($expected, $area->get_context_url($doc2));
// Repeat with block on module page (this cannot happen yet because the search query will
// only include course context blocks, but let's check it works for the future).
$instance->pagetypepattern = 'mod-page-view';
$instance->parentcontextid = $pagecontext->id;
$block3id = $DB->insert_record('block_instances', $instance);
// Get and check document URL, ignoring debugging message for unsupported page type.
$debugmessage = 'Unexpected module-level page type for block ' . $block3id .
': mod-page-view';
$doc3 = $this->get_doc($course->id, $block3id);
$this->assertDebuggingCalledCount(2, [$debugmessage, $debugmessage]);
$expected = new \moodle_url('/mod/page/view.php', ['id' => $page->cmid], 'inst' . $block3id);
$this->assertEquals($expected, $area->get_doc_url($doc3));
$this->assertDebuggingCalled($debugmessage);
$this->assertEquals($expected, $area->get_context_url($doc3));
$this->assertDebuggingCalled($debugmessage);
// Repeat with another block on course page but '*' pages.
$instance->pagetypepattern = '*';
$instance->parentcontextid = $coursecontext->id;
$block4id = $DB->insert_record('block_instances', $instance);
// Get document URL.
$doc = $this->get_doc($course->id, $block4id);
$expected = new \moodle_url('/course/view.php', ['id' => $course->id], 'inst' . $block4id);
$this->assertEquals($expected, $area->get_doc_url($doc));
$this->assertEquals($expected, $area->get_context_url($doc));
// And same thing but 'course-*' pages.
$instance->pagetypepattern = 'course-*';
$block5id = $DB->insert_record('block_instances', $instance);
// Get document URL.
$doc = $this->get_doc($course->id, $block5id);
$expected = new \moodle_url('/course/view.php', ['id' => $course->id], 'inst' . $block5id);
$this->assertEquals($expected, $area->get_doc_url($doc));
$this->assertEquals($expected, $area->get_context_url($doc));
}
/**
* Tests the check_access function.
*/
public function test_check_access(): void {
global $DB;
$this->resetAfterTest();
// Create course and activity module.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$page = $generator->create_module('page', ['course' => $course->id]);
$pagecontext = \context_module::instance($page->cmid);
// Create block on course page.
$configdata = base64_encode(serialize(new \stdClass()));
$instance = (object)['blockname' => 'mockblock', 'parentcontextid' => $coursecontext->id,
'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*', 'defaultweight' => 0,
'timecreated' => 1, 'timemodified' => 1, 'configdata' => $configdata];
$blockid = $DB->insert_record('block_instances', $instance);
// Check access for block that exists.
$area = new \block_mockblock\search\area();
$this->assertEquals(\core_search\manager::ACCESS_GRANTED, $area->check_access($blockid));
// Check access for nonexistent block.
$this->assertEquals(\core_search\manager::ACCESS_DELETED, $area->check_access($blockid + 1));
// Check if block is not in a course context any longer.
$DB->set_field('block_instances', 'parentcontextid', $pagecontext->id, ['id' => $blockid]);
\core_search\base_block::clear_static();
$this->assertEquals(\core_search\manager::ACCESS_DELETED, $area->check_access($blockid));
// Or what if it is in a course context but has supported vs. unsupported page type.
$DB->set_field('block_instances', 'parentcontextid', $coursecontext->id, ['id' => $blockid]);
$DB->set_field('block_instances', 'pagetypepattern', 'course-*', ['id' => $blockid]);
\core_search\base_block::clear_static();
$this->assertEquals(\core_search\manager::ACCESS_GRANTED, $area->check_access($blockid));
$DB->set_field('block_instances', 'pagetypepattern', '*', ['id' => $blockid]);
\core_search\base_block::clear_static();
$this->assertEquals(\core_search\manager::ACCESS_GRANTED, $area->check_access($blockid));
$DB->set_field('block_instances', 'pagetypepattern', 'course-view-frogs', ['id' => $blockid]);
\core_search\base_block::clear_static();
$this->assertEquals(\core_search\manager::ACCESS_GRANTED, $area->check_access($blockid));
$DB->set_field('block_instances', 'pagetypepattern', 'anythingelse', ['id' => $blockid]);
\core_search\base_block::clear_static();
$this->assertEquals(\core_search\manager::ACCESS_DELETED, $area->check_access($blockid));
}
/**
* Tests the block version of get_contexts_to_reindex, which is supposed to return all the
* block contexts in order of date added.
*/
public function test_get_contexts_to_reindex(): void {
global $DB;
$this->resetAfterTest();
// Create course and activity module.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$page = $generator->create_module('page', ['course' => $course->id]);
$pagecontext = \context_module::instance($page->cmid);
// Create blocks on course page, with time modified non-sequential.
$configdata = base64_encode(serialize(new \stdClass()));
$instance = (object)['blockname' => 'mockblock', 'parentcontextid' => $coursecontext->id,
'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*', 'defaultweight' => 0,
'timecreated' => 1, 'timemodified' => 100, 'configdata' => $configdata];
$blockid1 = $DB->insert_record('block_instances', $instance);
$context1 = \context_block::instance($blockid1);
$instance->timemodified = 120;
$blockid2 = $DB->insert_record('block_instances', $instance);
$context2 = \context_block::instance($blockid2);
$instance->timemodified = 110;
$blockid3 = $DB->insert_record('block_instances', $instance);
$context3 = \context_block::instance($blockid3);
// Create another block on the activity page (not included).
$instance->parentcontextid = $pagecontext->id;
$blockid4 = $DB->insert_record('block_instances', $instance);
\context_block::instance($blockid4);
// Check list of contexts.
$area = new \block_mockblock\search\area();
$contexts = iterator_to_array($area->get_contexts_to_reindex(), false);
$expected = [
$context2,
$context3,
$context1
];
$this->assertEquals($expected, $contexts);
}
/**
* Gets a search document object from the fake search area.
*
* @param int $courseid Course id in document
* @param int $blockinstanceid Block instance id in document
* @return \core_search\document Document object
*/
protected function get_doc($courseid, $blockinstanceid) {
$engine = \testable_core_search::instance()->get_engine();
$area = new \block_mockblock\search\area();
$docdata = ['id' => $blockinstanceid, 'courseid' => $courseid,
'areaid' => $area->get_area_id(), 'itemid' => 0];
return $engine->to_document($area, $docdata);
}
/**
* Test document icon.
*/
public function test_get_doc_icon(): void {
$baseblock = $this->getMockBuilder('\core_search\base_block')
->disableOriginalConstructor()
->getMockForAbstractClass();
$document = $this->getMockBuilder('\core_search\document')
->disableOriginalConstructor()
->getMock();
$result = $baseblock->get_doc_icon($document);
$this->assertEquals('e/anchor', $result->get_name());
$this->assertEquals('moodle', $result->get_component());
}
}
+175
View File
@@ -0,0 +1,175 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/fixtures/testable_core_search.php');
require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
/**
* Search engine base unit tests.
*
* @package core_search
* @copyright 2017 Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class base_test extends \advanced_testcase {
/**
* @var \core_search::manager
*/
protected $search = null;
/**
* @var Instace of core_search_generator.
*/
protected $generator = null;
/**
* @var Instace of testable_engine.
*/
protected $engine = null;
public function setUp(): void {
$this->resetAfterTest();
set_config('enableglobalsearch', true);
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = \testable_core_search::instance();
$this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
$this->generator->setup();
}
public function tearDown(): void {
// For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
if ($this->generator) {
// Moodle DML freaks out if we don't teardown the temp table after each run.
$this->generator->teardown();
$this->generator = null;
}
}
/**
* Test base get search fileareas
*/
public function test_get_search_fileareas_base(): void {
$builder = $this->getMockBuilder('\core_search\base');
$builder->disableOriginalConstructor();
$stub = $builder->getMockForAbstractClass();
$result = $stub->get_search_fileareas();
$this->assertEquals(array(), $result);
}
/**
* Test base attach files
*/
public function test_attach_files_base(): void {
$filearea = 'search';
$component = 'mod_test';
// Create file to add.
$fs = get_file_storage();
$filerecord = array(
'contextid' => 1,
'component' => $component,
'filearea' => $filearea,
'itemid' => 1,
'filepath' => '/',
'filename' => 'testfile.txt');
$content = 'All the news that\'s fit to print';
$file = $fs->create_file_from_string($filerecord, $content);
// Construct the search document.
$rec = new \stdClass();
$rec->contextid = 1;
$area = new \core_mocksearch\search\mock_search_area();
$record = $this->generator->create_record($rec);
$document = $area->get_document($record);
// Create a mock from the abstract class,
// with required methods stubbed.
$builder = $this->getMockBuilder('\core_search\base');
$builder->disableOriginalConstructor();
$builder->onlyMethods(array('get_search_fileareas', 'get_component_name'));
$stub = $builder->getMockForAbstractClass();
$stub->method('get_search_fileareas')->willReturn(array($filearea));
$stub->method('get_component_name')->willReturn($component);
// Attach file to our test document.
$stub->attach_files($document);
// Verify file is attached.
$files = $document->get_files();
$file = array_values($files)[0];
$this->assertEquals(1, count($files));
$this->assertEquals($content, $file->get_content());
}
/**
* Tests the base version (stub) of get_contexts_to_reindex.
*/
public function test_get_contexts_to_reindex(): void {
$area = new \core_mocksearch\search\mock_search_area();
$this->assertEquals([\context_system::instance()],
iterator_to_array($area->get_contexts_to_reindex(), false));
}
/**
* Test default document icon.
*/
public function test_get_default_doc_icon(): void {
$basearea = $this->getMockBuilder('\core_search\base')
->disableOriginalConstructor()
->getMockForAbstractClass();
$document = $this->getMockBuilder('\core_search\document')
->disableOriginalConstructor()
->getMock();
$result = $basearea->get_doc_icon($document);
$this->assertEquals('i/empty', $result->get_name());
$this->assertEquals('moodle', $result->get_component());
}
/**
* Test base search area category names.
*/
public function test_get_category_names(): void {
$builder = $this->getMockBuilder('\core_search\base');
$builder->disableOriginalConstructor();
$stub = $builder->getMockForAbstractClass();
$expected = ['core-other'];
$this->assertEquals($expected, $stub->get_category_names());
}
/**
* Test getting all required search area setting names.
*/
public function test_get_settingnames(): void {
$expected = array('_enabled', '_indexingstart', '_indexingend', '_lastindexrun',
'_docsignored', '_docsprocessed', '_recordsprocessed', '_partial');
$this->assertEquals($expected, \core_search\base::get_settingnames());
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat search-related step definitions.
*
* @package core_search
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL used, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode;
use Moodle\BehatExtension\Exception\SkippedException;
/**
* Behat search-related step definitions.
*
* @package core_search
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_search extends behat_base {
/**
* Create event when starting on the front page.
*
* @Given /^I search for "(?P<query>[^"]*)" using the header global search box$/
* @param string $query Query to search for
*/
public function i_search_for_using_the_header_global_search_box($query) {
// Click the search icon.
$this->execute("behat_general::i_click_on", [get_string('togglesearch', 'core'), 'button']);
// Set the field.
$this->execute('behat_forms::i_set_the_field_to', ['q', $query]);
// Submit the form.
$this->execute("behat_general::i_click_on_in_the",
[get_string('search', 'core'), 'button', '#usernavigation', 'css_element']);
}
/**
* Sets results which will be returned for the next search. It will only return links to
* activities at present.
*
* @Given /^global search expects the query "(?P<query>[^"]*)" and will return:$/
* @param string $query Expected query value (just used to check the query passed to the engine)
* @param TableNode $data Data rows
*/
public function global_search_expects_the_query_and_will_return($query, TableNode $data) {
global $DB;
$outdata = new stdClass();
$outdata->query = $query;
$outdata->results = [];
foreach ($data->getHash() as $rowdata) {
// Check and get the data from the user-entered row.
$input = [
'type' => '',
'idnumber' => '',
'title' => '',
'content' => '',
'modified' => ''
];
foreach ($rowdata as $key => $value) {
if (!array_key_exists($key, $input)) {
throw new Exception('Field ' . $key . '" does not exist');
}
$input[$key] = $value;
}
foreach (['idnumber', 'type'] as $requiredfield) {
if (!$input[$requiredfield]) {
throw new Exception('Must specify required field: ' . $requiredfield);
}
}
// Check type (we only support activity at present, this could be extended to allow
// faking other types of search results such as a user, course, or forum post).
if ($input['type'] !== 'activity') {
throw new Exception('Unsupported type: ' . $input['type']);
}
// Find the specified activity.
$idnumber = $input['idnumber'];
$cmid = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber], IGNORE_MISSING);
if (!$cmid) {
throw new Exception('Cannot find activity with idnumber: ' . $idnumber);
}
list ($course, $cm) = get_course_and_cm_from_cmid($cmid);
$rec = $DB->get_record($cm->modname, ['id' => $cm->instance], '*', MUST_EXIST);
$context = \context_module::instance($cm->id);
// Set up the internal fields used in creating the search document.
$out = new stdClass();
$out->itemid = $cm->instance;
$out->componentname = 'mod_' . $cm->modname;
$out->areaname = 'activity';
$out->fields = new stdClass();
$out->fields->contextid = $context->id;
$out->fields->courseid = $course->id;
if ($input['title']) {
$out->fields->title = $input['title'];
} else {
$out->fields->title = $cm->name;
}
if ($input['content']) {
$out->fields->content = $input['content'];
} else {
$out->fields->content = content_to_text($rec->intro, $rec->introformat);
}
if ($input['modified']) {
$out->fields->modified = strtotime($input['modified']);
} else {
$out->fields->modified = $cm->added;
}
$out->extrafields = new stdClass();
$out->extrafields->coursefullname = $course->fullname;
$outdata->results[] = $out;
}
set_config('behat_fakeresult', json_encode($outdata), 'core_search');
}
/**
* Updates the global search index to take account of any added activities.
*
* @Given /^I update the global search index$/
* @throws moodle_exception
*/
public function i_update_the_global_search_index() {
\core_search\manager::instance()->index(false);
}
/**
* This step looks to see if Solr is installed or skip the rest of the scenario otherwise
*
* @Given /^solr is installed/
*/
public function solr_is_installed() {
if (!function_exists('solr_get_version')) {
throw new SkippedException('Skipping this scenario because Solr is not installed.');
}
}
}
+68
View File
@@ -0,0 +1,68 @@
@core @core_search
Feature: Select users when searching for user-created content
In order to search for content by specific users
As a user
I need to be able to add users to the select list in the search form
Background:
Given solr is installed
And the following config values are set as admin:
| enableglobalsearch | 1 |
| searchengine | solr |
And the following "courses" exist:
| shortname | fullname |
| C1 | Frogs |
| C2 | Zombies |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| page | PageName1 | PageDesc1 | C1 | PAGE1 |
And the following "users" exist:
| username | firstname | lastname |
| s1 | Anne | Other |
| s2 | Anne | Additional |
| t | Anne | Ditin |
And the following "course enrolments" exist:
| user | course | role |
| s1 | C1 | student |
| s2 | C2 | student |
| t | C1 | teacher |
@javascript
Scenario: As administrator, search for users from home page
Given I log in as "admin"
And global search expects the query "frogs" and will return:
| type | idnumber |
| activity | PAGE1 |
And I search for "frogs" using the header global search box
And I expand all fieldsets
When I expand the "Users" autocomplete
# Alphabetical last name order.
Then "Anne Additional" "text" should appear before "Anne Ditin" "text" in the "Users" "autocomplete"
And "Anne Ditin" "text" should appear before "Anne Other" "text" in the "Users" "autocomplete"
@javascript
Scenario: As administrator, search for users within course
Given I log in as "admin"
And I am on "Frogs" course homepage
And global search expects the query "frogs" and will return:
| type | idnumber |
| activity | PAGE1 |
And I search for "frogs" using the header global search box
And I expand all fieldsets
And I select "Course: Frogs" from the "Search within" singleselect
When I expand the "Users" autocomplete
# Users in selected course appear first.
Then "Anne Additional" "text" should appear after "Anne Other" "text" in the "Users" "autocomplete"
@javascript
Scenario: As student, cannot see users on other courses
Given I log in as "s1"
And I am on "Frogs" course homepage
And global search expects the query "frogs" and will return:
| type | idnumber |
| activity | PAGE1 |
And I search for "frogs" using the header global search box
And I expand all fieldsets
When I expand the "Users" autocomplete
Then "Anne Ditin" "text" should appear before "Anne Other" "text" in the "Users" "autocomplete"
And "Anne Additional" "text" should not exist
@@ -0,0 +1,36 @@
@core @core_search
Feature: Show system information in the search interface
In order to let users know if there are current problems with search
As an admin
I need to be able to show information on search pages
Background:
Given the following config values are set as admin:
| enableglobalsearch | 1 |
| searchengine | simpledb |
And I log in as "admin"
@javascript
Scenario: Information displays when enabled
When the following config values are set as admin:
| searchbannerenable | 1 |
| searchbanner | The search currently only finds frog-related content; we hope to fix it soon. |
And I search for "toads" using the header global search box
Then I should see "The search currently only finds frog-related content" in the ".notifywarning" "css_element"
@javascript
Scenario: Information does not display when not enabled
When the following config values are set as admin:
| searchbannerenable | 0 |
| searchbanner | The search currently only finds frog-related content; we hope to fix it soon. |
And I search for "toads" using the header global search box
Then I should not see "The search currently only finds frog-related content"
And ".notifywarning" "css_element" should not exist
@javascript
Scenario: Information does not display when left blank
When the following config values are set as admin:
| searchbannerenable | 1 |
| searchbanner | |
And I search for "toads" using the header global search box
Then ".notifywarning" "css_element" should not exist
+128
View File
@@ -0,0 +1,128 @@
@core @core_search
Feature: Use global search interface
In order to search for things
As a user
I need to be able to type search queries and see results
Background:
Given the following config values are set as admin:
| enableglobalsearch | 1 |
| searchengine | simpledb |
And the following "courses" exist:
| shortname | fullname |
| F1 | Amphibians |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| page | PageName1 frogs amphibians | PageDesc1 | F1 | PAGE1 |
| forum | ForumName1 toads amphibians | ForumDesc1 | F1 | FORUM1 |
And I update the global search index
And I log in as "admin"
@javascript
Scenario: Search from header search box with one result
When I search for "frogs" using the header global search box
Then I should see "PageName1"
And I should see "PageDesc1"
# Check the link works.
And I follow "PageName1"
And I should see "PageName1" in the ".breadcrumb" "css_element"
@javascript
Scenario: Search from search page with two results
When I search for "zombies" using the header global search box
Then I should see "No results"
And I set the field "id_q" to "amphibians"
# You cannot press "Search" because there's a fieldset with the same name that gets in the way.
And I press "id_submitbutton"
And I should see "ForumName1"
And I should see "ForumDesc1"
And I should see "PageName1"
And I should see "PageDesc1"
# Check the link works.
And I follow "ForumName1"
And I should see "ForumName1" in the ".breadcrumb" "css_element"
@javascript
Scenario: Search from search page with quotes
Given I search for "zombies" using the header global search box
And I should see "No results"
When I set the field "id_q" to "\"amphibians\""
# You cannot press "Search" because there's a fieldset with the same name that gets in the way.
And I press "id_submitbutton"
Then I should see "ForumName1"
And I should see "ForumDesc1"
And I should see "PageName1"
And I should see "PageDesc1"
# Check the link works.
And I follow "ForumName1"
And I should see "ForumName1" in the ".breadcrumb" "css_element"
@javascript
Scenario: Search starting from site context (no within option)
When I search for "frogs" using the header global search box
And I expand all fieldsets
Then I should not see "Search within"
And I should see "Courses" in the "region-main" "region"
@javascript
Scenario: Search starting from course context (within option lists course)
When I am on "Amphibians" course homepage
And I search for "frogs" using the header global search box
And I expand all fieldsets
Then I should see "Search within"
And I select "Everywhere you can access" from the "Search within" singleselect
And I should see "Courses" in the "region-main" "region"
And I select "Course: Amphibians" from the "Search within" singleselect
And I should not see "Courses" in the "region-main" "region"
@javascript
Scenario: Search starting from forum context (within option lists course and forum)
Given I am on the "ForumName1 toads amphibians" "Forum activity" page
When I search for "frogs" using the header global search box
Then I expand all fieldsets
And I should see "Search within"
And I select "Everywhere you can access" from the "Search within" singleselect
And I should see "Courses" in the "region-main" "region"
And I select "Course: Amphibians" from the "Search within" singleselect
And I should not see "Courses" in the "region-main" "region"
And I select "Forum: ForumName1" from the "Search within" singleselect
And I should not see "Courses" in the "region-main" "region"
@javascript
Scenario: Check that groups option in search form appears when intended
# Switch to mocked Solr search because simpledb doesn't support groups.
Given solr is installed
And the following config values are set as admin:
| searchengine | solr |
And the following "groups" exist:
| name | course | idnumber |
| A Group | F1 | G1 |
| B Group | F1 | G2 |
And the following "activities" exist:
| activity | name | course | idnumber | groupmode |
| forum | ForumSG | F1 | FORUM2 | 1 |
When I am on the ForumSG "Forum activity" page
And global search expects the query "frogs" and will return:
| type | idnumber |
| activity | PAGE1 |
And I search for "frogs" using the header global search box
And I expand all fieldsets
Then I should not see "All groups" in the "region-main" "region"
And I select "Course: Amphibians" from the "Search within" singleselect
And I should see "All groups" in the "region-main" "region"
And I set the field "Groups" to "A Group"
And I select "Forum: ForumSG" from the "Search within" singleselect
And I should see "A Group" in the "region-main" "region"
And I am on the "ForumName1 toads amphibians" "Forum activity" page
And global search expects the query "frogs" and will return:
| type | idnumber |
| activity | PAGE1 |
And I search for "frogs" using the header global search box
And I expand all fieldsets
Then I should not see "All groups" in the "region-main" "region"
And I select "Course: Amphibians" from the "Search within" singleselect
And I should see "All groups" in the "region-main" "region"
And I select "Forum: ForumName1" from the "Search within" singleselect
And I should not see "All groups" in the "region-main" "region"
@@ -0,0 +1,22 @@
@core @core_search
Feature: Plugins > Search > Search setup contains Setup search engine only if the target section actually exists
In order to set up the selected search engine
As an admin
I need to be able to click the link 'Setup search engine' but only if the target section actually exists
Scenario: Selected search engine has an admin section
Given the following config values are set as admin:
| enableglobalsearch | 1 |
| searchengine | solr |
And I log in as "admin"
When I navigate to "Plugins > Search" in site administration
Then "Setup search engine" "link" should exist
Scenario: Selected search engine does not have an admin section
Given the following config values are set as admin:
| enableglobalsearch | 1 |
| searchengine | simpledb |
And I log in as "admin"
When I navigate to "Plugins > Search" in site administration
Then I should see "Setup search engine"
And "Setup search engine" "link" should not exist
+45
View File
@@ -0,0 +1,45 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
/**
* Document icon unit tests.
*
* @package core_search
* @copyright 2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class document_icon_test extends \advanced_testcase {
/**
* Test that default component gets returned correctly.
*/
public function test_default_component(): void {
$docicon = new \core_search\document_icon('test_name');
$this->assertEquals('test_name', $docicon->get_name());
$this->assertEquals('moodle', $docicon->get_component());
}
/**
* Test that name and component get returned correctly.
*/
public function test_can_get_name_and_component(): void {
$docicon = new \core_search\document_icon('test_name', 'test_component');
$this->assertEquals('test_name', $docicon->get_name());
$this->assertEquals('test_component', $docicon->get_component());
}
}
+275
View File
@@ -0,0 +1,275 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
use advanced_testcase;
use context_course;
use core_mocksearch\search\mock_search_area;
use mock_search\engine;
use testable_core_search;
use stdClass;
/**
* Unit tests for search document.
*
* @package core_search
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_search\document
*/
class document_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
}
/**
* @var Instace of core_search_generator.
*/
protected $generator = null;
public function setUp(): void {
$this->resetAfterTest();
set_config('enableglobalsearch', true);
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = \testable_core_search::instance();
$this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
$this->generator->setup();
}
/**
* Adding this test here as get_areas_user_accesses process is the same, results just depend on the context level.
*
* @covers ::export_for_template
* @return void
*/
public function test_search_user_accesses(): void {
global $PAGE;
$area = new mock_search_area();
$renderer = $PAGE->get_renderer('core_search');
$engine = new engine();
$course = $this->getDataGenerator()->create_course(['fullname' => 'Course & Title']);
$user = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'Escape & Name']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'teacher');
$this->setAdminUser();
// Make a record to enter in the search area.
$record = new stdClass();
$record->title = 'Escape & Title';
$record->content = 'Escape & Content';
$record->description1 = 'Escape & Description1';
$record->description2 = 'Escape & Description2';
$record->userid = $user->id;
$record->courseid = $course->id;
$record = $this->generator->create_record($record);
// Convert to a 'doc data' type format.
$docdata = $area->convert_record_to_doc_array($record);
// First see that the docuemnt has the right information, unescaped.
$doc = $engine->to_document($area, $docdata);
$this->assertEquals('Escape & Title', $doc->get('title'));
$this->assertEquals('Escape & Content', $doc->get('content'));
$this->assertEquals('Escape & Description1', $doc->get('description1'));
$this->assertEquals('Escape & Description2', $doc->get('description2'));
$this->assertEquals('User Escape & Name', $doc->get('userfullname'));
$this->assertEquals('Course & Title', $doc->get('coursefullname'));
// Export for template, and see if it is escaped.
$export = $doc->export_for_template($renderer);
$this->assertEquals('Escape &amp; Title', $export['title']);
$this->assertEquals('Escape &amp; Content', $export['content']);
$this->assertEquals('Escape &amp; Description1', $export['description1']);
$this->assertEquals('Escape &amp; Description2', $export['description2']);
$this->assertEquals('User Escape &amp; Name', $export['userfullname']);
$this->assertEquals('Course &amp; Title', $export['coursefullname']);
}
/**
* Test we can set and get document icon.
*
* @covers ::set_doc_icon
*/
public function test_get_and_set_doc_icon(): void {
$document = $this->getMockBuilder('\core_search\document')
->disableOriginalConstructor()
->getMockForAbstractClass();
$this->assertNull($document->get_doc_icon());
$docicon = new \core_search\document_icon('test_name', 'test_component');
$document->set_doc_icon($docicon);
$this->assertEquals($docicon, $document->get_doc_icon());
}
public function tearDown(): void {
// For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
if ($this->generator) {
// Moodle DML freaks out if we don't teardown the temp table after each run.
$this->generator->teardown();
$this->generator = null;
}
}
/**
* Test the document author visibility depending on the user capabilities.
*
* @covers ::export_for_template
* @dataProvider document_author_visibility_provider
* @param string $rolename the role name
* @param array $capexceptions the capabilities exceptions
* @param bool $expected the expected author visibility
* @param bool $owndocument if the resulting document belongs to the current user
*/
public function test_document_author_visibility(
string $rolename = 'editingteacher',
array $capexceptions = [],
bool $expected = true,
bool $owndocument = false
): void {
global $DB, $PAGE;
$area = new mock_search_area();
$renderer = $PAGE->get_renderer('core_search');
$engine = new engine();
$course = $this->getDataGenerator()->create_course(['fullname' => 'Course & Title']);
$context = context_course::instance($course->id);
$roleid = $DB->get_field('role', 'id', ['shortname' => $rolename]);
foreach ($capexceptions as $capability) {
assign_capability($capability, CAP_PROHIBIT, $roleid, $context->id);
}
$user = $this->getDataGenerator()->create_user(['firstname' => 'Test', 'lastname' => 'User']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename);
$this->setUser($user);
if ($owndocument) {
$author = $user;
} else {
$author = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'Escape & Name']);
$this->getDataGenerator()->enrol_user($author->id, $course->id, 'student');
}
// Make a record to enter in the search area.
$record = new stdClass();
$record->title = 'Escape & Title';
$record->content = 'Escape & Content';
$record->description1 = 'Escape & Description1';
$record->description2 = 'Escape & Description2';
$record->userid = $author->id;
$record->courseid = $course->id;
$record->contextid = $context->id;
$record = $this->generator->create_record($record);
// Convert to a 'doc data' type format.
$docdata = $area->convert_record_to_doc_array($record);
// First see that the document has the user information.
$doc = $engine->to_document($area, $docdata);
$this->assertEquals(fullname($author), $doc->get('userfullname'));
// Export for template, and see if it the user information is exported.
$export = $doc->export_for_template($renderer);
if ($expected) {
$authorname = htmlentities(fullname($author), ENT_COMPAT);
$this->assertEquals($authorname, $export['userfullname']);
} else {
$this->assertArrayNotHasKey('userfullname', $export);
}
}
/**
* Data provider for test_document_author_visibility().
*
* @return array
*/
public function document_author_visibility_provider(): array {
return [
'Teacher' => [
'rolename' => 'editingteacher',
'capexceptions' => [],
'expected' => true,
'owndocument' => false,
],
'Non editing teacher' => [
'rolename' => 'teacher',
'capexceptions' => [],
'expected' => true,
'owndocument' => false,
],
'Student' => [
'rolename' => 'student',
'capexceptions' => [],
'expected' => true,
'owndocument' => false,
],
// Adding capability exceptions.
'Student without view profiles' => [
'rolename' => 'student',
'capexceptions' => ['moodle/user:viewdetails'],
'expected' => false,
'owndocument' => false,
],
'Student without view participants' => [
'rolename' => 'student',
'capexceptions' => ['moodle/course:viewparticipants'],
'expected' => false,
'owndocument' => false,
],
'Student without view participants or profiles' => [
'rolename' => 'student',
'capexceptions' => ['moodle/user:viewdetails', 'moodle/course:viewparticipants'],
'expected' => false,
'owndocument' => false,
],
// Users should be able to see its own documents.
'Student author without view profiles' => [
'rolename' => 'student',
'capexceptions' => ['moodle/user:viewdetails'],
'expected' => true,
'owndocument' => true,
],
'Student author without view participants' => [
'rolename' => 'student',
'capexceptions' => ['moodle/course:viewparticipants'],
'expected' => true,
'owndocument' => true,
],
'Student author without view participants or profiles' => [
'rolename' => 'student',
'capexceptions' => ['moodle/user:viewdetails', 'moodle/course:viewparticipants'],
'expected' => true,
'owndocument' => true,
],
];
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/testable_core_search.php');
require_once(__DIR__ . '/fixtures/mock_search_area.php');
/**
* Search engine base unit tests.
*
* @package core_search
* @category phpunit
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class engine_test extends \advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
set_config('enableglobalsearch', true);
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = \testable_core_search::instance();
}
/**
* Engine basic info.
*
* @return void
*/
public function test_engine_info(): void {
$engine = new \mock_search\engine();
$this->assertEquals('mock_search', $engine->get_plugin_name());
// Resolves to the default one.
$this->assertEquals('\\core_search\\document', $engine->get_document_classname());
}
/**
* Test engine caches.
*
* @return void
*/
public function test_engine_caches(): void {
global $DB;
$engine = new \mock_search\engine();
$course1 = self::getDataGenerator()->create_course();
$this->assertEquals($course1->id, $engine->get_course($course1->id)->id);
$dbreads = $DB->perf_get_reads();
$engine->get_course($course1->id);
$this->assertEquals($dbreads, $DB->perf_get_reads());
$fakearea1 = \core_search\manager::generate_areaid('plugintype_unexisting', 'fakearea');
$fakearea2 = \core_search\manager::generate_areaid('mod_unexisting', 'morefake');
$this->assertFalse($engine->get_search_area($fakearea1));
$this->assertFalse($engine->get_search_area($fakearea2));
$this->assertFalse($engine->get_search_area($fakearea2));
$areaid = \core_search\manager::generate_areaid('mod_forum', 'post');
$this->assertInstanceOf('\\mod_forum\\search\\post', $engine->get_search_area($areaid));
$dbreads = $DB->perf_get_reads();
$this->assertInstanceOf('\\mod_forum\\search\\post', $engine->get_search_area($areaid));
$this->assertEquals($dbreads, $DB->perf_get_reads());
}
/**
* Tests the core functions related to schema updates.
*/
public function test_engine_schema_modification(): void {
// Apply a schema update starting from no version.
$engine = new \mock_search\engine();
$engine->check_latest_schema();
$updates = $engine->get_and_clear_schema_updates();
$this->assertCount(1, $updates);
$this->assertEquals(0, $updates[0][0]);
$this->assertEquals(\core_search\document::SCHEMA_VERSION, $updates[0][1]);
// Store older version and check that.
$engine->record_applied_schema_version(1066101400);
$engine = new \mock_search\engine();
$engine->check_latest_schema();
$updates = $engine->get_and_clear_schema_updates();
$this->assertCount(1, $updates);
$this->assertEquals(1066101400, $updates[0][0]);
$this->assertEquals(\core_search\document::SCHEMA_VERSION, $updates[0][1]);
// Store current version and check no updates.
$engine->record_applied_schema_version(\core_search\document::SCHEMA_VERSION);
$engine = new \mock_search\engine();
$engine->check_latest_schema();
$updates = $engine->get_and_clear_schema_updates();
$this->assertCount(0, $updates);
}
/**
* Tests the get_supported_orders stub function.
*/
public function test_get_supported_orders(): void {
$engine = new \mock_search\engine();
$orders = $engine->get_supported_orders(\context_system::instance());
$this->assertCount(1, $orders);
$this->assertArrayHasKey('relevance', $orders);
}
/**
* Test that search engine sets an icon before render a document.
*/
public function test_engine_sets_doc_icon(): void {
$generator = self::getDataGenerator()->get_plugin_generator('core_search');
$generator->setup();
$area = new \core_mocksearch\search\mock_search_area();
$engine = new \mock_search\engine();
$record = $generator->create_record();
$docdata = $area->get_document($record)->export_for_engine();
$doc = $engine->to_document($area, $docdata);
$this->assertNotNull($doc->get_doc_icon());
$generator->teardown();
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for search events.
*
* @package core_search
* @category phpunit
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search\event;
/**
* Unit tests for search events.
*
* @package core_search
* @category phpunit
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class events_test extends \advanced_testcase {
/**
* test_search_results_viewed
*
* @return void
*/
public function test_search_results_viewed(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$sink = $this->redirectEvents();
// Basic event.
\core_search\manager::trigger_search_results_viewed([
'q' => 'I am a query',
'page' => 0,
]);
$events = $sink->get_events();
$event = reset($events);
$sink->clear();
$this->assertEquals(\context_system::instance(), $event->get_context());
$urlparams = ['q' => 'I am a query', 'page' => 0];
$this->assertEquals($urlparams, $event->get_url()->params());
\core_search\manager::trigger_search_results_viewed([
'q' => 'I am a query',
'page' => 2,
'title' => 'I am the title',
'areaids' => array(3,4,5),
'courseids' => array(2,3),
'timestart' => 1445644800,
'timeend' => 1477267200
]);
$events = $sink->get_events();
$event = reset($events);
$this->assertEquals(\context_system::instance(), $event->get_context());
$urlparams = ['q' => 'I am a query', 'page' => 2, 'title' => 'I am the title', 'timestart' => 1445644800, 'timeend' => 1477267200];
$this->assertEquals($urlparams, $event->get_url()->params());
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search\external;
use core_external\external_api;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Tests for the get_results external function.
*
* @package core_search
* @category test
* @copyright 2023 Juan Leyva (juan@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_search\external\get_results
*/
class get_results_test extends \externallib_advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
/**
* test external api
* @covers ::execute
* @return void
*/
public function test_external_get_results(): void {
set_config('enableglobalsearch', true);
set_config('searchengine', 'simpledb');
$this->setAdminUser();
// Test search not returning anything (nothing in the index yet).
$return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('one'));
$this->assertEquals(0, $return['totalcount']);
// Create an index of searchable things.
$generator = $this->getDataGenerator();
$course = $generator->create_course(['fullname' => 'SearchTest course']);
$anothercourse = $generator->create_course(['fullname' => 'Another']);
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$studentothercourse = $this->getDataGenerator()->create_and_enrol($anothercourse, 'student');
$page = $generator->create_module('page', ['course' => $course->id, 'name' => 'SearchTest page']);
$forum = $generator->create_module('forum', ['course' => $course->id]);
$fgenerator = $generator->get_plugin_generator('mod_forum');
for ($i = 0; $i < 15; $i++) {
$fgenerator->create_discussion(
[
'course' => $course->id,
'forum' => $forum->id,
'userid' => $student->id,
]
);
}
$search = \core_search\manager::instance();
$search->index();
// Basic search, by text.
$return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('page'));
$this->assertEquals(1, $return['totalcount']);
$this->assertEquals('activity', $return['results'][0]['areaname']);
$this->assertEquals($page->name, $return['results'][0]['title']);
// Basic search, by name containing text.
$return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('SearchTest'));
$this->assertEquals(2, $return['totalcount']);
// Test pagination.
$return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('discussion', [], 0));
$this->assertCount(10, $return['results']); // The first 10 posts of a total of 15 for the second page.
$this->assertEquals(15, $return['totalcount']);
$return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('discussion', [], 1));
$this->assertCount(5, $return['results']); // The last 5 posts of a total of 15 for the second page.
$this->assertEquals(15, $return['totalcount']);
// Test some filters.
$return = external_api::clean_returnvalue(get_results::execute_returns(),
get_results::execute('discussion', ['title' => 'Discussion 11']));
$this->assertEquals(1, $return['totalcount']);
// No discussions created in the future.
$return = external_api::clean_returnvalue(get_results::execute_returns(),
get_results::execute('discussion', ['timestart' => time() + DAYSECS]));
$this->assertEquals(0, $return['totalcount']);
// Basic permissions check.
$this->setUser($studentothercourse);
$return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('discussion', [], 1));
$this->assertCount(0, $return['results']); // I should not see other courses discussions.
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search\external;
use core_external\external_api;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Tests for the get_search_areas_list external function.
*
* @package core_search
* @category test
* @copyright 2023 Juan Leyva (juan@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_search\external\get_search_areas_list
*/
class get_search_areas_list_test extends \externallib_advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
/**
* test external api
*
* @covers ::execute
* @return void
*/
public function test_external_get_search_areas_list(): void {
set_config('enableglobalsearch', true);
set_config('searchenablecategories', true);
$this->setAdminUser();
$result = get_search_areas_list::execute();
$result = external_api::clean_returnvalue(get_search_areas_list::execute_returns(), $result);
$this->assertNotEmpty($result['areas']);
$totalareas = count($result['areas']);
// Filter.
$result = get_search_areas_list::execute('core-users');
$result = external_api::clean_returnvalue(get_search_areas_list::execute_returns(), $result);
$totalfilterareas = count($result['areas']);
// Just count numbers, plugins can inject areas.
$this->assertLessThan($totalareas, $totalfilterareas);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search\external;
use core_external\external_api;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Tests for the get_top_results external function.
*
* @package core_search
* @category test
* @copyright 2023 Juan Leyva (juan@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_search\external\get_top_results
*/
class get_top_results_test extends \externallib_advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
/**
* test external api
* @covers ::execute
* @return void
*/
public function test_external_get_top_results(): void {
set_config('enableglobalsearch', true);
set_config('searchenablecategories', true); // Required for top search.
set_config('searchmaxtopresults', 5); // Change default.
set_config('searchengine', 'simpledb');
$this->setAdminUser();
// Create an index of searchable things.
$generator = $this->getDataGenerator();
$course = $generator->create_course(['fullname' => 'SearchTest course']);
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$forum = $generator->create_module('forum', ['course' => $course->id]);
$fgenerator = $generator->get_plugin_generator('mod_forum');
for ($i = 0; $i < 15; $i++) {
$fgenerator->create_discussion(
[
'course' => $course->id,
'forum' => $forum->id,
'userid' => $student->id,
]
);
}
$search = \core_search\manager::instance();
$search->index();
// Test top results.
$return = external_api::clean_returnvalue(get_top_results::execute_returns(), get_top_results::execute('discussion', []));
$this->assertCount(5, $return['results']); // We get the 5 top results according to searchmaxtopresults setting value.
set_config('searchmaxtopresults', 3); // Change to 3 top.
$return = external_api::clean_returnvalue(get_top_results::execute_returns(), get_top_results::execute('discussion', []));
$this->assertCount(3, $return['results']);
// Test some filters.
$return = external_api::clean_returnvalue(get_top_results::execute_returns(),
get_top_results::execute('discussion', ['title' => 'Discussion 11']));
$this->assertCount(1, $return['results']);
set_config('searchenablecategories', false); // Disable top search.
$return = external_api::clean_returnvalue(get_top_results::execute_returns(), get_top_results::execute('discussion', []));
$this->assertCount(0, $return['results']);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search\external;
use core_external\external_api;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Tests for the view_results external function.
*
* @package core_search
* @category test
* @copyright 2023 Juan Leyva (juan@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_search\external\view_results
*/
class view_results_test extends \externallib_advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
/**
* test external api
* @covers ::execute
* @return void
*/
public function test_external_view_results(): void {
set_config('enableglobalsearch', true);
$this->setAdminUser();
// Trigger and capture the event.
$sink = $this->redirectEvents();
$result = view_results::execute('forum post', ['title' => 'My progress'], 1);
$result = external_api::clean_returnvalue(view_results::execute_returns(), $result);
$this->assertEmpty($result['warnings']);
$this->assertTrue($result['status']);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = array_shift($events);
$sink->close();
// Checking that the event contains the expected values.
$this->assertInstanceOf('core\event\search_results_viewed', $event);
$this->assertEventContextNotUsed($event);
$this->assertNotEmpty($event->get_name());
$this->assertEquals('forum post', $event->get_data()['other']['q']);
$this->assertEquals('My progress', $event->get_data()['other']['title']);
$this->assertEquals(1, $event->get_data()['other']['page']);
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function unit tests.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
/**
* External function unit tests.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_test extends \advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
/**
* Checks the get_relevant_users function used when selecting users in search filter.
*/
public function test_get_relevant_users(): void {
// Set up two users to search for and one to do the searching.
$generator = $this->getDataGenerator();
$student1 = $generator->create_user(['firstname' => 'Amelia', 'lastname' => 'Aardvark']);
$student2 = $generator->create_user(['firstname' => 'Amelia', 'lastname' => 'Beetle']);
$student3 = $generator->create_user(['firstname' => 'Zebedee', 'lastname' => 'Boing']);
$course = $generator->create_course();
$generator->enrol_user($student1->id, $course->id, 'student');
$generator->enrol_user($student2->id, $course->id, 'student');
$generator->enrol_user($student3->id, $course->id, 'student');
// As student 3, search for the other two.
$this->setUser($student3);
$result = external::clean_returnvalue(
external::get_relevant_users_returns(),
external::get_relevant_users('Amelia', 0)
);
// Check we got the two expected users back.
$this->assertEquals([
$student1->id,
$student2->id,
], array_column($result, 'id'));
// Check that the result contains all the expected fields.
$this->assertEquals($student1->id, $result[0]['id']);
$this->assertEquals('Amelia Aardvark', $result[0]['fullname']);
$this->assertStringContainsString('/u/f2', $result[0]['profileimageurlsmall']);
// Check we aren't leaking information about user email address (for instance).
$this->assertArrayNotHasKey('email', $result[0]);
// Note: We are not checking search permissions, search by different fields, etc. as these
// are covered by the core_user::search unit test.
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test block area.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace block_mockblock\search;
defined('MOODLE_INTERNAL') || die;
/**
* Test block area.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class area extends \core_search\base_block {
public function get_document($record, $options = array()) {
throw new \coding_exception('Not implemented');
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_mocksearch\search;
/**
* Component implementing search for testing purposes.
*
* @package core_search
* @category phpunit
* @copyright David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
class mock_search_area extends \core_search\base {
/** @var float If set, waits when doing the indexing query (seconds) */
protected $indexingdelay = 0;
/**
* Multiple context level so we can test get_areas_user_accesses.
* @var int[]
*/
protected static $levels = [CONTEXT_COURSE, CONTEXT_USER];
/**
* To make things easier, base class required config stuff.
*
* @return bool
*/
public function is_enabled() {
return true;
}
public function get_recordset_by_timestamp($modifiedfrom = 0) {
global $DB;
if ($this->indexingdelay) {
\testable_core_search::fake_current_time(
\core_search\manager::get_current_time() + $this->indexingdelay);
}
$sql = "SELECT * FROM {temp_mock_search_area} WHERE timemodified >= ? ORDER BY timemodified ASC";
return $DB->get_recordset_sql($sql, array($modifiedfrom));
}
/**
* A helper function that will turn a record into 'data array', for use with document building.
*/
public function convert_record_to_doc_array($record) {
$docdata = (array)unserialize($record->info);
$docdata['areaid'] = $this->get_area_id();
$docdata['itemid'] = $record->id;
$docdata['modified'] = $record->timemodified;
return $docdata;
}
public function get_document($record, $options = array()) {
global $USER;
$info = unserialize($record->info);
// Prepare associative array with data from DB.
$doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
$doc->set('title', $info->title);
$doc->set('content', $info->content);
$doc->set('description1', $info->description1);
$doc->set('description2', $info->description2);
$doc->set('contextid', $info->contextid);
$doc->set('courseid', $info->courseid);
$doc->set('userid', $info->userid);
$doc->set('owneruserid', $info->owneruserid);
$doc->set('modified', $record->timemodified);
return $doc;
}
public function attach_files($document) {
global $DB;
if (!$record = $DB->get_record('temp_mock_search_area', array('id' => $document->get('itemid')))) {
return;
}
$info = unserialize($record->info);
foreach ($info->attachfileids as $fileid) {
$document->add_stored_file($fileid);
}
}
public function uses_file_indexing() {
return true;
}
public function check_access($id) {
global $DB, $USER;
if ($record = $DB->get_record('temp_mock_search_area', array('id' => $id))) {
$info = unserialize($record->info);
if (in_array($USER->id, $info->denyuserids)) {
return \core_search\manager::ACCESS_DENIED;
}
return \core_search\manager::ACCESS_GRANTED;
}
return \core_search\manager::ACCESS_DELETED;
}
public function get_doc_url(\core_search\document $doc) {
return new \moodle_url('/index.php');
}
public function get_context_url(\core_search\document $doc) {
return new \moodle_url('/index.php');
}
public function get_visible_name($lazyload = false) {
return 'Mock search area';
}
/**
* Sets a fake delay to simulate time taken doing the indexing query.
*
* @param float $seconds Delay in seconds for each time indexing query is called
*/
public function set_indexing_delay($seconds) {
$this->indexingdelay = $seconds;
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mock_search;
/**
* Search engine for testing purposes.
*
* @package core_search
* @category phpunit
* @copyright David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_search\manager;
defined('MOODLE_INTERNAL') || die;
class engine extends \core_search\engine {
/** @var float If set, waits when adding each document (seconds) */
protected $adddelay = 0;
/** @var \core_search\document[] Documents added */
protected $added = [];
/** @var array Schema updates applied */
protected $schemaupdates = [];
/** @var array delete of course index. */
protected $deletes = [];
public function is_installed() {
return true;
}
public function is_server_ready() {
return true;
}
public function add_document($document, $fileindexing = false) {
if ($this->adddelay) {
\testable_core_search::fake_current_time(manager::get_current_time() + $this->adddelay);
}
$this->added[] = $document;
return true;
}
public function execute_query($data, $usercontexts, $limit = 0) {
// No need to implement.
return [];
}
public function delete($areaid = null) {
return null;
}
public function to_document(\core_search\base $searcharea, $docdata) {
return parent::to_document($searcharea, $docdata);
}
public function get_course($courseid) {
return parent::get_course($courseid);
}
public function get_search_area($areaid) {
return parent::get_search_area($areaid);
}
public function get_query_total_count() {
return 0;
}
/**
* Sets an add delay to simulate time taken indexing.
*
* @param float $seconds Delay in seconds for each document
*/
public function set_add_delay($seconds) {
$this->adddelay = $seconds;
}
/**
* Gets the list of indexed (added) documents since last time this function
* was called.
*
* @return \core_search\document[] List of documents, in order added.
*/
public function get_and_clear_added_documents() {
$added = $this->added;
$this->added = [];
return $added;
}
public function update_schema($oldversion, $newversion) {
$this->schemaupdates[] = [$oldversion, $newversion];
}
/**
* Gets all schema updates applied, as an array. Each entry has an array with two values,
* old and new version.
*
* @return array List of schema updates for comparison
*/
public function get_and_clear_schema_updates() {
$result = $this->schemaupdates;
$this->schemaupdates = [];
return $result;
}
/**
* Records delete of course index so it can be checked later.
*
* @param int $oldcourseid Course id
* @return bool True to indicate action taken
*/
public function delete_index_for_course(int $oldcourseid) {
$this->deletes[] = ['course', $oldcourseid];
return true;
}
/**
* Records delete of context index so it can be checked later.
*
* @param int $oldcontextid Context id
* @return bool True to indicate action taken
*/
public function delete_index_for_context(int $oldcontextid) {
$this->deletes[] = ['context', $oldcontextid];
return true;
}
/**
* Gets all course/context deletes applied, as an array. Each entry is an array with two
* values, the first is either 'course' or 'context' and the second is the id deleted.
*
* @return array List of deletes for comparison
*/
public function get_and_clear_deletes() {
$deletes = $this->deletes;
$this->deletes = [];
return $deletes;
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Core search class adapted to unit test.
*
* @package core_search
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/mock_search_engine.php');
/**
* Core search class adapted to unit test.
*
* Note that by default all core search areas are returned when calling get_search_areas_list,
* if you want to use the mock search area you can use testable_core_search::add_search_area
* although if you want to add mock search areas on top of the core ones you should call
* testable_core_search::add_core_search_areas before calling testable_core_search::add_search_area.
*
* @package core_search
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_core_search extends \core_search\manager {
/**
* Attaches the mock engine to search.
*
* Auto enables global search.
*
* @param \core_search\engine|bool $searchengine
* @param bool $ignored Second param just to make this compatible with base class
* @return testable_core_search
*/
public static function instance($searchengine = false, bool $ignored = false) {
// One per request, this should be purged during testing.
if (self::$instance !== null) {
return self::$instance;
}
set_config('enableglobalsearch', true);
// Default to the mock one.
if ($searchengine === false) {
$searchengine = new \mock_search\engine();
}
self::$instance = new testable_core_search($searchengine);
return self::$instance;
}
/**
* Changes visibility.
*
* @return array
*/
public function get_areas_user_accesses($limitcourseids = false, $limitcontextids = false) {
return parent::get_areas_user_accesses($limitcourseids, $limitcontextids);
}
/**
* Adds an enabled search component to the search areas list.
*
* @param string $areaid
* @param \core_search\base $searcharea
* @return void
*/
public function add_search_area($areaid, \core_search\base $searcharea) {
self::$enabledsearchareas[$areaid] = $searcharea;
self::$allsearchareas[$areaid] = $searcharea;
}
/**
* Loads all core search areas.
*
* @return void
*/
public function add_core_search_areas() {
self::get_search_areas_list(false);
self::get_search_areas_list(true);
}
/**
* Changes visibility.
*
* @param string $classname
* @return bool
*/
public static function is_search_area($classname) {
return parent::is_search_area($classname);
}
/**
* Fakes the current time for PHPunit. Turns off faking time if called with default parameter.
*
* Note: This should be replaced with core functionality once possible (see MDL-60644).
*
* @param float $faketime Current time
*/
public static function fake_current_time($faketime = 0.0) {
static::$phpunitfaketime = $faketime;
}
/**
* Makes build_limitcourseids method public for testing.
*
* @param \stdClass $formdata Submitted search form data.
*
* @return array|bool
*/
public function build_limitcourseids(\stdClass $formdata) {
$limitcourseids = parent::build_limitcourseids($formdata);
return $limitcourseids;
}
}
+206
View File
@@ -0,0 +1,206 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generator for test search area.
*
* @package core_search
* @category phpunit
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Mock search area data generator class.
*
* @package core_search
* @category test
* @copyright 2016 Eric Merrill
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_search_generator extends component_generator_base {
/**
* Creates the mock search area temp table.
*/
public function setUp(): void {
global $DB;
$dbman = $DB->get_manager();
// Make our temp table if we need it.
if (!$dbman->table_exists('temp_mock_search_area')) {
$table = new \xmldb_table('temp_mock_search_area');
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
$table->add_field('info', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_temp_table($table);
}
}
/**
* Destroys the mock search area temp table.
*/
public function tearDown(): void {
global $DB;
$dbman = $DB->get_manager();
// Make our temp table if we need it.
if ($dbman->table_exists('temp_mock_search_area')) {
$table = new \xmldb_table('temp_mock_search_area');
$dbman->drop_table($table);
}
}
/**
* Deletes all records in the search area table.
*/
public function delete_all() {
global $DB;
// Delete any records in the search area.
$DB->delete_records('temp_mock_search_area');
}
/**
* Adds a new record to the mock search area based on the provided options.
*/
public function create_record($options = null) {
global $DB, $USER;
$record = new \stdClass();
$info = new \stdClass();
if (empty($options->timemodified)) {
$record->timemodified = time();
} else {
$record->timemodified = $options->timemodified;
}
if (!isset($options->content)) {
$info->content = 'A test message to find.';
} else {
$info->content = $options->content;
}
if (!isset($options->description1)) {
$info->description1 = 'Description 1.';
} else {
$info->description1 = $options->description1;
}
if (!isset($options->description2)) {
$info->description2 = 'Description 2.';
} else {
$info->description2 = $options->description2;
}
if (!isset($options->title)) {
$info->title = 'A basic title';
} else {
$info->title = $options->title;
}
if (!isset($options->contextid)) {
$info->contextid = \context_course::instance(SITEID)->id;
} else {
$info->contextid = $options->contextid;
}
if (!isset($options->courseid)) {
$info->courseid = SITEID;
} else {
$info->courseid = $options->courseid;
}
if (!isset($options->userid)) {
$info->userid = $USER->id;
} else {
$info->userid = $options->userid;
}
if (!isset($options->owneruserid)) {
$info->owneruserid = \core_search\manager::NO_OWNER_ID;
} else {
$info->owneruserid = $options->owneruserid;
}
// This takes a userid (or array of) that will be denied when check_access() is called.
if (!isset($options->denyuserids)) {
$info->denyuserids = array();
} else {
if (is_array($options->denyuserids)) {
$info->denyuserids = $options->denyuserids;
} else {
$info->denyuserids = array($options->denyuserids);
}
}
// Stored file ids that will be attached when attach_files() is called.
if (!isset($options->attachfileids)) {
$info->attachfileids = array();
} else {
if (is_array($options->attachfileids)) {
$info->attachfileids = $options->attachfileids;
} else {
$info->attachfileids = array($options->attachfileids);
}
}
$record->info = serialize($info);
$record->id = $DB->insert_record('temp_mock_search_area', $record);
return $record;
}
/**
* Creates a stored file that can be added to mock search area records for indexing.
*/
public function create_file($options = null) {
// Add the searchable file fixture.
$syscontext = \context_system::instance();
$filerecord = array(
'contextid' => $syscontext->id,
'component' => 'core',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'searchfile.txt',
);
if (isset($options->filename)) {
$filerecord['filename'] = $options->filename;
}
if (isset($options->content)) {
$content = $options->content;
} else {
$content = 'File contents';
}
if (isset($options->timemodified)) {
$filerecord['timemodified'] = $options->timemodified;
}
$fs = get_file_storage();
$file = $fs->create_file_from_string($filerecord, $content);
return $file;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test iterator that skips future documents
*
* @package core_search
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
defined('MOODLE_INTERNAL') || die();
/**
* Test iterator that skips future documents
*
* @package core_search
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class skip_future_documents_iterator_test extends \basic_testcase {
/**
* Test normal case with all documents in the past.
*/
public function test_iterator_all_in_past(): void {
$past = strtotime('2017-11-01');
$documents = [
self::make_doc($past, 1),
self::make_doc($past + 1, 2),
self::make_doc($past + 2, 3)
];
$this->assertEquals('mod_x-frog-1.mod_x-frog-2.mod_x-frog-3.',
self::do_iterator($documents));
}
/**
* Confirm that the iterator does not call its parent iterator current() function too many
* times.
*/
public function test_iterator_performance(): void {
$counter = new test_counting_iterator();
$iterator = new skip_future_documents_iterator($counter);
$items = 0;
foreach ($iterator as $value) {
$this->assertEquals(false, $value);
$items++;
}
$this->assertEquals(3, $items);
$this->assertEquals(3, $counter->get_count());
}
/**
* Test with no documents at all.
*/
public function test_iterator_empty(): void {
$this->assertEquals('', self::do_iterator([]));
}
/**
* Test if some documents are in the future.
*/
public function test_iterator_some_in_future(): void {
$past = strtotime('2017-11-01');
$future = time() + 1000;
$documents = [
self::make_doc($past, 1),
self::make_doc($past + 1, 2),
self::make_doc($future, 3)
];
$this->assertEquals('mod_x-frog-1.mod_x-frog-2.',
self::do_iterator($documents));
}
/**
* Test if all documents are in the future.
*/
public function test_iterator_all_in_future(): void {
$future = time() + 1000;
$documents = [
self::make_doc($future, 1),
self::make_doc($future + 1, 2),
self::make_doc($future + 2, 3)
];
$this->assertEquals('', self::do_iterator($documents));
}
/**
* Test when some documents return error.
*/
public function test_iterator_some_false(): void {
$past = strtotime('2017-11-01');
$documents = [
self::make_doc($past, 1),
false,
self::make_doc($past + 2, 3)
];
$this->assertEquals('mod_x-frog-1.false.mod_x-frog-3.',
self::do_iterator($documents));
}
/**
* Test when all documents return error.
*/
public function test_iterator_all_false(): void {
$documents = [
false,
false,
false
];
$this->assertEquals('false.false.false.',
self::do_iterator($documents));
}
/**
* Test iterator with all cases.
*/
public function test_iterator_past_false_and_future(): void {
$past = strtotime('2017-11-01');
$future = time() + 1000;
$documents = [
false,
self::make_doc($past, 1),
false,
self::make_doc($past + 1, 2),
false,
self::make_doc($future, 3),
false
];
$this->assertEquals('false.mod_x-frog-1.false.mod_x-frog-2.false.',
self::do_iterator($documents));
}
/**
* Helper function to create a search document.
*
* @param int $time Modified time
* @param int $index Item id
* @return document Search document
*/
protected static function make_doc($time, $index) {
$doc = new document($index, 'mod_x', 'frog');
$doc->set('modified', $time);
return $doc;
}
/**
* Puts documents through the iterator and returns result as a string for easy testing.
*
* @param document[] $documents Array of documents
* @return string Documents converted to string
*/
protected static function do_iterator(array $documents) {
$parent = new \ArrayIterator($documents);
$iterator = new skip_future_documents_iterator($parent);
$result = '';
foreach ($iterator as $rec) {
if (!$rec) {
$result .= 'false.';
} else {
$result .= $rec->get('id') . '.';
}
}
return $result;
}
}
/**
* Fake iterator just for counting how many times current() is called. It returns 'false' 3 times.
*
* @package core_search
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_counting_iterator implements \Iterator {
/** @var int Current position in iterator */
protected $pos = 0;
/** @var int Number of calls to current() function */
protected $count = 0;
/**
* Returns the current element.
*
* @return mixed Can return any type.
*/
#[\ReturnTypeWillChange]
public function current() {
$this->count++;
return false;
}
/**
* Counts iterator usage.
*
* @return int Number of times current() was called
*/
public function get_count() {
return $this->count;
}
/**
* Goes on to the next element.
*/
public function next(): void {
$this->pos++;
}
/**
* Gets the key (not supported)
*
* @throws \coding_exception Always
*/
#[\ReturnTypeWillChange]
public function key() {
throw new \coding_exception('Unsupported');
}
/**
* Checks if iterato is valid (still has entries).
*
* @return bool True if still valid
*/
public function valid(): bool {
return $this->pos < 3;
}
/**
* Rewinds the iterator.
*/
public function rewind(): void {
$this->pos = 0;
}
}
+213
View File
@@ -0,0 +1,213 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_search;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/fixtures/testable_core_search.php');
require_once(__DIR__ . '/fixtures/mock_search_area.php');
/**
* Test for top results
*
* @package core_search
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class top_result_test extends \advanced_testcase {
/** @var stdClass course 1 */
protected $course1;
/** @var stdClass course 2 */
protected $course2;
/** @var stdClass user 1 */
protected $user1;
/** @var stdClass user 2 */
protected $user2;
/** @var stdClass user 3 */
protected $user3;
/** @var stdClass search engine */
protected $search;
/**
* Prepare test and users.
*/
private function prepare_test_courses_and_users(): void {
global $DB;
$this->setAdminUser();
// Search engine.
$this->search = \testable_core_search::instance(new \search_simpledb\engine());
// Set default configurations.
set_config('searchallavailablecourses', 1);
set_config('searchincludeallcourses', 1);
set_config('searchenablecategories', true);
set_config('enableglobalsearch', true);
set_config('searchmaxtopresults', 3);
$teacher = $DB->get_record('role', ['shortname' => 'teacher']);
$editingteacher = $DB->get_record('role', ['shortname' => 'editingteacher']);
set_config('searchteacherroles', "$teacher->id, $editingteacher->id");
// Generate test data.
$generator = $this->getDataGenerator();
// Courses.
$this->course1 = $generator->create_course(['fullname' => 'Top course result 1']);
// Ensure course 1 is indexed before course 2.
$this->run_index();
$this->course2 = $generator->create_course(['fullname' => 'Top course result 2']);
// User 1.
$urecord1 = new \stdClass();
$urecord1->firstname = "User 1";
$urecord1->lastname = "Test";
$this->user1 = $generator->create_user($urecord1);
// User 2.
$urecord2 = new \stdClass();
$urecord2->firstname = "User 2";
$urecord2->lastname = "Test";
$this->user2 = $generator->create_user($urecord2);
// User 3.
$urecord3 = new \stdClass();
$urecord3->firstname = "User 3";
$urecord3->lastname = "Test";
$this->user3 = $generator->create_user($urecord3);
}
/**
* Test course ranking
*/
public function test_search_course_rank(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();
$this->setUser($this->user1);
// Search query.
$data = new \stdClass();
$data->q = 'Top course result';
$data->cat = 'core-all';
// Course 1 at the first index.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('Top course result 2', $docs[1]->get('title'));
// Enrol user to course 2.
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course2->id, 'student');
// Course 2 at the first index.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 2', $docs[0]->get('title'));
$this->assertEquals('Top course result 1', $docs[1]->get('title'));
}
/**
* Test without teacher indexing
*/
public function test_search_with_no_course_teacher_indexing(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();
set_config('searchteacherroles', "");
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'teacher');
// Search query.
$data = new \stdClass();
$data->q = 'Top course result';
$data->cat = 'core-all';
// Only return the course.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertCount(2, $docs);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('Top course result 2', $docs[1]->get('title'));
}
/**
* Test with teacher indexing
*/
public function test_search_with_course_teacher_indexing(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'teacher');
$this->getDataGenerator()->enrol_user($this->user2->id, $this->course1->id, 'student');
// Search query.
$data = new \stdClass();
$data->q = 'Top course result 1';
$data->cat = 'core-all';
// Return the course and the teachers.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('User 1 Test', $docs[1]->get('title'));
}
/**
* Test with teacher indexing
*/
public function test_search_with_course_teacher_content_indexing(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();
// Create forums as course content.
$generator = $this->getDataGenerator();
// Course Teacher.
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'teacher');
// Forums.
$generator->create_module('forum',
['course' => $this->course1->id, 'name' => 'Forum 1, does not contain the keyword']);
$generator->create_module('forum',
['course' => $this->course2->id, 'name' => 'Forum 2, contains keyword Top course result 1']);
$this->run_index();
// Search query.
$data = new \stdClass();
$data->q = 'Top course result 1';
$data->cat = 'core-all';
// Return the course and the teacher and the forum.
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('User 1 Test', $docs[1]->get('title'));
$this->assertEquals('Forum 2, contains keyword Top course result 1', $docs[2]->get('title'));
}
/**
* Execute indexing
*/
private function run_index(): void {
// Indexing.
$this->waitForSecond();
$this->search->index(false, 0);
}
}