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