Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Search engine base unit tests.
  19   *
  20   * @package     core_search
  21   * @copyright   2017 Matt Porritt <mattp@catalyst-au.net>
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  global $CFG;
  28  require_once (__DIR__ . '/fixtures/testable_core_search.php');
  29  require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
  30  
  31  /**
  32   * Search engine base unit tests.
  33   *
  34   * @package     core_search
  35   * @copyright   2017 Matt Porritt <mattp@catalyst-au.net>
  36   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class search_base_activity_testcase extends advanced_testcase {
  39      /**
  40       * @var \core_search::manager
  41       */
  42      protected $search = null;
  43  
  44      /**
  45       * @var Instace of core_search_generator.
  46       */
  47      protected $generator = null;
  48  
  49      /**
  50       * @var Instace of testable_engine.
  51       */
  52      protected $engine = null;
  53  
  54      /** @var context[] Array of test contexts */
  55      protected $contexts;
  56  
  57      /** @var stdClass[] Array of test forum objects */
  58      protected $forums;
  59  
  60      public function setUp() {
  61          global $DB;
  62          $this->resetAfterTest();
  63          set_config('enableglobalsearch', true);
  64  
  65          // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
  66          $search = testable_core_search::instance();
  67  
  68          $this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
  69          $this->generator->setup();
  70  
  71          $this->setAdminUser();
  72  
  73          // Create course and 2 forums.
  74          $generator = $this->getDataGenerator();
  75          $course = $generator->create_course();
  76          $this->contexts['c1'] = \context_course::instance($course->id);
  77          $this->forums[1] = $generator->create_module('forum', ['course' => $course->id, 'name' => 'Forum 1',
  78                  'intro' => '<p>Intro 1</p>', 'introformat' => FORMAT_HTML]);
  79          $this->contexts['f1'] = \context_module::instance($this->forums[1]->cmid);
  80          $this->forums[2] = $generator->create_module('forum', ['course' => $course->id, 'name' => 'Forum 2',
  81                  'intro' => '<p>Intro 2</p>', 'introformat' => FORMAT_HTML]);
  82          $this->contexts['f2'] = \context_module::instance($this->forums[2]->cmid);
  83  
  84          // Create another 2 courses (in same category and in a new category) with one forum each.
  85          $this->contexts['cc1']  = \context_coursecat::instance($course->category);
  86          $course2 = $generator->create_course();
  87          $this->contexts['c2'] = \context_course::instance($course2->id);
  88          $this->forums[3] = $generator->create_module('forum', ['course' => $course2->id, 'name' => 'Forum 3',
  89                  'intro' => '<p>Intro 3</p>', 'introformat' => FORMAT_HTML]);
  90          $this->contexts['f3'] = \context_module::instance($this->forums[3]->cmid);
  91          $cat2 = $generator->create_category();
  92          $this->contexts['cc2'] = \context_coursecat::instance($cat2->id);
  93          $course3 = $generator->create_course(['category' => $cat2->id]);
  94          $this->contexts['c3'] = \context_course::instance($course3->id);
  95          $this->forums[4] = $generator->create_module('forum', ['course' => $course3->id, 'name' => 'Forum 4',
  96                  'intro' => '<p>Intro 4</p>', 'introformat' => FORMAT_HTML]);
  97          $this->contexts['f4'] = \context_module::instance($this->forums[4]->cmid);
  98  
  99          // Hack about with the time modified values.
 100          foreach ($this->forums as $index => $forum) {
 101              $DB->set_field('forum', 'timemodified', $index, ['id' => $forum->id]);
 102          }
 103      }
 104  
 105      public function tearDown() {
 106          // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
 107          if ($this->generator) {
 108              // Moodle DML freaks out if we don't teardown the temp table after each run.
 109              $this->generator->teardown();
 110              $this->generator = null;
 111          }
 112      }
 113  
 114      /**
 115       * Test base activity get search fileareas
 116       */
 117      public function test_get_search_fileareas_base() {
 118  
 119          $builder = $this->getMockBuilder('\core_search\base_activity');
 120          $builder->disableOriginalConstructor();
 121          $stub = $builder->getMockForAbstractClass();
 122  
 123          $result = $stub->get_search_fileareas();
 124  
 125          $this->assertEquals(array('intro'), $result);
 126      }
 127  
 128      /**
 129       * Test base attach files
 130       */
 131      public function test_attach_files_base() {
 132          $filearea = 'intro';
 133          $component = 'mod_forum';
 134          $module = 'forum';
 135  
 136          $course = self::getDataGenerator()->create_course();
 137          $activity = self::getDataGenerator()->create_module('forum', array('course' => $course->id));
 138          $context = \context_module::instance($activity->cmid);
 139          $contextid = $context->id;
 140  
 141          // Create file to add.
 142          $fs = get_file_storage();
 143          $filerecord = array(
 144                  'contextid' => $contextid,
 145                  'component' => $component,
 146                  'filearea' => $filearea,
 147                  'itemid' => 0,
 148                  'filepath' => '/',
 149                  'filename' => 'testfile.txt');
 150          $content = 'All the news that\'s fit to print';
 151          $file = $fs->create_file_from_string($filerecord, $content);
 152  
 153          // Construct the search document.
 154          $rec = new \stdClass();
 155          $rec->courseid = $course->id;
 156          $area = new core_mocksearch\search\mock_search_area();
 157          $record = $this->generator->create_record($rec);
 158  
 159          $document = $area->get_document($record);
 160          $document->set('itemid', $activity->id);
 161  
 162          // Create a mock from the abstract class,
 163          // with required methods stubbed.
 164          $builder = $this->getMockBuilder('\core_search\base_activity');
 165          $builder->disableOriginalConstructor();
 166          $builder->setMethods(array('get_module_name', 'get_component_name'));
 167          $stub = $builder->getMockForAbstractClass();
 168          $stub->method('get_module_name')->willReturn($module);
 169          $stub->method('get_component_name')->willReturn($component);
 170  
 171          // Attach file to our test document.
 172          $stub->attach_files($document);
 173  
 174          // Verify file is attached.
 175          $files = $document->get_files();
 176          $file = array_values($files)[0];
 177  
 178          $this->assertEquals(1, count($files));
 179          $this->assertEquals($content, $file->get_content());
 180      }
 181  
 182      /**
 183       * Tests getting the recordset.
 184       */
 185      public function test_get_document_recordset() {
 186          global $USER, $DB;
 187  
 188          // Get all the forums to index (no restriction).
 189          $area = new mod_forum\search\activity();
 190          $results = self::recordset_to_indexed_array($area->get_document_recordset());
 191  
 192          // Should return all forums.
 193          $this->assertCount(4, $results);
 194  
 195          // Each result should basically have the contents of the forum table. We'll just check
 196          // the key fields for the first one and then the other ones by id only.
 197          $this->assertEquals($this->forums[1]->id, $results[0]->id);
 198          $this->assertEquals(1, $results[0]->timemodified);
 199          $this->assertEquals($this->forums[1]->course, $results[0]->course);
 200          $this->assertEquals('Forum 1', $results[0]->name);
 201          $this->assertEquals('<p>Intro 1</p>', $results[0]->intro);
 202          $this->assertEquals(FORMAT_HTML, $results[0]->introformat);
 203  
 204          $allids = self::records_to_ids($this->forums);
 205          $this->assertEquals($allids, self::records_to_ids($results));
 206  
 207          // Repeat with a time restriction.
 208          $results = self::recordset_to_indexed_array($area->get_document_recordset(3));
 209          $this->assertEquals([$this->forums[3]->id, $this->forums[4]->id],
 210                  self::records_to_ids($results));
 211  
 212          // Now use context restrictions. First, the whole site (no change).
 213          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 214                  0, context_system::instance()));
 215          $this->assertEquals($allids, self::records_to_ids($results));
 216  
 217          // Course 1 only.
 218          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 219                  0, $this->contexts['c1']));
 220          $this->assertEquals([$this->forums[1]->id, $this->forums[2]->id],
 221                  self::records_to_ids($results));
 222  
 223          // Course 2 only.
 224          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 225                  0, $this->contexts['c2']));
 226          $this->assertEquals([$this->forums[3]->id], self::records_to_ids($results));
 227  
 228          // Specific forum only.
 229          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 230                  0, $this->contexts['f4']));
 231          $this->assertEquals([$this->forums[4]->id], self::records_to_ids($results));
 232  
 233          // Category 1 context (courses 1 and 2).
 234          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 235                  0, $this->contexts['cc1']));
 236          $this->assertEquals([$this->forums[1]->id, $this->forums[2]->id, $this->forums[3]->id],
 237                  self::records_to_ids($results));
 238  
 239          // Category 2 context (course 3).
 240          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 241                  0, $this->contexts['cc2']));
 242          $this->assertEquals([$this->forums[4]->id], self::records_to_ids($results));
 243  
 244          // Combine context restriction (category 1) with timemodified.
 245          $results = self::recordset_to_indexed_array($area->get_document_recordset(
 246                  2, $this->contexts['cc1']));
 247          $this->assertEquals([$this->forums[2]->id, $this->forums[3]->id],
 248                  self::records_to_ids($results));
 249  
 250          // Find an arbitrary block on the system to get a block context.
 251          $blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
 252          $blockcontext = context_block::instance($blockid);
 253  
 254          // Block context (cannot return anything, so always null).
 255          $this->assertNull($area->get_document_recordset(0, $blockcontext));
 256  
 257          // User context (cannot return anything, so always null).
 258          $usercontext = context_user::instance($USER->id);
 259          $this->assertNull($area->get_document_recordset(0, $usercontext));
 260      }
 261  
 262      /**
 263       * Utility function to convert recordset to array for testing.
 264       *
 265       * @param moodle_recordset $rs Recordset to convert
 266       * @return array Array indexed by number (0, 1, 2, ...)
 267       */
 268      protected static function recordset_to_indexed_array(moodle_recordset $rs) {
 269          $results = [];
 270          foreach ($rs as $rec) {
 271              $results[] = $rec;
 272          }
 273          $rs->close();
 274          return $results;
 275      }
 276  
 277      /**
 278       * Utility function to convert records to array of IDs.
 279       *
 280       * @param array $recs Records which should have an 'id' field
 281       * @return array Array of ids
 282       */
 283      protected static function records_to_ids(array $recs) {
 284          $ids = [];
 285          foreach ($recs as $rec) {
 286              $ids[] = $rec->id;
 287          }
 288          return $ids;
 289      }
 290  
 291      /**
 292       * Tests the get_doc_url function.
 293       */
 294      public function test_get_doc_url() {
 295          $area = new mod_forum\search\activity();
 296          $results = self::recordset_to_indexed_array($area->get_document_recordset());
 297  
 298          for ($i = 0; $i < 4; $i++) {
 299              $this->assertEquals(new moodle_url('/mod/forum/view.php',
 300                      ['id' => $this->forums[$i + 1]->cmid]),
 301                      $area->get_doc_url($area->get_document($results[$i])));
 302          }
 303      }
 304  
 305      /**
 306       * Tests the check_access function.
 307       */
 308      public function test_check_access() {
 309          global $CFG;
 310          require_once($CFG->dirroot . '/course/lib.php');
 311  
 312          // Create a test user who can access courses 1 and 2 (everything except forum 4).
 313          $generator = $this->getDataGenerator();
 314          $user = $generator->create_user();
 315          $generator->enrol_user($user->id, $this->forums[1]->course, 'student');
 316          $generator->enrol_user($user->id, $this->forums[3]->course, 'student');
 317          $this->setUser($user);
 318  
 319          // Delete forum 2 and set forum 3 hidden.
 320          course_delete_module($this->forums[2]->cmid);
 321          set_coursemodule_visible($this->forums[3]->cmid, 0);
 322  
 323          // Call check access on all the first three.
 324          $area = new mod_forum\search\activity();
 325          $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $area->check_access(
 326                  $this->forums[1]->id));
 327          $this->assertEquals(\core_search\manager::ACCESS_DELETED, $area->check_access(
 328                  $this->forums[2]->id));
 329          $this->assertEquals(\core_search\manager::ACCESS_DENIED, $area->check_access(
 330                  $this->forums[3]->id));
 331  
 332          // Note: Do not check forum 4 which is in a course the user can't access; this will return
 333          // ACCESS_GRANTED, but it does not matter because the search engine will not have included
 334          // that context in the list to search. (This is because the $cm->uservisible access flag
 335          // is only valid if the user is known to be able to access the course.)
 336      }
 337  
 338      /**
 339       * Tests the module version of get_contexts_to_reindex, which is supposed to return all the
 340       * activity contexts in order of date added.
 341       */
 342      public function test_get_contexts_to_reindex() {
 343          global $DB;
 344  
 345          $this->resetAfterTest();
 346  
 347          // Set up a course with two URLs and a Page.
 348          $generator = $this->getDataGenerator();
 349          $course = $generator->create_course(['fullname' => 'TCourse']);
 350          $url1 = $generator->create_module('url', ['course' => $course->id, 'name' => 'TURL1']);
 351          $url2 = $generator->create_module('url', ['course' => $course->id, 'name' => 'TURL2']);
 352          $page = $generator->create_module('page', ['course' => $course->id, 'name' => 'TPage1']);
 353  
 354          // Hack the items so they have different added times.
 355          $now = time();
 356          $DB->set_field('course_modules', 'added', $now - 3, ['id' => $url2->cmid]);
 357          $DB->set_field('course_modules', 'added', $now - 2, ['id' => $url1->cmid]);
 358          $DB->set_field('course_modules', 'added', $now - 1, ['id' => $page->cmid]);
 359  
 360          // Check the URL contexts are in date order.
 361          $urlarea = new \mod_url\search\activity();
 362          $contexts = iterator_to_array($urlarea->get_contexts_to_reindex(), false);
 363          $this->assertEquals([\context_module::instance($url1->cmid),
 364                  \context_module::instance($url2->cmid)], $contexts);
 365  
 366          // Check the Page contexts.
 367          $pagearea = new \mod_page\search\activity();
 368          $contexts = iterator_to_array($pagearea->get_contexts_to_reindex(), false);
 369          $this->assertEquals([\context_module::instance($page->cmid)], $contexts);
 370  
 371          // Check another module area that has no instances.
 372          $glossaryarea = new \mod_glossary\search\activity();
 373          $contexts = iterator_to_array($glossaryarea->get_contexts_to_reindex(), false);
 374          $this->assertEquals([], $contexts);
 375      }
 376  
 377      /**
 378       * Test document icon.
 379       */
 380      public function test_get_doc_icon() {
 381          $baseactivity = $this->getMockBuilder('\core_search\base_activity')
 382              ->disableOriginalConstructor()
 383              ->setMethods(array('get_module_name'))
 384              ->getMockForAbstractClass();
 385  
 386          $baseactivity->method('get_module_name')->willReturn('test_activity');
 387  
 388          $document = $this->getMockBuilder('\core_search\document')
 389              ->disableOriginalConstructor()
 390              ->getMock();
 391  
 392          $result = $baseactivity->get_doc_icon($document);
 393  
 394          $this->assertEquals('icon', $result->get_name());
 395          $this->assertEquals('test_activity', $result->get_component());
 396      }
 397  }