Proyectos de Subversion Moodle

Rev

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