Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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