MDL-65806 Search: Solr does not find words in italic
[moodle.git] / search / engine / solr / tests / engine_test.php
CommitLineData
95c6aeaf
DM
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 * Solr earch engine base unit tests.
19 *
20 * Required params:
21 * - define('TEST_SEARCH_SOLR_HOSTNAME', '127.0.0.1');
22 * - define('TEST_SEARCH_SOLR_PORT', '8983');
23 * - define('TEST_SEARCH_SOLR_INDEXNAME', 'unittest');
24 *
25 * Optional params:
26 * - define('TEST_SEARCH_SOLR_USERNAME', '');
27 * - define('TEST_SEARCH_SOLR_PASSWORD', '');
5dc4624c
EM
28 * - define('TEST_SEARCH_SOLR_SSLCERT', '');
29 * - define('TEST_SEARCH_SOLR_SSLKEY', '');
30 * - define('TEST_SEARCH_SOLR_KEYPASSWORD', '');
31 * - define('TEST_SEARCH_SOLR_CAINFOCERT', '');
95c6aeaf 32 *
2ee2f530 33 * @package search_solr
95c6aeaf
DM
34 * @category phpunit
35 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38
39defined('MOODLE_INTERNAL') || die();
40
41global $CFG;
42require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
43require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
053118a1 44require_once($CFG->dirroot . '/search/engine/solr/tests/fixtures/testable_engine.php');
95c6aeaf
DM
45
46/**
47 * Solr search engine base unit tests.
48 *
2ee2f530 49 * @package search_solr
95c6aeaf
DM
50 * @category phpunit
51 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53 */
54class search_solr_engine_testcase extends advanced_testcase {
55
56 /**
163d2800 57 * @var \core_search\manager
95c6aeaf
DM
58 */
59 protected $search = null;
60
053118a1
EM
61 /**
62 * @var Instace of core_search_generator.
63 */
64 protected $generator = null;
65
66 /**
67 * @var Instace of testable_engine.
68 */
69 protected $engine = null;
70
95c6aeaf
DM
71 public function setUp() {
72 $this->resetAfterTest();
73 set_config('enableglobalsearch', true);
b137e029 74 set_config('searchengine', 'solr');
95c6aeaf
DM
75
76 if (!function_exists('solr_get_version')) {
77 $this->markTestSkipped('Solr extension is not loaded.');
78 }
79
80 if (!defined('TEST_SEARCH_SOLR_HOSTNAME') || !defined('TEST_SEARCH_SOLR_INDEXNAME') ||
81 !defined('TEST_SEARCH_SOLR_PORT')) {
82 $this->markTestSkipped('Solr extension test server not set.');
83 }
84
bd5d2c60
DM
85 set_config('server_hostname', TEST_SEARCH_SOLR_HOSTNAME, 'search_solr');
86 set_config('server_port', TEST_SEARCH_SOLR_PORT, 'search_solr');
95c6aeaf
DM
87 set_config('indexname', TEST_SEARCH_SOLR_INDEXNAME, 'search_solr');
88
89 if (defined('TEST_SEARCH_SOLR_USERNAME')) {
5dc4624c 90 set_config('server_username', TEST_SEARCH_SOLR_USERNAME, 'search_solr');
95c6aeaf
DM
91 }
92
93 if (defined('TEST_SEARCH_SOLR_PASSWORD')) {
5dc4624c 94 set_config('server_password', TEST_SEARCH_SOLR_PASSWORD, 'search_solr');
95c6aeaf
DM
95 }
96
5dc4624c
EM
97 if (defined('TEST_SEARCH_SOLR_SSLCERT')) {
98 set_config('secure', true, 'search_solr');
99 set_config('ssl_cert', TEST_SEARCH_SOLR_SSLCERT, 'search_solr');
100 }
101
102 if (defined('TEST_SEARCH_SOLR_SSLKEY')) {
103 set_config('ssl_key', TEST_SEARCH_SOLR_SSLKEY, 'search_solr');
104 }
105
106 if (defined('TEST_SEARCH_SOLR_KEYPASSWORD')) {
107 set_config('ssl_keypassword', TEST_SEARCH_SOLR_KEYPASSWORD, 'search_solr');
108 }
109
110 if (defined('TEST_SEARCH_SOLR_CAINFOCERT')) {
111 set_config('ssl_cainfo', TEST_SEARCH_SOLR_CAINFOCERT, 'search_solr');
112 }
113
8dfc6291 114 set_config('fileindexing', 1, 'search_solr');
cd894f84
EM
115
116 // We are only test indexing small string files, so setting this as low as we can.
117 set_config('maxindexfilekb', 1, 'search_solr');
5dc4624c 118
053118a1
EM
119 $this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
120 $this->generator->setup();
121
95c6aeaf
DM
122 // Inject search solr engine into the testable core search as we need to add the mock
123 // search component to it.
053118a1
EM
124 $this->engine = new \search_solr\testable_engine();
125 $this->search = testable_core_search::instance($this->engine);
126 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
127 $this->search->add_search_area($areaid, new core_mocksearch\search\mock_search_area());
95c6aeaf
DM
128
129 $this->setAdminUser();
130
131 // Cleanup before doing anything on it as the index it is out of this test control.
132 $this->search->delete_index();
133
134 // Add moodle fields if they don't exist.
135 $schema = new \search_solr\schema();
136 $schema->setup(false);
137 }
138
053118a1
EM
139 public function tearDown() {
140 // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
141 if ($this->generator) {
142 // Moodle DML freaks out if we don't teardown the temp table after each run.
143 $this->generator->teardown();
144 $this->generator = null;
145 }
146 }
147
148 /**
149 * Simple data provider to allow tests to be run with file indexing on and off.
150 */
151 public function file_indexing_provider() {
152 return array(
153 'file-indexing-on' => array(1),
154 'file-indexing-off' => array(0)
155 );
156 }
157
95c6aeaf 158 public function test_connection() {
053118a1 159 $this->assertTrue($this->engine->is_server_ready());
95c6aeaf
DM
160 }
161
053118a1
EM
162 /**
163 * @dataProvider file_indexing_provider
164 */
165 public function test_index($fileindexing) {
95c6aeaf
DM
166 global $DB;
167
053118a1
EM
168 $this->engine->test_set_config('fileindexing', $fileindexing);
169
170 $record = new \stdClass();
171 $record->timemodified = time() - 1;
172 $this->generator->create_record($record);
95c6aeaf
DM
173
174 // Data gets into the search engine.
175 $this->assertTrue($this->search->index());
176
177 // Not anymore as everything was already added.
178 sleep(1);
179 $this->assertFalse($this->search->index());
180
053118a1 181 $this->generator->create_record();
95c6aeaf
DM
182
183 // Indexing again once there is new data.
184 $this->assertTrue($this->search->index());
185 }
186
187 /**
188 * Better keep this not very strict about which or how many results are returned as may depend on solr engine config.
189 *
053118a1
EM
190 * @dataProvider file_indexing_provider
191 *
95c6aeaf
DM
192 * @return void
193 */
053118a1 194 public function test_search($fileindexing) {
95c6aeaf
DM
195 global $USER, $DB;
196
053118a1
EM
197 $this->engine->test_set_config('fileindexing', $fileindexing);
198
199 $this->generator->create_record();
200 $record = new \stdClass();
201 $record->title = "Special title";
202 $this->generator->create_record($record);
95c6aeaf
DM
203
204 $this->search->index();
205
206 $querydata = new stdClass();
207 $querydata->q = 'message';
208 $results = $this->search->search($querydata);
209 $this->assertCount(2, $results);
210
211 // Based on core_mocksearch\search\indexer.
212 $this->assertEquals($USER->id, $results[0]->get('userid'));
cfa00fc5 213 $this->assertEquals(\context_course::instance(SITEID)->id, $results[0]->get('contextid'));
95c6aeaf 214
546c0af5 215 // Do a test to make sure we aren't searching non-query fields, like areaid.
053118a1 216 $querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
546c0af5
EM
217 $this->assertCount(0, $this->search->search($querydata));
218 $querydata->q = 'message';
219
95c6aeaf
DM
220 sleep(1);
221 $beforeadding = time();
222 sleep(1);
053118a1 223 $this->generator->create_record();
95c6aeaf
DM
224 $this->search->index();
225
226 // Timestart.
227 $querydata->timestart = $beforeadding;
228 $this->assertCount(1, $this->search->search($querydata));
229
230 // Timeend.
231 unset($querydata->timestart);
232 $querydata->timeend = $beforeadding;
233 $this->assertCount(2, $this->search->search($querydata));
234
235 // Title.
236 unset($querydata->timeend);
053118a1 237 $querydata->title = 'Special title';
95c6aeaf 238 $this->assertCount(1, $this->search->search($querydata));
3dab4841 239
427e3cbc
EM
240 // Course IDs.
241 unset($querydata->title);
242 $querydata->courseids = array(SITEID + 1);
243 $this->assertCount(0, $this->search->search($querydata));
244
245 $querydata->courseids = array(SITEID);
246 $this->assertCount(3, $this->search->search($querydata));
247
501801a2
EM
248 // Now try some area-id combinations.
249 unset($querydata->courseids);
250 $forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
861de101 251 $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
501801a2
EM
252
253 $querydata->areaids = array($forumpostareaid);
254 $this->assertCount(0, $this->search->search($querydata));
255
256 $querydata->areaids = array($forumpostareaid, $mockareaid);
257 $this->assertCount(3, $this->search->search($querydata));
258
259 $querydata->areaids = array($mockareaid);
260 $this->assertCount(3, $this->search->search($querydata));
261
262 $querydata->areaids = array();
263 $this->assertCount(3, $this->search->search($querydata));
264
3dab4841 265 // Check that index contents get updated.
053118a1 266 $this->generator->delete_all();
3dab4841
DM
267 $this->search->index(true);
268 unset($querydata->title);
053118a1 269 $querydata->q = '*';
3dab4841 270 $this->assertCount(0, $this->search->search($querydata));
95c6aeaf
DM
271 }
272
053118a1
EM
273 /**
274 * @dataProvider file_indexing_provider
275 */
276 public function test_delete($fileindexing) {
277 $this->engine->test_set_config('fileindexing', $fileindexing);
278
279 $this->generator->create_record();
280 $this->generator->create_record();
95c6aeaf
DM
281 $this->search->index();
282
283 $querydata = new stdClass();
284 $querydata->q = 'message';
285
286 $this->assertCount(2, $this->search->search($querydata));
287
053118a1 288 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
95c6aeaf 289 $this->search->delete_index($areaid);
95c6aeaf
DM
290 $this->assertCount(0, $this->search->search($querydata));
291 }
f6b425e2 292
053118a1
EM
293 /**
294 * @dataProvider file_indexing_provider
295 */
296 public function test_alloweduserid($fileindexing) {
297 $this->engine->test_set_config('fileindexing', $fileindexing);
f6b425e2 298
053118a1
EM
299 $area = new core_mocksearch\search\mock_search_area();
300
301 $record = $this->generator->create_record();
f6b425e2
EM
302
303 // Get the doc and insert the default doc.
304 $doc = $area->get_document($record);
053118a1 305 $this->engine->add_document($doc);
f6b425e2
EM
306
307 $users = array();
308 $users[] = $this->getDataGenerator()->create_user();
309 $users[] = $this->getDataGenerator()->create_user();
310 $users[] = $this->getDataGenerator()->create_user();
311
312 // Add a record that only user 100 can see.
313 $originalid = $doc->get('id');
314
315 // Now add a custom doc for each user.
316 foreach ($users as $user) {
091973db 317 $doc = $area->get_document($record);
f6b425e2
EM
318 $doc->set('id', $originalid.'-'.$user->id);
319 $doc->set('owneruserid', $user->id);
053118a1 320 $this->engine->add_document($doc);
f6b425e2
EM
321 }
322
053118a1 323 $this->engine->area_index_complete($area->get_area_id());
f6b425e2
EM
324
325 $querydata = new stdClass();
326 $querydata->q = 'message';
327 $querydata->title = $doc->get('title');
328
329 // We are going to go through each user and see if they get the original and the owned doc.
330 foreach ($users as $user) {
331 $this->setUser($user);
332
333 $results = $this->search->search($querydata);
334 $this->assertCount(2, $results);
335
336 $owned = 0;
337 $notowned = 0;
338
339 // We don't know what order we will get the results in, so we are doing this.
340 foreach ($results as $result) {
341 $owneruserid = $result->get('owneruserid');
342 if (empty($owneruserid)) {
343 $notowned++;
344 $this->assertEquals(0, $owneruserid);
345 $this->assertEquals($originalid, $result->get('id'));
346 } else {
347 $owned++;
348 $this->assertEquals($user->id, $owneruserid);
349 $this->assertEquals($originalid.'-'.$user->id, $result->get('id'));
350 }
351 }
352
353 $this->assertEquals(1, $owned);
354 $this->assertEquals(1, $notowned);
355 }
356
357 // Now test a user with no owned results.
358 $otheruser = $this->getDataGenerator()->create_user();
359 $this->setUser($otheruser);
360
361 $results = $this->search->search($querydata);
362 $this->assertCount(1, $results);
363
364 $this->assertEquals(0, $results[0]->get('owneruserid'));
365 $this->assertEquals($originalid, $results[0]->get('id'));
366 }
4894840d 367
053118a1
EM
368 /**
369 * @dataProvider file_indexing_provider
370 */
371 public function test_highlight($fileindexing) {
4894840d
EM
372 global $PAGE;
373
053118a1
EM
374 $this->engine->test_set_config('fileindexing', $fileindexing);
375
376 $this->generator->create_record();
4894840d
EM
377 $this->search->index();
378
379 $querydata = new stdClass();
380 $querydata->q = 'message';
381
382 $results = $this->search->search($querydata);
053118a1 383 $this->assertCount(1, $results);
4894840d
EM
384
385 $result = reset($results);
386
387 $regex = '|'.\search_solr\engine::HIGHLIGHT_START.'message'.\search_solr\engine::HIGHLIGHT_END.'|';
388 $this->assertRegExp($regex, $result->get('content'));
389
390 $searchrenderer = $PAGE->get_renderer('core_search');
391 $exported = $result->export_for_template($searchrenderer);
392
393 $regex = '|<span class="highlight">message</span>|';
394 $this->assertRegExp($regex, $exported['content']);
395 }
e195da19 396
1aaead91
EM
397 public function test_export_file_for_engine() {
398 // Get area to work with.
399 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
400 $area = \core_search\manager::get_search_area($areaid);
401
402 $record = $this->generator->create_record();
403
404 $doc = $area->get_document($record);
405 $filerecord = new stdClass();
406 $filerecord->timemodified = 978310800;
407 $file = $this->generator->create_file($filerecord);
408 $doc->add_stored_file($file);
409
410 $filearray = $doc->export_file_for_engine($file);
411
412 $this->assertEquals(\core_search\manager::TYPE_FILE, $filearray['type']);
413 $this->assertEquals($file->get_id(), $filearray['solr_fileid']);
414 $this->assertEquals($file->get_contenthash(), $filearray['solr_filecontenthash']);
415 $this->assertEquals(\search_solr\document::INDEXED_FILE_TRUE, $filearray['solr_fileindexstatus']);
416 $this->assertEquals($file->get_filename(), $filearray['title']);
417 $this->assertEquals(978310800, \search_solr\document::import_time_from_engine($filearray['modified']));
418 }
419
cd894f84 420 public function test_index_file() {
cd894f84 421 // Very simple test.
053118a1
EM
422 $file = $this->generator->create_file();
423
424 $record = new \stdClass();
425 $record->attachfileids = array($file->get_id());
426 $this->generator->create_record($record);
427
cd894f84
EM
428 $this->search->index();
429 $querydata = new stdClass();
430 $querydata->q = '"File contents"';
431
053118a1 432 $this->assertCount(1, $this->search->search($querydata));
cd894f84
EM
433 }
434
435 public function test_reindexing_files() {
053118a1
EM
436 // Get area to work with.
437 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
cd894f84
EM
438 $area = \core_search\manager::get_search_area($areaid);
439
053118a1 440 $record = $this->generator->create_record();
cd894f84
EM
441
442 $doc = $area->get_document($record);
443
444 // Now we are going to make some files.
445 $fs = get_file_storage();
446 $syscontext = \context_system::instance();
447
448 $files = array();
cd894f84 449
053118a1 450 $filerecord = new \stdClass();
cd894f84
EM
451 // We make enough so that we pass the 500 files threashold. That is the boundary when getting files.
452 $boundary = 500;
453 $top = (int)($boundary * 1.1);
454 for ($i = 0; $i < $top; $i++) {
053118a1
EM
455 $filerecord->filename = 'searchfile'.$i;
456 $filerecord->content = 'Some FileContents'.$i;
457 $file = $this->generator->create_file($filerecord);
cd894f84
EM
458 $doc->add_stored_file($file);
459 $files[] = $file;
460 }
461
462 // Add the doc with lots of files, then commit.
053118a1
EM
463 $this->engine->add_document($doc, true);
464 $this->engine->area_index_complete($area->get_area_id());
cd894f84
EM
465
466 // Indexes we are going to check. 0 means we will delete, 1 means we will keep.
467 $checkfiles = array(
468 0 => 0, // Check the begining of the set.
469 1 => 1,
470 2 => 0,
471 ($top - 3) => 0, // Check the end of the set.
472 ($top - 2) => 1,
473 ($top - 1) => 0,
474 ($boundary - 2) => 0, // Check at the boundary between fetch groups.
475 ($boundary - 1) => 0,
476 $boundary => 0,
477 ($boundary + 1) => 0,
478 ((int)($boundary * 0.5)) => 1, // Make sure we keep some middle ones.
479 ((int)($boundary * 1.05)) => 1
480 );
481
482 $querydata = new stdClass();
483
484 // First, check that all the files are currently there.
485 foreach ($checkfiles as $key => $unused) {
486 $querydata->q = 'FileContents'.$key;
487 $this->assertCount(1, $this->search->search($querydata));
488 $querydata->q = 'searchfile'.$key;
489 $this->assertCount(1, $this->search->search($querydata));
490 }
491
492 // Remove the files we want removed from the files array.
493 foreach ($checkfiles as $key => $keep) {
494 if (!$keep) {
495 unset($files[$key]);
496 }
497 }
498
499 // And make us a new file to add.
053118a1
EM
500 $filerecord->filename = 'searchfileNew';
501 $filerecord->content = 'Some FileContentsNew';
502 $files[] = $this->generator->create_file($filerecord);
cd894f84
EM
503 $checkfiles['New'] = 1;
504
505 $doc = $area->get_document($record);
506 foreach($files as $file) {
507 $doc->add_stored_file($file);
508 }
509
510 // Reindex the document with the changed files.
053118a1
EM
511 $this->engine->add_document($doc, true);
512 $this->engine->area_index_complete($area->get_area_id());
cd894f84
EM
513
514 // Go through our check array, and see if the file is there or not.
515 foreach ($checkfiles as $key => $keep) {
516 $querydata->q = 'FileContents'.$key;
517 $this->assertCount($keep, $this->search->search($querydata));
518 $querydata->q = 'searchfile'.$key;
519 $this->assertCount($keep, $this->search->search($querydata));
520 }
521
522 // Now check that we get one result when we search from something in all of them.
523 $querydata->q = 'Some';
524 $this->assertCount(1, $this->search->search($querydata));
525 }
526
053118a1
EM
527 /**
528 * Test indexing a file we don't consider indexable.
529 */
cd894f84 530 public function test_index_filtered_file() {
053118a1
EM
531 // Get area to work with.
532 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
cd894f84
EM
533 $area = \core_search\manager::get_search_area($areaid);
534
535 // Get a single record to make a doc from.
053118a1 536 $record = $this->generator->create_record();
cd894f84
EM
537
538 $doc = $area->get_document($record);
539
540 // Now we are going to make some files.
541 $fs = get_file_storage();
542 $syscontext = \context_system::instance();
543
cd894f84 544 // We need to make a file greater than 1kB in size, which is the lowest filter size.
053118a1
EM
545 $filerecord = new \stdClass();
546 $filerecord->filename = 'largefile';
547 $filerecord->content = 'Some LargeFindContent to find.';
cd894f84 548 for ($i = 0; $i < 200; $i++) {
053118a1 549 $filerecord->content .= ' The quick brown fox jumps over the lazy dog.';
cd894f84
EM
550 }
551
053118a1 552 $this->assertGreaterThan(1024, strlen($filerecord->content));
cd894f84 553
053118a1 554 $file = $this->generator->create_file($filerecord);
cd894f84
EM
555 $doc->add_stored_file($file);
556
053118a1
EM
557 $filerecord->filename = 'smallfile';
558 $filerecord->content = 'Some SmallFindContent to find.';
559 $file = $this->generator->create_file($filerecord);
cd894f84
EM
560 $doc->add_stored_file($file);
561
053118a1
EM
562 $this->engine->add_document($doc, true);
563 $this->engine->area_index_complete($area->get_area_id());
cd894f84
EM
564
565 $querydata = new stdClass();
566 // We shouldn't be able to find the large file contents.
567 $querydata->q = 'LargeFindContent';
568 $this->assertCount(0, $this->search->search($querydata));
569
570 // But we should be able to find the filename.
571 $querydata->q = 'largefile';
572 $this->assertCount(1, $this->search->search($querydata));
573
574 // We should be able to find the small file contents.
575 $querydata->q = 'SmallFindContent';
576 $this->assertCount(1, $this->search->search($querydata));
577
578 // And we should be able to find the filename.
579 $querydata->q = 'smallfile';
580 $this->assertCount(1, $this->search->search($querydata));
581 }
582
583 public function test_delete_by_id() {
cd894f84 584 // First get files in the index.
053118a1
EM
585 $file = $this->generator->create_file();
586 $record = new \stdClass();
587 $record->attachfileids = array($file->get_id());
588 $this->generator->create_record($record);
589 $this->generator->create_record($record);
cd894f84 590 $this->search->index();
cd894f84
EM
591
592 $querydata = new stdClass();
593
594 // Then search to make sure they are there.
595 $querydata->q = '"File contents"';
596 $results = $this->search->search($querydata);
597 $this->assertCount(2, $results);
598
599 $first = reset($results);
600 $deleteid = $first->get('id');
601
053118a1 602 $this->engine->delete_by_id($deleteid);
cd894f84
EM
603
604 // Check that we don't get a result for it anymore.
605 $results = $this->search->search($querydata);
606 $this->assertCount(1, $results);
607 $result = reset($results);
608 $this->assertNotEquals($deleteid, $result->get('id'));
609 }
053118a1
EM
610
611 /**
612 * Test that expected results are returned, even with low check_access success rate.
613 *
614 * @dataProvider file_indexing_provider
615 */
616 public function test_solr_filling($fileindexing) {
617 $this->engine->test_set_config('fileindexing', $fileindexing);
618
619 $user1 = self::getDataGenerator()->create_user();
620 $user2 = self::getDataGenerator()->create_user();
621
622 // We are going to create a bunch of records that user 1 can see with 2 keywords.
623 // Then we are going to create a bunch for user 2 with only 1 of the keywords.
624 // If user 2 searches for both keywords, solr will return all of the user 1 results, then the user 2 results.
625 // This is because the user 1 results will match 2 keywords, while the others will match only 1.
626
627 $record = new \stdClass();
628
629 // First create a bunch of records for user 1 to see.
630 $record->denyuserids = array($user2->id);
631 $record->content = 'Something1 Something2';
632 $maxresults = (int)(\core_search\manager::MAX_RESULTS * .75);
633 for ($i = 0; $i < $maxresults; $i++) {
634 $this->generator->create_record($record);
635 }
636
637 // Then create a bunch of records for user 2 to see.
638 $record->denyuserids = array($user1->id);
639 $record->content = 'Something1';
640 for ($i = 0; $i < $maxresults; $i++) {
641 $this->generator->create_record($record);
642 }
643
644 $this->search->index();
645
646 // Check that user 1 sees all their results.
647 $this->setUser($user1);
648 $querydata = new stdClass();
649 $querydata->q = 'Something1 Something2';
650 $results = $this->search->search($querydata);
651 $this->assertCount($maxresults, $results);
652
653 // Check that user 2 will see theirs, even though they may be crouded out.
654 $this->setUser($user2);
655 $results = $this->search->search($querydata);
656 $this->assertCount($maxresults, $results);
657 }
658
659 /**
660 * Create 40 docs, that will be return from Solr in 10 hidden, 10 visible, 10 hidden, 10 visible if you query for:
661 * Something1 Something2 Something3 Something4, with the specified user set.
662 */
663 protected function setup_user_hidden_docs($user) {
664 // These results will come first, and will not be visible by the user.
665 $record = new \stdClass();
666 $record->denyuserids = array($user->id);
667 $record->content = 'Something1 Something2 Something3 Something4';
668 for ($i = 0; $i < 10; $i++) {
669 $this->generator->create_record($record);
670 }
671
672 // These results will come second, and will be visible by the user.
673 unset($record->denyuserids);
674 $record->content = 'Something1 Something2 Something3';
675 for ($i = 0; $i < 10; $i++) {
676 $this->generator->create_record($record);
677 }
678
679 // These results will come third, and will not be visible by the user.
680 $record->denyuserids = array($user->id);
681 $record->content = 'Something1 Something2';
682 for ($i = 0; $i < 10; $i++) {
683 $this->generator->create_record($record);
684 }
685
686 // These results will come fourth, and will be visible by the user.
687 unset($record->denyuserids);
688 $record->content = 'Something1 ';
689 for ($i = 0; $i < 10; $i++) {
690 $this->generator->create_record($record);
691 }
692 }
693
694 /**
695 * Test that counts are what we expect.
696 *
697 * @dataProvider file_indexing_provider
698 */
699 public function test_get_query_total_count($fileindexing) {
700 $this->engine->test_set_config('fileindexing', $fileindexing);
701
702 $user = self::getDataGenerator()->create_user();
703 $this->setup_user_hidden_docs($user);
704 $this->search->index();
705
706 $this->setUser($user);
707 $querydata = new stdClass();
708 $querydata->q = 'Something1 Something2 Something3 Something4';
709
710 // In this first set, it should have determined the first 10 of 40 are bad, so there could be up to 30 left.
163d2800 711 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 5);
053118a1
EM
712 $this->assertEquals(30, $this->engine->get_query_total_count());
713 $this->assertCount(5, $results);
714
715 // To get to 15, it has to process the first 10 that are bad, 10 that are good, 10 that are bad, then 5 that are good.
716 // So we now know 20 are bad out of 40.
163d2800 717 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 15);
053118a1
EM
718 $this->assertEquals(20, $this->engine->get_query_total_count());
719 $this->assertCount(15, $results);
720
721 // Try to get more then all, make sure we still see 20 count and 20 returned.
163d2800 722 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 30);
053118a1
EM
723 $this->assertEquals(20, $this->engine->get_query_total_count());
724 $this->assertCount(20, $results);
725 }
726
727 /**
728 * Test that paged results are what we expect.
729 *
730 * @dataProvider file_indexing_provider
731 */
732 public function test_manager_paged_search($fileindexing) {
733 $this->engine->test_set_config('fileindexing', $fileindexing);
734
735 $user = self::getDataGenerator()->create_user();
736 $this->setup_user_hidden_docs($user);
737 $this->search->index();
738
739 // Check that user 1 sees all their results.
740 $this->setUser($user);
741 $querydata = new stdClass();
742 $querydata->q = 'Something1 Something2 Something3 Something4';
743
744 // On this first page, it should have determined the first 10 of 40 are bad, so there could be up to 30 left.
745 $results = $this->search->paged_search($querydata, 0);
746 $this->assertEquals(30, $results->totalcount);
747 $this->assertCount(10, $results->results);
748 $this->assertEquals(0, $results->actualpage);
749
750 // On the second page, it should have found the next 10 bad ones, so we no know there are only 20 total.
751 $results = $this->search->paged_search($querydata, 1);
752 $this->assertEquals(20, $results->totalcount);
753 $this->assertCount(10, $results->results);
754 $this->assertEquals(1, $results->actualpage);
755
756 // Try to get an additional page - we should get back page 1 results, since that is the last page with valid results.
757 $results = $this->search->paged_search($querydata, 2);
758 $this->assertEquals(20, $results->totalcount);
759 $this->assertCount(10, $results->results);
760 $this->assertEquals(1, $results->actualpage);
761 }
cfa00fc5 762
763 /**
764 * Tests searching for results restricted to context id.
765 */
766 public function test_context_restriction() {
767 // Use real search areas.
768 $this->search->clear_static();
769 $this->search->add_core_search_areas();
770
771 // Create 2 courses and some forums.
772 $generator = $this->getDataGenerator();
773 $course1 = $generator->create_course(['fullname' => 'Course 1', 'summary' => 'xyzzy']);
774 $contextc1 = \context_course::instance($course1->id);
775 $course1forum1 = $generator->create_module('forum', ['course' => $course1,
776 'name' => 'C1F1', 'intro' => 'xyzzy']);
777 $contextc1f1 = \context_module::instance($course1forum1->cmid);
778 $course1forum2 = $generator->create_module('forum', ['course' => $course1,
779 'name' => 'C1F2', 'intro' => 'xyzzy']);
780 $contextc1f2 = \context_module::instance($course1forum2->cmid);
781 $course2 = $generator->create_course(['fullname' => 'Course 2', 'summary' => 'xyzzy']);
782 $contextc2 = \context_course::instance($course1->id);
783 $course2forum = $generator->create_module('forum', ['course' => $course2,
784 'name' => 'C2F', 'intro' => 'xyzzy']);
785 $contextc2f = \context_module::instance($course2forum->cmid);
786
787 // Index the courses and forums.
788 $this->search->index();
789
790 // Search as admin user should find everything.
791 $querydata = new stdClass();
792 $querydata->q = 'xyzzy';
793 $results = $this->search->search($querydata);
794 $this->assert_result_titles(
795 ['Course 1', 'Course 2', 'C1F1', 'C1F2', 'C2F'], $results);
796
797 // Admin user manually restricts results by context id to include one course and one forum.
798 $querydata->contextids = [$contextc2f->id, $contextc1->id];
799 $results = $this->search->search($querydata);
800 $this->assert_result_titles(['Course 1', 'C2F'], $results);
801
802 // Student enrolled in only one course, same restriction, only has the available results.
803 $student2 = $generator->create_user();
804 $generator->enrol_user($student2->id, $course2->id, 'student');
805 $this->setUser($student2);
806 $results = $this->search->search($querydata);
807 $this->assert_result_titles(['C2F'], $results);
808
809 // Student enrolled in both courses, same restriction, same results as admin.
810 $student1 = $generator->create_user();
811 $generator->enrol_user($student1->id, $course1->id, 'student');
812 $generator->enrol_user($student1->id, $course2->id, 'student');
813 $this->setUser($student1);
814 $results = $this->search->search($querydata);
815 $this->assert_result_titles(['Course 1', 'C2F'], $results);
816
817 // Restrict both course and context.
818 $querydata->courseids = [$course2->id];
819 $results = $this->search->search($querydata);
820 $this->assert_result_titles(['C2F'], $results);
821 unset($querydata->courseids);
822
823 // Restrict both area and context.
2085e860 824 $querydata->areaids = ['core_course-course'];
cfa00fc5 825 $results = $this->search->search($querydata);
826 $this->assert_result_titles(['Course 1'], $results);
827
828 // Restrict area and context, incompatibly - this has no results (and doesn't do a query).
829 $querydata->contextids = [$contextc2f->id];
830 $results = $this->search->search($querydata);
831 $this->assert_result_titles([], $results);
832 }
833
163d2800 834 /**
835 * Tests searching for results in groups, either by specified group ids or based on user
836 * access permissions.
837 */
838 public function test_groups() {
839 global $USER;
840
841 // Use real search areas.
842 $this->search->clear_static();
843 $this->search->add_core_search_areas();
844
845 // Create 2 courses and a selection of forums with different group mode.
846 $generator = $this->getDataGenerator();
847 $course1 = $generator->create_course(['fullname' => 'Course 1']);
848 $forum1nogroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => NOGROUPS]);
849 $forum1separategroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => SEPARATEGROUPS]);
850 $forum1visiblegroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => VISIBLEGROUPS]);
851 $course2 = $generator->create_course(['fullname' => 'Course 2']);
852 $forum2separategroups = $generator->create_module('forum', ['course' => $course2, 'groupmode' => SEPARATEGROUPS]);
853
854 // Create two groups on each course.
855 $group1a = $generator->create_group(['courseid' => $course1->id]);
856 $group1b = $generator->create_group(['courseid' => $course1->id]);
857 $group2a = $generator->create_group(['courseid' => $course2->id]);
858 $group2b = $generator->create_group(['courseid' => $course2->id]);
859
860 // Create search records in each activity and (where relevant) in each group.
861 $forumgenerator = $generator->get_plugin_generator('mod_forum');
862 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
863 'forum' => $forum1nogroups->id, 'name' => 'F1NG', 'message' => 'xyzzy']);
864 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
865 'forum' => $forum1separategroups->id, 'name' => 'F1SG-A', 'message' => 'xyzzy',
866 'groupid' => $group1a->id]);
867 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
868 'forum' => $forum1separategroups->id, 'name' => 'F1SG-B', 'message' => 'xyzzy',
869 'groupid' => $group1b->id]);
870 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
871 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-A', 'message' => 'xyzzy',
872 'groupid' => $group1a->id]);
873 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id,
874 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-B', 'message' => 'xyzzy',
875 'groupid' => $group1b->id]);
876 $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id,
877 'forum' => $forum2separategroups->id, 'name' => 'F2SG-A', 'message' => 'xyzzy',
878 'groupid' => $group2a->id]);
879 $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id,
880 'forum' => $forum2separategroups->id, 'name' => 'F2SG-B', 'message' => 'xyzzy',
881 'groupid' => $group2b->id]);
882
883 $this->search->index();
884
885 // Search as admin user should find everything.
886 $querydata = new stdClass();
887 $querydata->q = 'xyzzy';
888 $results = $this->search->search($querydata);
889 $this->assert_result_titles(
890 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results);
891
892 // Admin user manually restricts results by groups.
893 $querydata->groupids = [$group1b->id, $group2a->id];
894 $results = $this->search->search($querydata);
895 $this->assert_result_titles(['F1SG-B', 'F1VG-B', 'F2SG-A'], $results);
896
897 // Student enrolled in both courses but no groups.
898 $student1 = $generator->create_user();
899 $generator->enrol_user($student1->id, $course1->id, 'student');
900 $generator->enrol_user($student1->id, $course2->id, 'student');
901 $this->setUser($student1);
902
903 unset($querydata->groupids);
904 $results = $this->search->search($querydata);
905 $this->assert_result_titles(['F1NG', 'F1VG-A', 'F1VG-B'], $results);
906
907 // Student enrolled in both courses and group A in both cases.
908 $student2 = $generator->create_user();
909 $generator->enrol_user($student2->id, $course1->id, 'student');
910 $generator->enrol_user($student2->id, $course2->id, 'student');
911 groups_add_member($group1a, $student2);
912 groups_add_member($group2a, $student2);
913 $this->setUser($student2);
914
915 $results = $this->search->search($querydata);
916 $this->assert_result_titles(['F1NG', 'F1SG-A', 'F1VG-A', 'F1VG-B', 'F2SG-A'], $results);
917
918 // Manually restrict results to group B in course 1.
919 $querydata->groupids = [$group1b->id];
920 $results = $this->search->search($querydata);
921 $this->assert_result_titles(['F1VG-B'], $results);
922
923 // Manually restrict results to group A in course 1.
924 $querydata->groupids = [$group1a->id];
925 $results = $this->search->search($querydata);
926 $this->assert_result_titles(['F1SG-A', 'F1VG-A'], $results);
927
928 // Manager enrolled in both courses (has access all groups).
929 $manager = $generator->create_user();
930 $generator->enrol_user($manager->id, $course1->id, 'manager');
931 $generator->enrol_user($manager->id, $course2->id, 'manager');
932 $this->setUser($manager);
933 unset($querydata->groupids);
934 $results = $this->search->search($querydata);
935 $this->assert_result_titles(
936 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results);
937 }
938
222a97ce 939 /**
940 * Tests searching for results restricted to specific user id(s).
941 */
942 public function test_user_restriction() {
943 // Use real search areas.
944 $this->search->clear_static();
945 $this->search->add_core_search_areas();
946
947 // Create a course, a forum, and a glossary.
948 $generator = $this->getDataGenerator();
949 $course = $generator->create_course();
950 $forum = $generator->create_module('forum', ['course' => $course->id]);
951 $glossary = $generator->create_module('glossary', ['course' => $course->id]);
952
953 // Create 3 user accounts, all enrolled as students on the course.
954 $user1 = $generator->create_user();
955 $user2 = $generator->create_user();
956 $user3 = $generator->create_user();
957 $generator->enrol_user($user1->id, $course->id, 'student');
958 $generator->enrol_user($user2->id, $course->id, 'student');
959 $generator->enrol_user($user3->id, $course->id, 'student');
960
961 // All users create a forum discussion.
962 $forumgen = $generator->get_plugin_generator('mod_forum');
963 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
964 'userid' => $user1->id, 'name' => 'Post1', 'message' => 'plugh']);
965 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
966 'userid' => $user2->id, 'name' => 'Post2', 'message' => 'plugh']);
967 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
968 'userid' => $user3->id, 'name' => 'Post3', 'message' => 'plugh']);
969
970 // Two of the users create entries in the glossary.
971 $glossarygen = $generator->get_plugin_generator('mod_glossary');
972 $glossarygen->create_content($glossary, ['concept' => 'Entry1', 'definition' => 'plugh',
973 'userid' => $user1->id]);
974 $glossarygen->create_content($glossary, ['concept' => 'Entry3', 'definition' => 'plugh',
975 'userid' => $user3->id]);
976
977 // Index the data.
978 $this->search->index();
979
980 // Search without user restriction should find everything.
981 $querydata = new stdClass();
982 $querydata->q = 'plugh';
983 $results = $this->search->search($querydata);
984 $this->assert_result_titles(
985 ['Entry1', 'Entry3', 'Post1', 'Post2', 'Post3'], $results);
986
987 // Restriction to user 3 only.
988 $querydata->userids = [$user3->id];
989 $results = $this->search->search($querydata);
990 $this->assert_result_titles(
991 ['Entry3', 'Post3'], $results);
992
993 // Restriction to users 1 and 2.
994 $querydata->userids = [$user1->id, $user2->id];
995 $results = $this->search->search($querydata);
996 $this->assert_result_titles(
997 ['Entry1', 'Post1', 'Post2'], $results);
998
999 // Restriction to users 1 and 2 combined with context restriction.
1000 $querydata->contextids = [context_module::instance($glossary->cmid)->id];
1001 $results = $this->search->search($querydata);
1002 $this->assert_result_titles(
1003 ['Entry1'], $results);
1004
1005 // Restriction to users 1 and 2 combined with area restriction.
1006 unset($querydata->contextids);
1007 $querydata->areaids = [\core_search\manager::generate_areaid('mod_forum', 'post')];
1008 $results = $this->search->search($querydata);
1009 $this->assert_result_titles(
1010 ['Post1', 'Post2'], $results);
1011 }
1012
c2072891 1013 /**
1014 * Tests searching for results containing words in italic text. (This used to fail.)
1015 */
1016 public function test_italics() {
1017 global $USER;
1018
1019 // Use real search areas.
1020 $this->search->clear_static();
1021 $this->search->add_core_search_areas();
1022
1023 // Create a course and a forum.
1024 $generator = $this->getDataGenerator();
1025 $course = $generator->create_course();
1026 $forum = $generator->create_module('forum', ['course' => $course->id]);
1027
1028 // As admin user, create forum discussions with various words in italics or with underlines.
1029 $this->setAdminUser();
1030 $forumgen = $generator->get_plugin_generator('mod_forum');
1031 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
1032 'userid' => $USER->id, 'name' => 'Post1',
1033 'message' => '<p>This is a post about <i>frogs</i>.</p>']);
1034 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
1035 'userid' => $USER->id, 'name' => 'Post2',
1036 'message' => '<p>This is a post about <i>toads and zombies</i>.</p>']);
1037 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
1038 'userid' => $USER->id, 'name' => 'Post3',
1039 'message' => '<p>This is a post about toads_and_zombies.</p>']);
1040 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
1041 'userid' => $USER->id, 'name' => 'Post4',
1042 'message' => '<p>This is a post about _leading and trailing_ underlines.</p>']);
1043
1044 // Index the data.
1045 $this->search->index();
1046
1047 // Search for 'frogs' should find the post.
1048 $querydata = new stdClass();
1049 $querydata->q = 'frogs';
1050 $results = $this->search->search($querydata);
1051 $this->assert_result_titles(['Post1'], $results);
1052
1053 // Search for 'toads' or 'zombies' should find post 2 (and not 3)...
1054 $querydata->q = 'toads';
1055 $results = $this->search->search($querydata);
1056 $this->assert_result_titles(['Post2'], $results);
1057 $querydata->q = 'zombies';
1058 $results = $this->search->search($querydata);
1059 $this->assert_result_titles(['Post2'], $results);
1060
1061 // Search for 'toads_and_zombies' should find post 3.
1062 $querydata->q = 'toads_and_zombies';
1063 $results = $this->search->search($querydata);
1064 $this->assert_result_titles(['Post3'], $results);
1065
1066 // Search for '_leading' or 'trailing_' should find post 4.
1067 $querydata->q = '_leading';
1068 $results = $this->search->search($querydata);
1069 $this->assert_result_titles(['Post4'], $results);
1070 $querydata->q = 'trailing_';
1071 $results = $this->search->search($querydata);
1072 $this->assert_result_titles(['Post4'], $results);
1073 }
1074
cfa00fc5 1075 /**
1076 * Asserts that the returned documents have the expected titles (regardless of order).
1077 *
1078 * @param string[] $expected List of expected document titles
1079 * @param \core_search\document[] $results List of returned documents
1080 */
1081 protected function assert_result_titles(array $expected, array $results) {
1082 $titles = [];
1083 foreach ($results as $result) {
1084 $titles[] = $result->get('title');
1085 }
1086 sort($titles);
1087 sort($expected);
1088 $this->assertEquals($expected, $titles);
1089 }
fc440796 1090
1091 /**
1092 * Tests the get_supported_orders function for contexts where we can only use relevance
1093 * (system, category).
1094 */
1095 public function test_get_supported_orders_relevance_only() {
1096 global $DB;
1097
1098 // System or category context: relevance only.
1099 $orders = $this->engine->get_supported_orders(\context_system::instance());
1100 $this->assertCount(1, $orders);
1101 $this->assertArrayHasKey('relevance', $orders);
1102
1103 $categoryid = $DB->get_field_sql('SELECT MIN(id) FROM {course_categories}');
1104 $orders = $this->engine->get_supported_orders(\context_coursecat::instance($categoryid));
1105 $this->assertCount(1, $orders);
1106 $this->assertArrayHasKey('relevance', $orders);
1107 }
1108
1109 /**
1110 * Tests the get_supported_orders function for contexts where we support location as well
1111 * (course, activity, block).
1112 */
1113 public function test_get_supported_orders_relevance_and_location() {
1114 global $DB;
1115
1116 // Test with course context.
1117 $generator = $this->getDataGenerator();
1118 $course = $generator->create_course(['fullname' => 'Frogs']);
1119 $coursecontext = \context_course::instance($course->id);
1120
1121 $orders = $this->engine->get_supported_orders($coursecontext);
1122 $this->assertCount(2, $orders);
1123 $this->assertArrayHasKey('relevance', $orders);
1124 $this->assertArrayHasKey('location', $orders);
1125 $this->assertContains('Course: Frogs', $orders['location']);
1126
1127 // Test with activity context.
1128 $page = $generator->create_module('page', ['course' => $course->id, 'name' => 'Toads']);
1129
1130 $orders = $this->engine->get_supported_orders(\context_module::instance($page->cmid));
1131 $this->assertCount(2, $orders);
1132 $this->assertArrayHasKey('relevance', $orders);
1133 $this->assertArrayHasKey('location', $orders);
1134 $this->assertContains('Page: Toads', $orders['location']);
1135
1136 // Test with block context.
1137 $instance = (object)['blockname' => 'html', 'parentcontextid' => $coursecontext->id,
1138 'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*',
1139 'defaultweight' => 0, 'timecreated' => 1, 'timemodified' => 1,
1140 'configdata' => ''];
1141 $blockid = $DB->insert_record('block_instances', $instance);
1142 $blockcontext = \context_block::instance($blockid);
1143
1144 $orders = $this->engine->get_supported_orders($blockcontext);
1145 $this->assertCount(2, $orders);
1146 $this->assertArrayHasKey('relevance', $orders);
1147 $this->assertArrayHasKey('location', $orders);
1148 $this->assertContains('Block: HTML', $orders['location']);
1149 }
1150
1151 /**
1152 * Tests ordering by relevance vs location.
1153 */
1154 public function test_ordering() {
1155 // Create 2 courses and 2 activities.
1156 $generator = $this->getDataGenerator();
1157 $course1 = $generator->create_course(['fullname' => 'Course 1']);
1158 $course1context = \context_course::instance($course1->id);
1159 $course1page = $generator->create_module('page', ['course' => $course1]);
1160 $course1pagecontext = \context_module::instance($course1page->cmid);
1161 $course2 = $generator->create_course(['fullname' => 'Course 2']);
1162 $course2context = \context_course::instance($course2->id);
1163 $course2page = $generator->create_module('page', ['course' => $course2]);
1164 $course2pagecontext = \context_module::instance($course2page->cmid);
1165
1166 // Create one search record in each activity and course.
1167 $this->create_search_record($course1->id, $course1context->id, 'C1', 'Xyzzy');
1168 $this->create_search_record($course1->id, $course1pagecontext->id, 'C1P', 'Xyzzy');
1169 $this->create_search_record($course2->id, $course2context->id, 'C2', 'Xyzzy');
1170 $this->create_search_record($course2->id, $course2pagecontext->id, 'C2P', 'Xyzzy plugh');
1171 $this->search->index();
1172
1173 // Default search works by relevance so the one with both words should be top.
1174 $querydata = new stdClass();
1175 $querydata->q = 'xyzzy plugh';
1176 $results = $this->search->search($querydata);
1177 $this->assertCount(4, $results);
1178 $this->assertEquals('C2P', $results[0]->get('title'));
1179
1180 // Same if you explicitly specify relevance.
1181 $querydata->order = 'relevance';
1182 $results = $this->search->search($querydata);
1183 $this->assertEquals('C2P', $results[0]->get('title'));
1184
1185 // If you specify order by location and you are in C2 or C2P then results are the same.
1186 $querydata->order = 'location';
1187 $querydata->context = $course2context;
1188 $results = $this->search->search($querydata);
1189 $this->assertEquals('C2P', $results[0]->get('title'));
1190 $querydata->context = $course2pagecontext;
1191 $results = $this->search->search($querydata);
1192 $this->assertEquals('C2P', $results[0]->get('title'));
1193
1194 // But if you are in C1P then you get different results (C1P first).
1195 $querydata->context = $course1pagecontext;
1196 $results = $this->search->search($querydata);
1197 $this->assertEquals('C1P', $results[0]->get('title'));
1198 }
1199
ffa868a9 1200 /**
1201 * Tests with bogus content (that can be entered into Moodle) to see if it crashes.
1202 */
1203 public function test_bogus_content() {
1204 $generator = $this->getDataGenerator();
1205 $course1 = $generator->create_course(['fullname' => 'Course 1']);
1206 $course1context = \context_course::instance($course1->id);
1207
1208 // It is possible to enter into a Moodle database content containing these characters,
1209 // which are Unicode non-characters / byte order marks. If sent to Solr, these cause
1210 // failures.
1211 $boguscontent = html_entity_decode('&#xfffe;') . 'frog';
1212 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent);
1213 $boguscontent = html_entity_decode('&#xffff;') . 'frog';
1214 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent);
1215
1216 // Unicode Standard Version 9.0 - Core Specification, section 23.7, lists 66 non-characters
1217 // in total. Here are some of them - these work OK for me but it may depend on platform.
1218 $boguscontent = html_entity_decode('&#xfdd0;') . 'frog';
1219 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent);
1220 $boguscontent = html_entity_decode('&#xfdef;') . 'frog';
1221 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent);
1222 $boguscontent = html_entity_decode('&#x1fffe;') . 'frog';
1223 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent);
1224 $boguscontent = html_entity_decode('&#x10ffff;') . 'frog';
1225 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent);
1226
1227 // Do the indexing (this will check it doesn't throw warnings).
1228 $this->search->index();
1229
1230 // Confirm that all 6 documents are found in search.
1231 $querydata = new stdClass();
1232 $querydata->q = 'frog';
1233 $results = $this->search->search($querydata);
1234 $this->assertCount(6, $results);
1235 }
1236
fc440796 1237 /**
1238 * Adds a record to the mock search area, so that the search engine can find it later.
1239 *
1240 * @param int $courseid Course id
1241 * @param int $contextid Context id
1242 * @param string $title Title for search index
1243 * @param string $content Content for search index
1244 */
1245 protected function create_search_record($courseid, $contextid, $title, $content) {
1246 $record = new \stdClass();
1247 $record->content = $content;
1248 $record->title = $title;
1249 $record->courseid = $courseid;
1250 $record->contextid = $contextid;
1251 $this->generator->create_record($record);
1252 }
95c6aeaf 1253}