Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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 manager unit tests.
  19   *
  20   * @package     core_search
  21   * @category    phpunit
  22   * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
  23   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once (__DIR__ . '/fixtures/testable_core_search.php');
  29  require_once (__DIR__ . '/fixtures/mock_search_area.php');
  30  
  31  /**
  32   * Unit tests for search manager.
  33   *
  34   * @package     core_search
  35   * @category    phpunit
  36   * @copyright   2015 David Monllao {@link http://www.davidmonllao.com}
  37   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class search_manager_testcase extends advanced_testcase {
  40  
  41      /**
  42       * Forum area id.
  43       *
  44       * @var string
  45       */
  46  
  47      protected $forumpostareaid = null;
  48  
  49      /**
  50       * Courses area id.
  51       *
  52       * @var string
  53       */
  54      protected $coursesareaid = null;
  55  
  56      public function setUp(): void {
  57          $this->forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
  58          $this->coursesareaid = \core_search\manager::generate_areaid('core_course', 'course');
  59      }
  60  
  61      protected function tearDown(): void {
  62          // Stop it from faking time in the search manager (if set by test).
  63          testable_core_search::fake_current_time();
  64          parent::tearDown();
  65      }
  66  
  67      public function test_search_enabled() {
  68  
  69          $this->resetAfterTest();
  70  
  71          // Disabled by default.
  72          $this->assertFalse(\core_search\manager::is_global_search_enabled());
  73  
  74          set_config('enableglobalsearch', true);
  75          $this->assertTrue(\core_search\manager::is_global_search_enabled());
  76  
  77          set_config('enableglobalsearch', false);
  78          $this->assertFalse(\core_search\manager::is_global_search_enabled());
  79      }
  80  
  81      public function test_course_search_url() {
  82  
  83          $this->resetAfterTest();
  84  
  85          // URL is course/search.php by default.
  86          $this->assertEquals(new moodle_url("/course/search.php"), \core_search\manager::get_course_search_url());
  87  
  88          set_config('enableglobalsearch', true);
  89          $this->assertEquals(new moodle_url("/search/index.php"), \core_search\manager::get_course_search_url());
  90  
  91          set_config('enableglobalsearch', false);
  92          $this->assertEquals(new moodle_url("/course/search.php"), \core_search\manager::get_course_search_url());
  93      }
  94  
  95      public function test_search_areas() {
  96          global $CFG;
  97  
  98          $this->resetAfterTest();
  99  
 100          set_config('enableglobalsearch', true);
 101  
 102          $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
 103  
 104          $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
 105          $this->assertInstanceOf('\core_search\base', $searcharea);
 106  
 107          $this->assertFalse(\core_search\manager::get_search_area($fakeareaid));
 108  
 109          $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list());
 110          $this->assertArrayNotHasKey($fakeareaid, \core_search\manager::get_search_areas_list());
 111  
 112          // Enabled by default once global search is enabled.
 113          $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
 114  
 115          list($componentname, $varname) = $searcharea->get_config_var_name();
 116          set_config($varname . '_enabled', 0, $componentname);
 117          \core_search\manager::clear_static();
 118  
 119          $this->assertArrayNotHasKey('mod_forum', \core_search\manager::get_search_areas_list(true));
 120  
 121          set_config($varname . '_enabled', 1, $componentname);
 122  
 123          // Although the result is wrong, we want to check that \core_search\manager::get_search_areas_list returns cached results.
 124          $this->assertArrayNotHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
 125  
 126          // Now we check the real result.
 127          \core_search\manager::clear_static();
 128          $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
 129      }
 130  
 131      public function test_search_config() {
 132  
 133          $this->resetAfterTest();
 134  
 135          $search = testable_core_search::instance();
 136  
 137          // We should test both plugin types and core subsystems. No core subsystems available yet.
 138          $searcharea = $search->get_search_area($this->forumpostareaid);
 139  
 140          list($componentname, $varname) = $searcharea->get_config_var_name();
 141  
 142          // Just with a couple of vars should be enough.
 143          $start = time() - 100;
 144          $end = time();
 145          set_config($varname . '_indexingstart', $start, $componentname);
 146          set_config($varname . '_indexingend', $end, $componentname);
 147  
 148          $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
 149          $this->assertEquals($start, $configs[$this->forumpostareaid]->indexingstart);
 150          $this->assertEquals($end, $configs[$this->forumpostareaid]->indexingend);
 151          $this->assertEquals(false, $configs[$this->forumpostareaid]->partial);
 152  
 153          try {
 154              $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
 155              $search->reset_config($fakeareaid);
 156              $this->fail('An exception should be triggered if the provided search area does not exist.');
 157          } catch (moodle_exception $ex) {
 158              $this->assertStringContainsString($fakeareaid . ' search area is not available.', $ex->getMessage());
 159          }
 160  
 161          // We clean it all but enabled components.
 162          $search->reset_config($this->forumpostareaid);
 163          $config = $searcharea->get_config();
 164          $this->assertEquals(1, $config[$varname . '_enabled']);
 165          $this->assertEquals(0, $config[$varname . '_indexingstart']);
 166          $this->assertEquals(0, $config[$varname . '_indexingend']);
 167          $this->assertEquals(0, $config[$varname . '_lastindexrun']);
 168          $this->assertEquals(0, $config[$varname . '_partial']);
 169          // No caching.
 170          $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
 171          $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
 172          $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
 173  
 174          set_config($varname . '_indexingstart', $start, $componentname);
 175          set_config($varname . '_indexingend', $end, $componentname);
 176  
 177          // All components config should be reset.
 178          $search->reset_config();
 179          $this->assertEquals(0, get_config($componentname, $varname . '_indexingstart'));
 180          $this->assertEquals(0, get_config($componentname, $varname . '_indexingend'));
 181          $this->assertEquals(0, get_config($componentname, $varname . '_lastindexrun'));
 182          // No caching.
 183          $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
 184          $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
 185          $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
 186      }
 187  
 188      /**
 189       * Tests the get_last_indexing_duration method in the base area class.
 190       */
 191      public function test_get_last_indexing_duration() {
 192          $this->resetAfterTest();
 193  
 194          $search = testable_core_search::instance();
 195  
 196          $searcharea = $search->get_search_area($this->forumpostareaid);
 197  
 198          // When never indexed, the duration is false.
 199          $this->assertSame(false, $searcharea->get_last_indexing_duration());
 200  
 201          // Set the start/end times.
 202          list($componentname, $varname) = $searcharea->get_config_var_name();
 203          $start = time() - 100;
 204          $end = time();
 205          set_config($varname . '_indexingstart', $start, $componentname);
 206          set_config($varname . '_indexingend', $end, $componentname);
 207  
 208          // The duration should now be 100.
 209          $this->assertSame(100, $searcharea->get_last_indexing_duration());
 210      }
 211  
 212      /**
 213       * Tests that partial indexing works correctly.
 214       */
 215      public function test_partial_indexing() {
 216          global $USER;
 217  
 218          $this->resetAfterTest();
 219          $this->setAdminUser();
 220  
 221          // Create a course and a forum.
 222          $generator = $this->getDataGenerator();
 223          $course = $generator->create_course();
 224          $forum = $generator->create_module('forum', ['course' => $course->id]);
 225  
 226          // Index everything up to current. Ensure the course is older than current second so it
 227          // definitely doesn't get indexed again next time.
 228          $this->waitForSecond();
 229          $search = testable_core_search::instance();
 230          $search->index(false, 0);
 231  
 232          $searcharea = $search->get_search_area($this->forumpostareaid);
 233          list($componentname, $varname) = $searcharea->get_config_var_name();
 234          $this->assertFalse(get_config($componentname, $varname . '_partial'));
 235  
 236          // Add 3 discussions to the forum.
 237          $now = time();
 238          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 239                  'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now,
 240                  'name' => 'Frog']);
 241          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 242                  'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
 243                  'name' => 'Toad']);
 244          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 245                  'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
 246                  'name' => 'Zombie']);
 247          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 248                  'forum' => $forum->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
 249                  'name' => 'Werewolf']);
 250          time_sleep_until($now + 3);
 251  
 252          // Clear the count of added documents.
 253          $search->get_engine()->get_and_clear_added_documents();
 254  
 255          // Make the search engine delay while indexing each document.
 256          $search->get_engine()->set_add_delay(1.2);
 257  
 258          // Use fake time, starting from now.
 259          testable_core_search::fake_current_time(time());
 260  
 261          // Index with a limit of 2 seconds - it should index 2 of the documents (after the second
 262          // one, it will have taken 2.4 seconds so it will stop).
 263          $search->index(false, 2);
 264          $added = $search->get_engine()->get_and_clear_added_documents();
 265          $this->assertCount(2, $added);
 266          $this->assertEquals('Frog', $added[0]->get('title'));
 267          $this->assertEquals('Toad', $added[1]->get('title'));
 268          $this->assertEquals(1, get_config($componentname, $varname . '_partial'));
 269          // Whilst 2.4 seconds of "time" have elapsed, the indexing duration is
 270          // measured in seconds, so should be 2.
 271          $this->assertEquals(2, $searcharea->get_last_indexing_duration());
 272  
 273          // Add a label.
 274          $generator->create_module('label', ['course' => $course->id, 'intro' => 'Vampire']);
 275  
 276          // Wait to next second (so as to not reindex the label more than once, as it will now
 277          // be timed before the indexing run).
 278          $this->waitForSecond();
 279          testable_core_search::fake_current_time(time());
 280  
 281          // Next index with 1 second limit should do the label and not the forum - the logic is,
 282          // if it spent ages indexing an area last time, do that one last on next run.
 283          $search->index(false, 1);
 284          $added = $search->get_engine()->get_and_clear_added_documents();
 285          $this->assertCount(1, $added);
 286          $this->assertEquals('Vampire', $added[0]->get('title'));
 287  
 288          // Index again with a 3 second limit - it will redo last post for safety (because of other
 289          // things possibly having the same time second), and then do the remaining one. (Note:
 290          // because it always does more than one second worth of items, it would actually index 2
 291          // posts even if the limit were less than 2, we are testing it does 3 posts to make sure
 292          // the time limiting is actually working with the specified time.)
 293          $search->index(false, 3);
 294          $added = $search->get_engine()->get_and_clear_added_documents();
 295          $this->assertCount(3, $added);
 296          $this->assertEquals('Toad', $added[0]->get('title'));
 297          $remainingtitles = [$added[1]->get('title'), $added[2]->get('title')];
 298          sort($remainingtitles);
 299          $this->assertEquals(['Werewolf', 'Zombie'], $remainingtitles);
 300          $this->assertFalse(get_config($componentname, $varname . '_partial'));
 301  
 302          // Index again - there should be nothing to index this time.
 303          $search->index(false, 2);
 304          $added = $search->get_engine()->get_and_clear_added_documents();
 305          $this->assertCount(0, $added);
 306          $this->assertFalse(get_config($componentname, $varname . '_partial'));
 307      }
 308  
 309      /**
 310       * Tests the progress display while indexing.
 311       *
 312       * This tests the different logic about displaying progress for slow/fast and
 313       * complete/incomplete processing.
 314       */
 315      public function test_index_progress() {
 316          $this->resetAfterTest();
 317          $generator = $this->getDataGenerator();
 318  
 319          // Set up the fake search area.
 320          $search = testable_core_search::instance();
 321          $area = new \core_mocksearch\search\mock_search_area();
 322          $search->add_search_area('whatever', $area);
 323          $searchgenerator = $generator->get_plugin_generator('core_search');
 324          $searchgenerator->setUp();
 325  
 326          // Add records with specific time modified values.
 327          $time = strtotime('2017-11-01 01:00');
 328          for ($i = 0; $i < 8; $i ++) {
 329              $searchgenerator->create_record((object)['timemodified' => $time]);
 330              $time += 60;
 331          }
 332  
 333          // Simulate slow progress on indexing and initial query.
 334          $now = strtotime('2017-11-11 01:00');
 335          \testable_core_search::fake_current_time($now);
 336          $area->set_indexing_delay(10.123);
 337          $search->get_engine()->set_add_delay(15.789);
 338  
 339          // Run search indexing and check output.
 340          $progress = new progress_trace_buffer(new text_progress_trace(), false);
 341          $search->index(false, 75, $progress);
 342          $out = $progress->get_buffer();
 343          $progress->reset_buffer();
 344  
 345          // Check for the standard text.
 346          $this->assertStringContainsString('Processing area: Mock search area', $out);
 347          $this->assertStringContainsString('Stopping indexing due to time limit', $out);
 348  
 349          // Check for initial query performance indication.
 350          $this->assertStringContainsString('Initial query took 10.1 seconds.', $out);
 351  
 352          // Check for the two (approximately) every-30-seconds messages.
 353          $this->assertStringContainsString('01:00:41: Done to 1/11/17, 01:01', $out);
 354          $this->assertStringContainsString('01:01:13: Done to 1/11/17, 01:03', $out);
 355  
 356          // Check for the 'not complete' indicator showing when it was done until.
 357          $this->assertStringContainsString('Processed 5 records containing 5 documents, in 89.1 seconds ' .
 358                  '(not complete; done to 1/11/17, 01:04)', $out);
 359  
 360          // Make the initial query delay less than 5 seconds, so it won't appear. Make the documents
 361          // quicker, so that the 30-second delay won't be needed.
 362          $area->set_indexing_delay(4.9);
 363          $search->get_engine()->set_add_delay(1);
 364  
 365          // Run search indexing (still partial) and check output.
 366          $progress = new progress_trace_buffer(new text_progress_trace(), false);
 367          $search->index(false, 5, $progress);
 368          $out = $progress->get_buffer();
 369          $progress->reset_buffer();
 370  
 371          $this->assertStringContainsString('Processing area: Mock search area', $out);
 372          $this->assertStringContainsString('Stopping indexing due to time limit', $out);
 373          $this->assertStringNotContainsString('Initial query took', $out);
 374          $this->assertStringNotContainsString(': Done to', $out);
 375          $this->assertStringContainsString('Processed 2 records containing 2 documents, in 6.9 seconds ' .
 376                  '(not complete; done to 1/11/17, 01:05).', $out);
 377  
 378          // Run the remaining items to complete it.
 379          $progress = new progress_trace_buffer(new text_progress_trace(), false);
 380          $search->index(false, 100, $progress);
 381          $out = $progress->get_buffer();
 382          $progress->reset_buffer();
 383  
 384          $this->assertStringContainsString('Processing area: Mock search area', $out);
 385          $this->assertStringNotContainsString('Stopping indexing due to time limit', $out);
 386          $this->assertStringNotContainsString('Initial query took', $out);
 387          $this->assertStringNotContainsString(': Done to', $out);
 388          $this->assertStringContainsString('Processed 3 records containing 3 documents, in 7.9 seconds.', $out);
 389  
 390          $searchgenerator->tearDown();
 391      }
 392  
 393      /**
 394       * Tests that documents with modified time in the future are NOT indexed (as this would cause
 395       * a problem by preventing it from indexing other documents modified between now and the future
 396       * date).
 397       */
 398      public function test_future_documents() {
 399          $this->resetAfterTest();
 400  
 401          // Create a course and a forum.
 402          $generator = $this->getDataGenerator();
 403          $course = $generator->create_course();
 404          $forum = $generator->create_module('forum', ['course' => $course->id]);
 405  
 406          // Index everything up to current. Ensure the course is older than current second so it
 407          // definitely doesn't get indexed again next time.
 408          $this->waitForSecond();
 409          $search = testable_core_search::instance();
 410          $search->index(false, 0);
 411          $search->get_engine()->get_and_clear_added_documents();
 412  
 413          // Add 2 discussions to the forum, one of which happend just now, but the other is
 414          // incorrectly set to the future.
 415          $now = time();
 416          $userid = get_admin()->id;
 417          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 418                  'forum' => $forum->id, 'userid' => $userid, 'timemodified' => $now,
 419                  'name' => 'Frog']);
 420          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 421                  'forum' => $forum->id, 'userid' => $userid, 'timemodified' => $now + 100,
 422                  'name' => 'Toad']);
 423  
 424          // Wait for a second so we're not actually on the same second as the forum post (there's a
 425          // 1 second overlap between indexing; it would get indexed in both checks below otherwise).
 426          $this->waitForSecond();
 427  
 428          // Index.
 429          $search->index(false);
 430          $added = $search->get_engine()->get_and_clear_added_documents();
 431          $this->assertCount(1, $added);
 432          $this->assertEquals('Frog', $added[0]->get('title'));
 433  
 434          // Check latest time - it should be the same as $now, not the + 100.
 435          $searcharea = $search->get_search_area($this->forumpostareaid);
 436          list($componentname, $varname) = $searcharea->get_config_var_name();
 437          $this->assertEquals($now, get_config($componentname, $varname . '_lastindexrun'));
 438  
 439          // Index again - there should be nothing to index this time.
 440          $search->index(false);
 441          $added = $search->get_engine()->get_and_clear_added_documents();
 442          $this->assertCount(0, $added);
 443      }
 444  
 445      /**
 446       * Tests that indexing a specified context works correctly.
 447       */
 448      public function test_context_indexing() {
 449          global $USER;
 450  
 451          $this->resetAfterTest();
 452          $this->setAdminUser();
 453  
 454          // Create a course and two forums and a page.
 455          $generator = $this->getDataGenerator();
 456          $course = $generator->create_course();
 457          $now = time();
 458          $forum1 = $generator->create_module('forum', ['course' => $course->id]);
 459          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 460                  'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now,
 461                  'name' => 'Frog']);
 462          $this->waitForSecond();
 463          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 464                  'forum' => $forum1->id, 'userid' => $USER->id, 'timemodified' => $now + 2,
 465                  'name' => 'Zombie']);
 466          $forum2 = $generator->create_module('forum', ['course' => $course->id]);
 467          $this->waitForSecond();
 468          $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id,
 469                  'forum' => $forum2->id, 'userid' => $USER->id, 'timemodified' => $now + 1,
 470                  'name' => 'Toad']);
 471          $generator->create_module('page', ['course' => $course->id]);
 472          $generator->create_module('forum', ['course' => $course->id]);
 473  
 474          // Index forum 1 only.
 475          $search = testable_core_search::instance();
 476          $buffer = new progress_trace_buffer(new text_progress_trace(), false);
 477          $result = $search->index_context(\context_module::instance($forum1->cmid), '', 0, $buffer);
 478          $this->assertTrue($result->complete);
 479          $log = $buffer->get_buffer();
 480          $buffer->reset_buffer();
 481  
 482          // Confirm that output only processed 1 forum activity and 2 posts.
 483          $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 1 "));
 484          $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
 485  
 486          // Confirm that some areas for different types of context were skipped.
 487          $this->assertNotFalse(strpos($log, "area: Users\n  Skipping"));
 488          $this->assertNotFalse(strpos($log, "area: Courses\n  Skipping"));
 489  
 490          // Confirm that another module area had no results.
 491          $this->assertNotFalse(strpos($log, "area: Page\n  No documents"));
 492  
 493          // Index whole course.
 494          $result = $search->index_context(\context_course::instance($course->id), '', 0, $buffer);
 495          $this->assertTrue($result->complete);
 496          $log = $buffer->get_buffer();
 497          $buffer->reset_buffer();
 498  
 499          // Confirm that output processed 3 forum activities and 3 posts.
 500          $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 3 "));
 501          $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
 502  
 503          // The course area was also included this time.
 504          $this->assertNotFalse(strpos($log, "area: Courses\n  Processed 1 "));
 505  
 506          // Confirm that another module area had results too.
 507          $this->assertNotFalse(strpos($log, "area: Page\n  Processed 1 "));
 508  
 509          // Index whole course, but only forum posts.
 510          $result = $search->index_context(\context_course::instance($course->id), 'mod_forum-post',
 511                  0, $buffer);
 512          $this->assertTrue($result->complete);
 513          $log = $buffer->get_buffer();
 514          $buffer->reset_buffer();
 515  
 516          // Confirm that output processed 3 posts but not forum activities.
 517          $this->assertFalse(strpos($log, "area: Forum - activity information"));
 518          $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 3 "));
 519  
 520          // Set time limit and retry index of whole course, taking 3 tries to complete it.
 521          $search->get_engine()->set_add_delay(0.4);
 522          $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer);
 523          $log = $buffer->get_buffer();
 524          $buffer->reset_buffer();
 525          $this->assertFalse($result->complete);
 526          $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
 527  
 528          $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
 529                  $result->startfromarea, $result->startfromtime);
 530          $log = $buffer->get_buffer();
 531          $buffer->reset_buffer();
 532          $this->assertNotFalse(strpos($log, "area: Forum - activity information\n  Processed 2 "));
 533          $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
 534          $this->assertFalse($result->complete);
 535  
 536          $result = $search->index_context(\context_course::instance($course->id), '', 1, $buffer,
 537                  $result->startfromarea, $result->startfromtime);
 538          $log = $buffer->get_buffer();
 539          $buffer->reset_buffer();
 540          $this->assertNotFalse(strpos($log, "area: Forum - posts\n  Processed 2 "));
 541          $this->assertTrue($result->complete);
 542      }
 543  
 544      /**
 545       * Adding this test here as get_areas_user_accesses process is the same, results just depend on the context level.
 546       *
 547       * @return void
 548       */
 549      public function test_search_user_accesses() {
 550          global $DB;
 551  
 552          $this->resetAfterTest();
 553  
 554          $frontpage = $DB->get_record('course', array('id' => SITEID));
 555          $frontpagectx = context_course::instance($frontpage->id);
 556          $course1 = $this->getDataGenerator()->create_course();
 557          $course1ctx = context_course::instance($course1->id);
 558          $course2 = $this->getDataGenerator()->create_course();
 559          $course2ctx = context_course::instance($course2->id);
 560          $course3 = $this->getDataGenerator()->create_course();
 561          $course3ctx = context_course::instance($course3->id);
 562          $teacher = $this->getDataGenerator()->create_user();
 563          $teacherctx = context_user::instance($teacher->id);
 564          $student = $this->getDataGenerator()->create_user();
 565          $studentctx = context_user::instance($student->id);
 566          $noaccess = $this->getDataGenerator()->create_user();
 567          $noaccessctx = context_user::instance($noaccess->id);
 568          $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, 'teacher');
 569          $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
 570  
 571          $frontpageforum = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
 572          $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
 573          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
 574          $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 575          $frontpageforumcontext = context_module::instance($frontpageforum->cmid);
 576          $context1 = context_module::instance($forum1->cmid);
 577          $context2 = context_module::instance($forum2->cmid);
 578          $context3 = context_module::instance($forum3->cmid);
 579          $forum4 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id));
 580          $context4 = context_module::instance($forum4->cmid);
 581  
 582          $search = testable_core_search::instance();
 583          $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
 584          $search->add_core_search_areas();
 585          $search->add_search_area($mockareaid, new core_mocksearch\search\mock_search_area());
 586  
 587          $this->setAdminUser();
 588          $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
 589  
 590          $sitectx = \context_course::instance(SITEID);
 591  
 592          // Can access the frontpage ones.
 593          $this->setUser($noaccess);
 594          $contexts = $search->get_areas_user_accesses()->usercontexts;
 595          $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id), $contexts[$this->forumpostareaid]);
 596          $this->assertEquals(array($sitectx->id => $sitectx->id), $contexts[$this->coursesareaid]);
 597          $mockctxs = array($noaccessctx->id => $noaccessctx->id, $frontpagectx->id => $frontpagectx->id);
 598          $this->assertEquals($mockctxs, $contexts[$mockareaid]);
 599  
 600          $this->setUser($teacher);
 601          $contexts = $search->get_areas_user_accesses()->usercontexts;
 602          $frontpageandcourse1 = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
 603              $context2->id => $context2->id);
 604          $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
 605          $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
 606              $contexts[$this->coursesareaid]);
 607          $mockctxs = array($teacherctx->id => $teacherctx->id,
 608                  $frontpagectx->id => $frontpagectx->id, $course1ctx->id => $course1ctx->id);
 609          $this->assertEquals($mockctxs, $contexts[$mockareaid]);
 610  
 611          $this->setUser($student);
 612          $contexts = $search->get_areas_user_accesses()->usercontexts;
 613          $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
 614          $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
 615              $contexts[$this->coursesareaid]);
 616          $mockctxs = array($studentctx->id => $studentctx->id,
 617                  $frontpagectx->id => $frontpagectx->id, $course1ctx->id => $course1ctx->id);
 618          $this->assertEquals($mockctxs, $contexts[$mockareaid]);
 619  
 620          // Hide the activity.
 621          set_coursemodule_visible($forum2->cmid, 0);
 622          $contexts = $search->get_areas_user_accesses()->usercontexts;
 623          $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id),
 624              $contexts[$this->forumpostareaid]);
 625  
 626          // Now test course limited searches.
 627          set_coursemodule_visible($forum2->cmid, 1);
 628          $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student');
 629          $contexts = $search->get_areas_user_accesses()->usercontexts;
 630          $allcontexts = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
 631              $context2->id => $context2->id, $context3->id => $context3->id);
 632          $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
 633          $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id,
 634              $course2ctx->id => $course2ctx->id), $contexts[$this->coursesareaid]);
 635  
 636          $contexts = $search->get_areas_user_accesses(array($course1->id, $course2->id))->usercontexts;
 637          $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id, $context3->id => $context3->id);
 638          $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
 639          $this->assertEquals(array($course1ctx->id => $course1ctx->id,
 640              $course2ctx->id => $course2ctx->id), $contexts[$this->coursesareaid]);
 641  
 642          $contexts = $search->get_areas_user_accesses(array($course2->id))->usercontexts;
 643          $allcontexts = array($context3->id => $context3->id);
 644          $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
 645          $this->assertEquals(array($course2ctx->id => $course2ctx->id), $contexts[$this->coursesareaid]);
 646  
 647          $contexts = $search->get_areas_user_accesses(array($course1->id))->usercontexts;
 648          $allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id);
 649          $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
 650          $this->assertEquals(array($course1ctx->id => $course1ctx->id), $contexts[$this->coursesareaid]);
 651  
 652          // Test context limited search with no course limit.
 653          $contexts = $search->get_areas_user_accesses(false,
 654                  [$frontpageforumcontext->id, $course2ctx->id])->usercontexts;
 655          $this->assertEquals([$frontpageforumcontext->id => $frontpageforumcontext->id],
 656                  $contexts[$this->forumpostareaid]);
 657          $this->assertEquals([$course2ctx->id => $course2ctx->id],
 658                  $contexts[$this->coursesareaid]);
 659  
 660          // Test context limited search with course limit.
 661          $contexts = $search->get_areas_user_accesses([$course1->id, $course2->id],
 662                  [$frontpageforumcontext->id, $course2ctx->id])->usercontexts;
 663          $this->assertArrayNotHasKey($this->forumpostareaid, $contexts);
 664          $this->assertEquals([$course2ctx->id => $course2ctx->id],
 665                  $contexts[$this->coursesareaid]);
 666  
 667          // Single context and course.
 668          $contexts = $search->get_areas_user_accesses([$course1->id], [$context1->id])->usercontexts;
 669          $this->assertEquals([$context1->id => $context1->id], $contexts[$this->forumpostareaid]);
 670          $this->assertArrayNotHasKey($this->coursesareaid, $contexts);
 671  
 672          // Enable "Include all visible courses" feature.
 673          set_config('searchincludeallcourses', 1);
 674          $contexts = $search->get_areas_user_accesses()->usercontexts;
 675          $expected = [
 676              $sitectx->id => $sitectx->id,
 677              $course1ctx->id => $course1ctx->id,
 678              $course2ctx->id => $course2ctx->id,
 679              $course3ctx->id => $course3ctx->id
 680          ];
 681          // Check that a student has assess to all courses data when "searchincludeallcourses" is enabled.
 682          $this->assertEquals($expected, $contexts[$this->coursesareaid]);
 683          // But at the same time doesn't have access to activities in the courses that the student can't access.
 684          $this->assertFalse(key_exists($context4->id, $contexts[$this->forumpostareaid]));
 685  
 686          // For admins, this is still limited only if we specify the things, so it should be same.
 687          $this->setAdminUser();
 688          $contexts = $search->get_areas_user_accesses([$course1->id], [$context1->id])->usercontexts;
 689          $this->assertEquals([$context1->id => $context1->id], $contexts[$this->forumpostareaid]);
 690          $this->assertArrayNotHasKey($this->coursesareaid, $contexts);
 691      }
 692  
 693      /**
 694       * Tests the block support in get_search_user_accesses.
 695       *
 696       * @return void
 697       */
 698      public function test_search_user_accesses_blocks() {
 699          global $DB;
 700  
 701          $this->resetAfterTest();
 702          $this->setAdminUser();
 703  
 704          // Create course and add HTML block.
 705          $generator = $this->getDataGenerator();
 706          $course1 = $generator->create_course();
 707          $context1 = \context_course::instance($course1->id);
 708          $page = new \moodle_page();
 709          $page->set_context($context1);
 710          $page->set_course($course1);
 711          $page->set_pagelayout('standard');
 712          $page->set_pagetype('course-view');
 713          $page->blocks->load_blocks();
 714          $page->blocks->add_block_at_end_of_default_region('html');
 715  
 716          // Create another course with HTML blocks only in some weird page or a module page (not
 717          // yet supported, so both these blocks will be ignored).
 718          $course2 = $generator->create_course();
 719          $context2 = \context_course::instance($course2->id);
 720          $page = new \moodle_page();
 721          $page->set_context($context2);
 722          $page->set_course($course2);
 723          $page->set_pagelayout('standard');
 724          $page->set_pagetype('bogus-page');
 725          $page->blocks->load_blocks();
 726          $page->blocks->add_block_at_end_of_default_region('html');
 727  
 728          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 729          $forumcontext = context_module::instance($forum->cmid);
 730          $page = new \moodle_page();
 731          $page->set_context($forumcontext);
 732          $page->set_course($course2);
 733          $page->set_pagelayout('standard');
 734          $page->set_pagetype('mod-forum-view');
 735          $page->blocks->load_blocks();
 736          $page->blocks->add_block_at_end_of_default_region('html');
 737  
 738          // The third course has 2 HTML blocks.
 739          $course3 = $generator->create_course();
 740          $context3 = \context_course::instance($course3->id);
 741          $page = new \moodle_page();
 742          $page->set_context($context3);
 743          $page->set_course($course3);
 744          $page->set_pagelayout('standard');
 745          $page->set_pagetype('course-view');
 746          $page->blocks->load_blocks();
 747          $page->blocks->add_block_at_end_of_default_region('html');
 748          $page->blocks->add_block_at_end_of_default_region('html');
 749  
 750          // Student 1 belongs to all 3 courses.
 751          $student1 = $generator->create_user();
 752          $generator->enrol_user($student1->id, $course1->id, 'student');
 753          $generator->enrol_user($student1->id, $course2->id, 'student');
 754          $generator->enrol_user($student1->id, $course3->id, 'student');
 755  
 756          // Student 2 belongs only to course 2.
 757          $student2 = $generator->create_user();
 758          $generator->enrol_user($student2->id, $course2->id, 'student');
 759  
 760          // And the third student is only in course 3.
 761          $student3 = $generator->create_user();
 762          $generator->enrol_user($student3->id, $course3->id, 'student');
 763  
 764          $search = testable_core_search::instance();
 765          $search->add_core_search_areas();
 766  
 767          // Admin gets 'true' result to function regardless of blocks.
 768          $this->setAdminUser();
 769          $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
 770  
 771          // Student 1 gets all 3 block contexts.
 772          $this->setUser($student1);
 773          $contexts = $search->get_areas_user_accesses()->usercontexts;
 774          $this->assertArrayHasKey('block_html-content', $contexts);
 775          $this->assertCount(3, $contexts['block_html-content']);
 776  
 777          // Student 2 does not get any blocks.
 778          $this->setUser($student2);
 779          $contexts = $search->get_areas_user_accesses()->usercontexts;
 780          $this->assertArrayNotHasKey('block_html-content', $contexts);
 781  
 782          // Student 3 gets only two of them.
 783          $this->setUser($student3);
 784          $contexts = $search->get_areas_user_accesses()->usercontexts;
 785          $this->assertArrayHasKey('block_html-content', $contexts);
 786          $this->assertCount(2, $contexts['block_html-content']);
 787  
 788          // A course limited search for student 1 is the same as the student 3 search.
 789          $this->setUser($student1);
 790          $limitedcontexts = $search->get_areas_user_accesses([$course3->id])->usercontexts;
 791          $this->assertEquals($contexts['block_html-content'], $limitedcontexts['block_html-content']);
 792  
 793          // Get block context ids for the blocks that appear.
 794          $blockcontextids = $DB->get_fieldset_sql('
 795              SELECT x.id
 796                FROM {block_instances} bi
 797                JOIN {context} x ON x.instanceid = bi.id AND x.contextlevel = ?
 798               WHERE (parentcontextid = ? OR parentcontextid = ?)
 799                     AND blockname = ?
 800            ORDER BY bi.id', [CONTEXT_BLOCK, $context1->id, $context3->id, 'html']);
 801  
 802          // Context limited search (no course).
 803          $contexts = $search->get_areas_user_accesses(false,
 804                  [$blockcontextids[0], $blockcontextids[2]])->usercontexts;
 805          $this->assertCount(2, $contexts['block_html-content']);
 806  
 807          // Context limited search (with course 3).
 808          $contexts = $search->get_areas_user_accesses([$course2->id, $course3->id],
 809                  [$blockcontextids[0], $blockcontextids[2]])->usercontexts;
 810          $this->assertCount(1, $contexts['block_html-content']);
 811      }
 812  
 813      /**
 814       * Tests retrieval of users search areas when limiting to a course the user is not enrolled in
 815       */
 816      public function test_search_users_accesses_limit_non_enrolled_course() {
 817          global $DB;
 818  
 819          $this->resetAfterTest();
 820  
 821          $user = $this->getDataGenerator()->create_user();
 822          $this->setUser($user);
 823  
 824          $search = testable_core_search::instance();
 825          $search->add_core_search_areas();
 826  
 827          $course = $this->getDataGenerator()->create_course();
 828          $context = context_course::instance($course->id);
 829  
 830          // Limit courses to search to only those the user is enrolled in.
 831          set_config('searchallavailablecourses', 0);
 832  
 833          $usercontexts = $search->get_areas_user_accesses([$course->id])->usercontexts;
 834          $this->assertNotEmpty($usercontexts);
 835          $this->assertArrayNotHasKey('core_course-course', $usercontexts);
 836  
 837          // This config ensures the search will also include courses the user can view.
 838          set_config('searchallavailablecourses', 1);
 839  
 840          // Allow "Authenticated user" role to view the course without being enrolled in it.
 841          $userrole = $DB->get_record('role', ['shortname' => 'user'], '*', MUST_EXIST);
 842          role_change_permission($userrole->id, $context, 'moodle/course:view', CAP_ALLOW);
 843  
 844          $usercontexts = $search->get_areas_user_accesses([$course->id])->usercontexts;
 845          $this->assertNotEmpty($usercontexts);
 846          $this->assertArrayHasKey('core_course-course', $usercontexts);
 847          $this->assertEquals($context->id, reset($usercontexts['core_course-course']));
 848      }
 849  
 850      /**
 851       * Test get_areas_user_accesses with regard to the 'all available courses' config option.
 852       *
 853       * @return void
 854       */
 855      public function test_search_user_accesses_allavailable() {
 856          global $DB, $CFG;
 857  
 858          $this->resetAfterTest();
 859  
 860          // Front page, including a forum.
 861          $frontpage = $DB->get_record('course', array('id' => SITEID));
 862          $forumfront = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
 863          $forumfrontctx = context_module::instance($forumfront->cmid);
 864  
 865          // Course 1 does not allow guest access.
 866          $course1 = $this->getDataGenerator()->create_course((object)array(
 867                  'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
 868                  'enrol_guest_password_0' => ''));
 869          $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
 870          $forum1ctx = context_module::instance($forum1->cmid);
 871  
 872          // Course 2 does not allow guest but is accessible by all users.
 873          $course2 = $this->getDataGenerator()->create_course((object)array(
 874                  'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
 875                  'enrol_guest_password_0' => ''));
 876          $course2ctx = context_course::instance($course2->id);
 877          $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 878          $forum2ctx = context_module::instance($forum2->cmid);
 879          assign_capability('moodle/course:view', CAP_ALLOW, $CFG->defaultuserroleid, $course2ctx->id);
 880  
 881          // Course 3 allows guest access without password.
 882          $course3 = $this->getDataGenerator()->create_course((object)array(
 883                  'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
 884                  'enrol_guest_password_0' => ''));
 885          $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 886          $forum3ctx = context_module::instance($forum3->cmid);
 887  
 888          // Student user is enrolled in course 1.
 889          $student = $this->getDataGenerator()->create_user();
 890          $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
 891  
 892          // No access user is just a user with no permissions.
 893          $noaccess = $this->getDataGenerator()->create_user();
 894  
 895          // First test without the all available option.
 896          $search = testable_core_search::instance();
 897  
 898          // Admin user can access everything.
 899          $this->setAdminUser();
 900          $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
 901  
 902          // No-access user can access only the front page forum.
 903          $this->setUser($noaccess);
 904          $contexts = $search->get_areas_user_accesses()->usercontexts;
 905          $this->assertEquals([$forumfrontctx->id], array_keys($contexts[$this->forumpostareaid]));
 906  
 907          // Student can access the front page forum plus the enrolled one.
 908          $this->setUser($student);
 909          $contexts = $search->get_areas_user_accesses()->usercontexts;
 910          $this->assertEquals([$forum1ctx->id, $forumfrontctx->id],
 911                  array_keys($contexts[$this->forumpostareaid]));
 912  
 913          // Now turn on the all available option.
 914          set_config('searchallavailablecourses', 1);
 915  
 916          // Admin user can access everything.
 917          $this->setAdminUser();
 918          $this->assertEquals((object)['everything' => true], $search->get_areas_user_accesses());
 919  
 920          // No-access user can access the front page forum and course 2, 3.
 921          $this->setUser($noaccess);
 922          $contexts = $search->get_areas_user_accesses()->usercontexts;
 923          $this->assertEquals([$forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
 924                  array_keys($contexts[$this->forumpostareaid]));
 925  
 926          // Student can access the front page forum plus the enrolled one plus courses 2, 3.
 927          $this->setUser($student);
 928          $contexts = $search->get_areas_user_accesses()->usercontexts;
 929          $this->assertEquals([$forum1ctx->id, $forum2ctx->id, $forum3ctx->id, $forumfrontctx->id],
 930                  array_keys($contexts[$this->forumpostareaid]));
 931      }
 932  
 933      /**
 934       * Tests group-related aspects of the get_areas_user_accesses function.
 935       */
 936      public function test_search_user_accesses_groups() {
 937          global $DB;
 938  
 939          $this->resetAfterTest();
 940          $this->setAdminUser();
 941  
 942          // Create 2 courses each with 2 groups and 2 forums (separate/visible groups).
 943          $generator = $this->getDataGenerator();
 944          $course1 = $generator->create_course();
 945          $course2 = $generator->create_course();
 946          $group1 = $generator->create_group(['courseid' => $course1->id]);
 947          $group2 = $generator->create_group(['courseid' => $course1->id]);
 948          $group3 = $generator->create_group(['courseid' => $course2->id]);
 949          $group4 = $generator->create_group(['courseid' => $course2->id]);
 950          $forum1s = $generator->create_module('forum', ['course' => $course1->id, 'groupmode' => SEPARATEGROUPS]);
 951          $id1s = context_module::instance($forum1s->cmid)->id;
 952          $forum1v = $generator->create_module('forum', ['course' => $course1->id, 'groupmode' => VISIBLEGROUPS]);
 953          $id1v = context_module::instance($forum1v->cmid)->id;
 954          $forum2s = $generator->create_module('forum', ['course' => $course2->id, 'groupmode' => SEPARATEGROUPS]);
 955          $id2s = context_module::instance($forum2s->cmid)->id;
 956          $forum2n = $generator->create_module('forum', ['course' => $course2->id, 'groupmode' => NOGROUPS]);
 957          $id2n = context_module::instance($forum2n->cmid)->id;
 958  
 959          // Get search instance.
 960          $search = testable_core_search::instance();
 961          $search->add_core_search_areas();
 962  
 963          // User 1 is a manager in one course and a student in the other one. They belong to
 964          // all of the groups 1, 2, 3, and 4.
 965          $user1 = $generator->create_user();
 966          $generator->enrol_user($user1->id, $course1->id, 'manager');
 967          $generator->enrol_user($user1->id, $course2->id, 'student');
 968          groups_add_member($group1, $user1);
 969          groups_add_member($group2, $user1);
 970          groups_add_member($group3, $user1);
 971          groups_add_member($group4, $user1);
 972  
 973          $this->setUser($user1);
 974          $accessinfo = $search->get_areas_user_accesses();
 975          $contexts = $accessinfo->usercontexts;
 976  
 977          // Double-check all the forum contexts.
 978          $postcontexts = $contexts['mod_forum-post'];
 979          sort($postcontexts);
 980          $this->assertEquals([$id1s, $id1v, $id2s, $id2n], $postcontexts);
 981  
 982          // Only the context in the second course (no accessallgroups) is restricted.
 983          $restrictedcontexts = $accessinfo->separategroupscontexts;
 984          sort($restrictedcontexts);
 985          $this->assertEquals([$id2s], $restrictedcontexts);
 986  
 987          // Only the groups from the second course (no accessallgroups) are included.
 988          $groupids = $accessinfo->usergroups;
 989          sort($groupids);
 990          $this->assertEquals([$group3->id, $group4->id], $groupids);
 991  
 992          // User 2 is a student in each course and belongs to groups 2 and 4.
 993          $user2 = $generator->create_user();
 994          $generator->enrol_user($user2->id, $course1->id, 'student');
 995          $generator->enrol_user($user2->id, $course2->id, 'student');
 996          groups_add_member($group2, $user2);
 997          groups_add_member($group4, $user2);
 998  
 999          $this->setUser($user2);
1000          $accessinfo = $search->get_areas_user_accesses();
1001          $contexts = $accessinfo->usercontexts;
1002  
1003          // Double-check all the forum contexts.
1004          $postcontexts = $contexts['mod_forum-post'];
1005          sort($postcontexts);
1006          $this->assertEquals([$id1s, $id1v, $id2s, $id2n], $postcontexts);
1007  
1008          // Both separate groups forums are restricted.
1009          $restrictedcontexts = $accessinfo->separategroupscontexts;
1010          sort($restrictedcontexts);
1011          $this->assertEquals([$id1s, $id2s], $restrictedcontexts);
1012  
1013          // Groups from both courses are included.
1014          $groupids = $accessinfo->usergroups;
1015          sort($groupids);
1016          $this->assertEquals([$group2->id, $group4->id], $groupids);
1017  
1018          // User 3 is a manager at system level.
1019          $user3 = $generator->create_user();
1020          role_assign($DB->get_field('role', 'id', ['shortname' => 'manager'], MUST_EXIST), $user3->id,
1021                  \context_system::instance());
1022  
1023          $this->setUser($user3);
1024          $accessinfo = $search->get_areas_user_accesses();
1025  
1026          // Nothing is restricted and no groups are relevant.
1027          $this->assertEquals([], $accessinfo->separategroupscontexts);
1028          $this->assertEquals([], $accessinfo->usergroups);
1029      }
1030  
1031      /**
1032       * test_is_search_area
1033       *
1034       * @return void
1035       */
1036      public function test_is_search_area() {
1037  
1038          $this->assertFalse(testable_core_search::is_search_area('\asd\asd'));
1039          $this->assertFalse(testable_core_search::is_search_area('\mod_forum\search\posta'));
1040          $this->assertFalse(testable_core_search::is_search_area('\core_search\base_mod'));
1041          $this->assertTrue(testable_core_search::is_search_area('\mod_forum\search\post'));
1042          $this->assertTrue(testable_core_search::is_search_area('\\mod_forum\\search\\post'));
1043          $this->assertTrue(testable_core_search::is_search_area('mod_forum\\search\\post'));
1044      }
1045  
1046      /**
1047       * Tests the request_index function used for reindexing certain contexts. This only tests
1048       * adding things to the request list, it doesn't test that they are actually indexed by the
1049       * scheduled task.
1050       */
1051      public function test_request_index() {
1052          global $DB;
1053  
1054          $this->resetAfterTest();
1055  
1056          $course1 = $this->getDataGenerator()->create_course();
1057          $course1ctx = context_course::instance($course1->id);
1058          $course2 = $this->getDataGenerator()->create_course();
1059          $course2ctx = context_course::instance($course2->id);
1060          $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
1061          $forum1ctx = context_module::instance($forum1->cmid);
1062          $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
1063          $forum2ctx = context_module::instance($forum2->cmid);
1064  
1065          // Initially no requests.
1066          $this->assertEquals(0, $DB->count_records('search_index_requests'));
1067  
1068          // Request update for course 1, all areas.
1069          \core_search\manager::request_index($course1ctx);
1070  
1071          // Check all details of entry.
1072          $results = array_values($DB->get_records('search_index_requests'));
1073          $this->assertCount(1, $results);
1074          $this->assertEquals($course1ctx->id, $results[0]->contextid);
1075          $this->assertEquals('', $results[0]->searcharea);
1076          $now = time();
1077          $this->assertLessThanOrEqual($now, $results[0]->timerequested);
1078          $this->assertGreaterThan($now - 10, $results[0]->timerequested);
1079          $this->assertEquals('', $results[0]->partialarea);
1080          $this->assertEquals(0, $results[0]->partialtime);
1081  
1082          // Request forum 1, all areas; not added as covered by course 1.
1083          \core_search\manager::request_index($forum1ctx);
1084          $this->assertEquals(1, $DB->count_records('search_index_requests'));
1085  
1086          // Request forum 1, specific area; not added as covered by course 1 all areas.
1087          \core_search\manager::request_index($forum1ctx, 'forum-post');
1088          $this->assertEquals(1, $DB->count_records('search_index_requests'));
1089  
1090          // Request course 1 again, specific area; not added as covered by all areas.
1091          \core_search\manager::request_index($course1ctx, 'forum-post');
1092          $this->assertEquals(1, $DB->count_records('search_index_requests'));
1093  
1094          // Request course 1 again, all areas; not needed as covered already.
1095          \core_search\manager::request_index($course1ctx);
1096          $this->assertEquals(1, $DB->count_records('search_index_requests'));
1097  
1098          // Request course 2, specific area.
1099          \core_search\manager::request_index($course2ctx, 'label-activity');
1100          // Note: I'm ordering by ID for convenience - this is dangerous in real code (see MDL-43447)
1101          // but in a unit test it shouldn't matter as nobody is using clustered databases for unit
1102          // test.
1103          $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1104          $this->assertCount(2, $results);
1105          $this->assertEquals($course1ctx->id, $results[0]->contextid);
1106          $this->assertEquals($course2ctx->id, $results[1]->contextid);
1107          $this->assertEquals('label-activity', $results[1]->searcharea);
1108  
1109          // Request forum 2, same specific area; not added.
1110          \core_search\manager::request_index($forum2ctx, 'label-activity');
1111          $this->assertEquals(2, $DB->count_records('search_index_requests'));
1112  
1113          // Request forum 2, different specific area; added.
1114          \core_search\manager::request_index($forum2ctx, 'forum-post');
1115          $this->assertEquals(3, $DB->count_records('search_index_requests'));
1116  
1117          // Request forum 2, all areas; also added. (Note: This could obviously remove the previous
1118          // one, but for simplicity, I didn't make it do that; also it could perhaps cause problems
1119          // if we had already begun processing the previous entry.)
1120          \core_search\manager::request_index($forum2ctx);
1121          $this->assertEquals(4, $DB->count_records('search_index_requests'));
1122  
1123          // Clear queue and do tests relating to priority.
1124          $DB->delete_records('search_index_requests');
1125  
1126          // Request forum 1, specific area, priority 100.
1127          \core_search\manager::request_index($forum1ctx, 'forum-post', 100);
1128          $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1129          $this->assertCount(1, $results);
1130          $this->assertEquals(100, $results[0]->indexpriority);
1131  
1132          // Request forum 1, same area, lower priority; no change.
1133          \core_search\manager::request_index($forum1ctx, 'forum-post', 99);
1134          $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1135          $this->assertCount(1, $results);
1136          $this->assertEquals(100, $results[0]->indexpriority);
1137  
1138          // Request forum 1, same area, higher priority; priority stored changes.
1139          \core_search\manager::request_index($forum1ctx, 'forum-post', 101);
1140          $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1141          $this->assertCount(1, $results);
1142          $this->assertEquals(101, $results[0]->indexpriority);
1143  
1144          // Request forum 1, all areas, lower priority; adds second entry.
1145          \core_search\manager::request_index($forum1ctx, '', 100);
1146          $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1147          $this->assertCount(2, $results);
1148          $this->assertEquals(100, $results[1]->indexpriority);
1149  
1150          // Request course 1, all areas, lower priority; adds third entry.
1151          \core_search\manager::request_index($course1ctx, '', 99);
1152          $results = array_values($DB->get_records('search_index_requests', null, 'id'));
1153          $this->assertCount(3, $results);
1154          $this->assertEquals(99, $results[2]->indexpriority);
1155      }
1156  
1157      /**
1158       * Tests the process_index_requests function.
1159       */
1160      public function test_process_index_requests() {
1161          global $DB;
1162  
1163          $this->resetAfterTest();
1164  
1165          $search = testable_core_search::instance();
1166  
1167          // When there are no index requests, nothing gets logged.
1168          $progress = new progress_trace_buffer(new text_progress_trace(), false);
1169          $search->process_index_requests(0.0, $progress);
1170          $out = $progress->get_buffer();
1171          $progress->reset_buffer();
1172          $this->assertEquals('', $out);
1173  
1174          // Set up the course with 3 forums.
1175          $generator = $this->getDataGenerator();
1176          $course = $generator->create_course(['fullname' => 'TCourse']);
1177          $forum1 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum1']);
1178          $forum2 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum2']);
1179          $forum3 = $generator->create_module('forum', ['course' => $course->id, 'name' => 'TForum3']);
1180  
1181          // Hack the forums so they have different creation times.
1182          $now = time();
1183          $DB->set_field('forum', 'timemodified', $now - 30, ['id' => $forum1->id]);
1184          $DB->set_field('forum', 'timemodified', $now - 20, ['id' => $forum2->id]);
1185          $DB->set_field('forum', 'timemodified', $now - 10, ['id' => $forum3->id]);
1186          $forum2time = $now - 20;
1187  
1188          // Make 2 index requests.
1189          testable_core_search::fake_current_time($now - 3);
1190          $search::request_index(context_course::instance($course->id), 'mod_label-activity');
1191          testable_core_search::fake_current_time($now - 2);
1192          $search::request_index(context_module::instance($forum1->cmid));
1193  
1194          // Run with no time limit.
1195          $search->process_index_requests(0.0, $progress);
1196          $out = $progress->get_buffer();
1197          $progress->reset_buffer();
1198  
1199          // Check that it's done both areas.
1200          $this->assertStringContainsString(
1201                  'Indexing requested context: Course: TCourse (search area: mod_label-activity)',
1202                  $out);
1203          $this->assertStringContainsString(
1204                  'Completed requested context: Course: TCourse (search area: mod_label-activity)',
1205                  $out);
1206          $this->assertStringContainsString('Indexing requested context: Forum: TForum1', $out);
1207          $this->assertStringContainsString('Completed requested context: Forum: TForum1', $out);
1208  
1209          // Check the requests database table is now empty.
1210          $this->assertEquals(0, $DB->count_records('search_index_requests'));
1211  
1212          // Request indexing the course a couple of times.
1213          testable_core_search::fake_current_time($now - 3);
1214          $search::request_index(context_course::instance($course->id), 'mod_forum-activity');
1215          testable_core_search::fake_current_time($now - 2);
1216          $search::request_index(context_course::instance($course->id), 'mod_forum-post');
1217  
1218          // Do the processing again with a time limit and indexing delay. The time limit is too
1219          // small; because of the way the logic works, this means it will index 2 activities.
1220          $search->get_engine()->set_add_delay(0.2);
1221          $search->process_index_requests(0.1, $progress);
1222          $out = $progress->get_buffer();
1223          $progress->reset_buffer();
1224  
1225          // Confirm the right wrapper information was logged.
1226          $this->assertStringContainsString(
1227                  'Indexing requested context: Course: TCourse (search area: mod_forum-activity)',
1228                  $out);
1229          $this->assertStringContainsString('Stopping indexing due to time limit', $out);
1230          $this->assertStringContainsString(
1231                  'Ending requested context: Course: TCourse (search area: mod_forum-activity)',
1232                  $out);
1233  
1234          // Check the database table has been updated with progress.
1235          $records = array_values($DB->get_records('search_index_requests', null, 'searcharea'));
1236          $this->assertEquals('mod_forum-activity', $records[0]->partialarea);
1237          $this->assertEquals($forum2time, $records[0]->partialtime);
1238  
1239          // Run again and confirm it now finishes.
1240          $search->process_index_requests(2.0, $progress);
1241          $out = $progress->get_buffer();
1242          $progress->reset_buffer();
1243          $this->assertStringContainsString(
1244                  'Completed requested context: Course: TCourse (search area: mod_forum-activity)',
1245                  $out);
1246          $this->assertStringContainsString(
1247                  'Completed requested context: Course: TCourse (search area: mod_forum-post)',
1248                  $out);
1249  
1250          // Confirm table is now empty.
1251          $this->assertEquals(0, $DB->count_records('search_index_requests'));
1252  
1253          // Make 2 requests - first one is low priority.
1254          testable_core_search::fake_current_time($now - 3);
1255          $search::request_index(context_module::instance($forum1->cmid), 'mod_forum-activity',
1256                  \core_search\manager::INDEX_PRIORITY_REINDEXING);
1257          testable_core_search::fake_current_time($now - 2);
1258          $search::request_index(context_module::instance($forum2->cmid), 'mod_forum-activity');
1259  
1260          // Process with short time limit and confirm it does the second one first.
1261          $search->process_index_requests(0.1, $progress);
1262          $out = $progress->get_buffer();
1263          $progress->reset_buffer();
1264          $this->assertStringContainsString(
1265                  'Completed requested context: Forum: TForum2 (search area: mod_forum-activity)',
1266                  $out);
1267          $search->process_index_requests(0.1, $progress);
1268          $out = $progress->get_buffer();
1269          $progress->reset_buffer();
1270          $this->assertStringContainsString(
1271                  'Completed requested context: Forum: TForum1 (search area: mod_forum-activity)',
1272                  $out);
1273  
1274          // Make a request for a course context...
1275          $course = $generator->create_course();
1276          $context = context_course::instance($course->id);
1277          $search::request_index($context);
1278  
1279          // ...but then delete it (note: delete_course spews output, so we throw it away).
1280          ob_start();
1281          delete_course($course);
1282          ob_end_clean();
1283  
1284          // Process requests - it should only note the deleted context.
1285          $search->process_index_requests(10, $progress);
1286          $out = $progress->get_buffer();
1287          $progress->reset_buffer();
1288          $this->assertStringContainsString('Skipped deleted context: ' . $context->id, $out);
1289  
1290          // Confirm request table is now empty.
1291          $this->assertEquals(0, $DB->count_records('search_index_requests'));
1292      }
1293  
1294      /**
1295       * Test search area categories.
1296       */
1297      public function test_get_search_area_categories() {
1298          $categories = \core_search\manager::get_search_area_categories();
1299  
1300          $this->assertTrue(is_array($categories));
1301          $this->assertTrue(count($categories) >= 4); // We always should have 4 core categories.
1302          $this->assertArrayHasKey('core-all', $categories);
1303          $this->assertArrayHasKey('core-course-content', $categories);
1304          $this->assertArrayHasKey('core-courses', $categories);
1305          $this->assertArrayHasKey('core-users', $categories);
1306  
1307          foreach ($categories as $category) {
1308              $this->assertInstanceOf('\core_search\area_category', $category);
1309          }
1310      }
1311  
1312      /**
1313       * Test that we can find out if search area categories functionality is enabled.
1314       */
1315      public function test_is_search_area_categories_enabled() {
1316          $this->resetAfterTest();
1317  
1318          $this->assertFalse(\core_search\manager::is_search_area_categories_enabled());
1319          set_config('searchenablecategories', 1);
1320          $this->assertTrue(\core_search\manager::is_search_area_categories_enabled());
1321          set_config('searchenablecategories', 0);
1322          $this->assertFalse(\core_search\manager::is_search_area_categories_enabled());
1323      }
1324  
1325      /**
1326       * Test that we can find out if hiding all results category is enabled.
1327       */
1328      public function test_should_hide_all_results_category() {
1329          $this->resetAfterTest();
1330  
1331          $this->assertEquals(0, \core_search\manager::should_hide_all_results_category());
1332          set_config('searchhideallcategory', 1);
1333          $this->assertEquals(1, \core_search\manager::should_hide_all_results_category());
1334          set_config('searchhideallcategory', 0);
1335          $this->assertEquals(0, \core_search\manager::should_hide_all_results_category());
1336      }
1337  
1338      /**
1339       * Test that we can get default search category name.
1340       */
1341      public function test_get_default_area_category_name() {
1342          $this->resetAfterTest();
1343  
1344          $expected = 'core-all';
1345          $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
1346  
1347          set_config('searchhideallcategory', 1);
1348          $expected = 'core-course-content';
1349          $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
1350  
1351          set_config('searchhideallcategory', 0);
1352          $expected = 'core-all';
1353          $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
1354      }
1355  
1356      /**
1357       * Test that we can get correct search area category by its name.
1358       */
1359      public function test_get_search_area_category_by_name() {
1360          $this->resetAfterTest();
1361  
1362          $testcategory = \core_search\manager::get_search_area_category_by_name('test_random_name');
1363          $this->assertEquals('core-all', $testcategory->get_name());
1364  
1365          $testcategory = \core_search\manager::get_search_area_category_by_name('core-courses');
1366          $this->assertEquals('core-courses', $testcategory->get_name());
1367  
1368          set_config('searchhideallcategory', 1);
1369          $testcategory = \core_search\manager::get_search_area_category_by_name('test_random_name');
1370          $this->assertEquals('core-course-content', $testcategory->get_name());
1371      }
1372  
1373      /**
1374       * Test that we can check that "Include all visible courses" feature is enabled.
1375       */
1376      public function test_include_all_courses_enabled() {
1377          $this->resetAfterTest();
1378          $this->assertFalse(\core_search\manager::include_all_courses());
1379          set_config('searchincludeallcourses', 1);
1380          $this->assertTrue(\core_search\manager::include_all_courses());
1381      }
1382  
1383      /**
1384       * Test that we can correctly build a list of courses for a course filter for the search results.
1385       */
1386      public function test_build_limitcourseids() {
1387          global $USER;
1388  
1389          $this->resetAfterTest();
1390          $this->setAdminUser();
1391  
1392          $course1 = $this->getDataGenerator()->create_course();
1393          $course2 = $this->getDataGenerator()->create_course();
1394          $course3 = $this->getDataGenerator()->create_course();
1395          $course4 = $this->getDataGenerator()->create_course();
1396  
1397          $this->getDataGenerator()->enrol_user($USER->id, $course1->id);
1398          $this->getDataGenerator()->enrol_user($USER->id, $course3->id);
1399  
1400          $search = testable_core_search::instance();
1401  
1402          $formdata = new stdClass();
1403          $formdata->courseids = [];
1404          $formdata->mycoursesonly = false;
1405          $limitcourseids = $search->build_limitcourseids($formdata);
1406          $this->assertEquals(false, $limitcourseids);
1407  
1408          $formdata->courseids = [];
1409          $formdata->mycoursesonly = true;
1410          $limitcourseids = $search->build_limitcourseids($formdata);
1411          $this->assertEquals([$course1->id, $course3->id], $limitcourseids);
1412  
1413          $formdata->courseids = [$course1->id, $course2->id, $course4->id];
1414          $formdata->mycoursesonly = false;
1415          $limitcourseids = $search->build_limitcourseids($formdata);
1416          $this->assertEquals([$course1->id, $course2->id, $course4->id], $limitcourseids);
1417  
1418          $formdata->courseids = [$course1->id, $course2->id, $course4->id];
1419          $formdata->mycoursesonly = true;
1420          $limitcourseids = $search->build_limitcourseids($formdata);
1421          $this->assertEquals([$course1->id], $limitcourseids);
1422      }
1423  
1424      /**
1425       * Test data for test_parse_areaid test fucntion.
1426       *
1427       * @return array
1428       */
1429      public function parse_search_area_id_data_provider() {
1430          return [
1431              ['mod_book-chapter', ['mod_book', 'search_chapter']],
1432              ['mod_customcert-activity', ['mod_customcert', 'search_activity']],
1433              ['core_course-mycourse', ['core_search', 'core_course_mycourse']],
1434          ];
1435      }
1436  
1437      /**
1438       * Test that manager class can parse area id correctly.
1439       * @dataProvider parse_search_area_id_data_provider
1440       *
1441       * @param string $areaid Area id to parse.
1442       * @param array $expected Expected result of parsing.
1443       */
1444      public function test_parse_search_area_id($areaid, $expected) {
1445          $this->assertEquals($expected, \core_search\manager::parse_areaid($areaid));
1446      }
1447  
1448      /**
1449       * Test that manager class will throw an exception when parsing an invalid area id.
1450       */
1451      public function test_parse_invalid_search_area_id() {
1452          $this->expectException('coding_exception');
1453          $this->expectExceptionMessage('Trying to parse invalid search area id invalid_area');
1454          \core_search\manager::parse_areaid('invalid_area');
1455      }
1456  
1457      /**
1458       * Test getting a coding exception when trying to lean up existing search area.
1459       */
1460      public function test_cleaning_up_existing_search_area() {
1461          $expectedmessage = "Area mod_assign-activity exists. Please use appropriate search area class to manipulate the data.";
1462  
1463          $this->expectException('coding_exception');
1464          $this->expectExceptionMessage($expectedmessage);
1465  
1466          \core_search\manager::clean_up_non_existing_area('mod_assign-activity');
1467      }
1468  
1469      /**
1470       * Test clean up of non existing search area.
1471       */
1472      public function test_clean_up_non_existing_search_area() {
1473          global $DB;
1474  
1475          $this->resetAfterTest();
1476  
1477          $areaid = 'core_course-mycourse';
1478          $plugin = 'core_search';
1479  
1480          // Get all settings to DB and make sure they are there.
1481          foreach (\core_search\base::get_settingnames() as $settingname) {
1482              $record = new stdClass();
1483              $record->plugin = $plugin;
1484              $record->name = 'core_course_mycourse'. $settingname;
1485              $record->value = 'test';
1486  
1487              $DB->insert_record('config_plugins', $record);
1488              $this->assertTrue($DB->record_exists('config_plugins', ['plugin' => $plugin, 'name' => $record->name]));
1489          }
1490  
1491          // Clean up the search area.
1492          \core_search\manager::clean_up_non_existing_area($areaid);
1493  
1494          // Check that records are not in DB after we ran clean up.
1495          foreach (\core_search\base::get_settingnames() as $settingname) {
1496              $plugin = 'core_search';
1497              $name = 'core_course_mycourse'. $settingname;
1498              $this->assertFalse($DB->record_exists('config_plugins', ['plugin' => $plugin, 'name' => $name]));
1499          }
1500      }
1501  
1502      /**
1503       * Tests the context_deleted, course_deleting_start, and course_deleting_finish methods.
1504       */
1505      public function test_context_deletion() {
1506          $this->resetAfterTest();
1507  
1508          // Create one course with 4 activities, and another with one.
1509          $generator = $this->getDataGenerator();
1510          $course1 = $generator->create_course();
1511          $page1 = $generator->create_module('page', ['course' => $course1]);
1512          $context1 = \context_module::instance($page1->cmid);
1513          $page2 = $generator->create_module('page', ['course' => $course1]);
1514          $page3 = $generator->create_module('page', ['course' => $course1]);
1515          $context3 = \context_module::instance($page3->cmid);
1516          $page4 = $generator->create_module('page', ['course' => $course1]);
1517          $course2 = $generator->create_course();
1518          $page5 = $generator->create_module('page', ['course' => $course2]);
1519          $context5 = \context_module::instance($page5->cmid);
1520  
1521          // Also create a user.
1522          $user = $generator->create_user();
1523          $usercontext = \context_user::instance($user->id);
1524  
1525          $search = testable_core_search::instance();
1526  
1527          // Delete two of the pages individually.
1528          course_delete_module($page1->cmid);
1529          course_delete_module($page3->cmid);
1530  
1531          // Delete the course with another two.
1532          delete_course($course1->id, false);
1533  
1534          // Delete the user.
1535          delete_user($user);
1536  
1537          // Delete the page from the other course.
1538          course_delete_module($page5->cmid);
1539  
1540          // It should have deleted the contexts and the course, but not the contexts in the course.
1541          $expected = [
1542              ['context', $context1->id],
1543              ['context', $context3->id],
1544              ['course', $course1->id],
1545              ['context', $usercontext->id],
1546              ['context', $context5->id]
1547          ];
1548          $this->assertEquals($expected, $search->get_engine()->get_and_clear_deletes());
1549      }
1550  }