Passed
Pull Request — 4 (#10382)
by Guy
06:55
created

SearchContextTest::testPrimarySearchFilterIsUsed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 0
dl 0
loc 21
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Tests\Search;
4
5
use LogicException;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Dev\SapphireTest;
8
use SilverStripe\Forms\TextField;
9
use SilverStripe\Forms\TextareaField;
10
use SilverStripe\Forms\NumericField;
11
use SilverStripe\Forms\DropdownField;
12
use SilverStripe\Forms\CheckboxField;
13
use SilverStripe\Forms\FieldList;
14
use SilverStripe\Forms\HiddenField;
15
use SilverStripe\ORM\DataList;
16
use SilverStripe\ORM\Filters\EndsWithFilter;
17
use SilverStripe\ORM\Filters\ExactMatchFilter;
18
use SilverStripe\ORM\Filters\PartialMatchFilter;
19
use SilverStripe\ORM\Search\SearchContext;
20
21
class SearchContextTest extends SapphireTest
22
{
23
24
    protected static $fixture_file = 'SearchContextTest.yml';
25
26
    protected static $extra_dataobjects = [
27
        SearchContextTest\Person::class,
28
        SearchContextTest\NoSearchableFields::class,
29
        SearchContextTest\Book::class,
30
        SearchContextTest\Company::class,
31
        SearchContextTest\Project::class,
32
        SearchContextTest\Deadline::class,
33
        SearchContextTest\Action::class,
34
        SearchContextTest\AllFilterTypes::class,
35
        SearchContextTest\Customer::class,
36
        SearchContextTest\Address::class,
37
        SearchContextTest\Order::class,
38
        SearchContextTest\PrimarySearch::class,
39
    ];
40
41
    public function testResultSetFilterReturnsExpectedCount()
42
    {
43
        $person = SearchContextTest\Person::singleton();
44
        $context = $person->getDefaultSearchContext();
45
        $results = $context->getResults(['Name' => '']);
46
        $this->assertEquals(5, $results->Count());
47
48
        $results = $context->getResults(['EyeColor' => 'green']);
49
        $this->assertEquals(2, $results->Count());
50
51
        $results = $context->getResults(['EyeColor' => 'green', 'HairColor' => 'black']);
52
        $this->assertEquals(1, $results->Count());
53
    }
54
55
    public function testSearchableFieldsDefaultsToSummaryFields()
56
    {
57
        $obj = new SearchContextTest\NoSearchableFields();
58
        $summaryFields = $obj->summaryFields();
59
        $expected = [];
60
        foreach ($summaryFields as $field => $label) {
61
            $expected[$field] = [
62
                'title' => $obj->fieldLabel($field),
63
                'filter' => 'PartialMatchFilter',
64
            ];
65
        }
66
        $this->assertEquals($expected, $obj->searchableFields());
67
    }
68
69
    public function testSummaryIncludesDefaultFieldsIfNotDefined()
70
    {
71
        $person = SearchContextTest\Person::singleton();
72
        $this->assertContains('Name', $person->summaryFields());
73
74
        $book = SearchContextTest\Book::singleton();
75
        $this->assertContains('Title', $book->summaryFields());
76
    }
77
78
    public function testAccessDefinedSummaryFields()
79
    {
80
        $company = SearchContextTest\Company::singleton();
81
        $this->assertContains('Industry', $company->summaryFields());
82
    }
83
84
    public function testPartialMatchUsedByDefaultWhenNotExplicitlySet()
85
    {
86
        $person = SearchContextTest\Person::singleton();
87
        $context = $person->getDefaultSearchContext();
88
89
        $this->assertEquals(
90
            [
91
                "Name" => new PartialMatchFilter("Name"),
92
                "HairColor" => new PartialMatchFilter("HairColor"),
93
                "EyeColor" => new PartialMatchFilter("EyeColor")
94
            ],
95
            $context->getFilters()
96
        );
97
    }
98
99
    public function testDefaultFiltersDefinedWhenNotSetInDataObject()
100
    {
101
        $book = SearchContextTest\Book::singleton();
102
        $context = $book->getDefaultSearchContext();
103
104
        $this->assertEquals(
105
            [
106
                "Title" => new PartialMatchFilter("Title")
107
            ],
108
            $context->getFilters()
109
        );
110
    }
111
112
    public function testUserDefinedFiltersAppearInSearchContext()
113
    {
114
        $company = SearchContextTest\Company::singleton();
115
        $context = $company->getDefaultSearchContext();
116
117
        $this->assertEquals(
118
            [
119
                "Name" => new PartialMatchFilter("Name"),
120
                "Industry" => new PartialMatchFilter("Industry"),
121
                "AnnualProfit" => new PartialMatchFilter("AnnualProfit")
122
            ],
123
            $context->getFilters()
124
        );
125
    }
126
127
    public function testUserDefinedFieldsAppearInSearchContext()
128
    {
129
        $company = SearchContextTest\Company::singleton();
130
        $context = $company->getDefaultSearchContext();
131
        $this->assertEquals(
132
            new FieldList(
133
                new HiddenField($company->primarySearchFieldName(), 'Primary Search'),
134
                (new TextField("Name", 'Name'))
135
                    ->setMaxLength(255),
136
                new TextareaField("Industry", 'Industry'),
137
                new NumericField("AnnualProfit", 'The Almighty Annual Profit')
138
            ),
139
            $context->getFields()
140
        );
141
    }
142
143
    public function testRelationshipObjectsLinkedInSearch()
144
    {
145
        $action3 = $this->objFromFixture(SearchContextTest\Action::class, 'action3');
146
147
        $project = SearchContextTest\Project::singleton();
148
        $context = $project->getDefaultSearchContext();
149
150
        $params = ["Name" => "Blog Website", "Actions__SolutionArea" => "technical"];
151
152
        /** @var DataList $results */
153
        $results = $context->getResults($params);
154
155
        $this->assertEquals(1, $results->count());
156
157
        /** @var SearchContextTest\Project $project */
158
        $project = $results->first();
159
160
        $this->assertInstanceOf(SearchContextTest\Project::class, $project);
161
        $this->assertEquals("Blog Website", $project->Name);
162
        $this->assertEquals(2, $project->Actions()->Count());
163
164
        $this->assertEquals(
165
            "Get RSS feeds working",
166
            $project->Actions()->find('ID', $action3->ID)->Description
0 ignored issues
show
Bug Best Practice introduced by
The property Description does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
167
        );
168
    }
169
170
    public function testCanGenerateQueryUsingAllFilterTypes()
171
    {
172
        $all = SearchContextTest\AllFilterTypes::singleton();
173
        $context = $all->getDefaultSearchContext();
174
        $params = [
175
            "ExactMatch" => "Match me exactly",
176
            "PartialMatch" => "partially",
177
            "CollectionMatch" => [
178
                "ExistingCollectionValue",
179
                "NonExistingCollectionValue",
180
                4,
181
                "Inline'Quotes'"
182
            ],
183
            "StartsWith" => "12345",
184
            "EndsWith" => "ijkl",
185
            "Fulltext" => "two"
186
        ];
187
188
        $results = $context->getResults($params);
189
        $this->assertEquals(1, $results->Count());
190
        $this->assertEquals("Filtered value", $results->First()->HiddenValue);
191
    }
192
193
    public function testStartsWithFilterCaseInsensitive()
194
    {
195
        $all = SearchContextTest\AllFilterTypes::singleton();
196
        $context = $all->getDefaultSearchContext();
197
        $params = [
198
            "StartsWith" => "12345-6789 camelcase", // spelled lowercase
199
        ];
200
201
        $results = $context->getResults($params);
202
        $this->assertEquals(1, $results->Count());
203
        $this->assertEquals("Filtered value", $results->First()->HiddenValue);
204
    }
205
206
    public function testEndsWithFilterCaseInsensitive()
207
    {
208
        $all = SearchContextTest\AllFilterTypes::singleton();
209
        $context = $all->getDefaultSearchContext();
210
        $params = [
211
            "EndsWith" => "IJKL", // spelled uppercase
212
        ];
213
214
        $results = $context->getResults($params);
215
        $this->assertEquals(1, $results->Count());
216
        $this->assertEquals("Filtered value", $results->First()->HiddenValue);
217
    }
218
219
    public function testSearchContextSummary()
220
    {
221
        $filters = [
222
            'KeywordSearch' => PartialMatchFilter::create('KeywordSearch'),
223
            'Country' => PartialMatchFilter::create('Country'),
224
            'CategoryID' => PartialMatchFilter::create('CategoryID'),
225
            'Featured' => PartialMatchFilter::create('Featured'),
226
            'Nothing' => PartialMatchFilter::create('Nothing'),
227
        ];
228
229
        $fields = FieldList::create(
230
            TextField::create('KeywordSearch', 'Keywords'),
231
            TextField::create('Country', 'Country'),
232
            DropdownField::create('CategoryID', 'Category', [
233
                1 => 'Category one',
234
                2 => 'Category two',
235
            ]),
236
            CheckboxField::create('Featured', 'Featured')
237
        );
238
239
        $context = SearchContext::create(
240
            SearchContextTest\Person::class,
241
            $fields,
242
            $filters
243
        );
244
245
        $context->setSearchParams([
246
            'KeywordSearch' => 'tester',
247
            'Country' => null,
248
            'CategoryID' => 2,
249
            'Featured' => 1,
250
            'Nothing' => 'empty',
251
        ]);
252
253
        $list = $context->getSummary();
254
255
        $this->assertEquals(3, $list->count());
256
        // KeywordSearch should be in the summary
257
        $keyword = $list->find('Field', 'Keywords');
258
        $this->assertNotNull($keyword);
259
        $this->assertEquals('tester', $keyword->Value);
260
261
        // Country should be skipped over
262
        $country = $list->find('Field', 'Country');
263
        $this->assertNull($country);
264
265
        // Category should be expressed as the label
266
        $category = $list->find('Field', 'Category');
267
        $this->assertNotNull($category);
268
        $this->assertEquals('Category two', $category->Value);
269
270
        // Featured should have no value, since it's binary
271
        $featured = $list->find('Field', 'Featured');
272
        $this->assertNotNull($featured);
273
        $this->assertNull($featured->Value);
274
275
        // "Nothing" should come back null since there's no field for it
276
        $nothing = $list->find('Field', 'Nothing');
277
        $this->assertNull($nothing);
278
    }
279
280
    public function testPrimarySearch()
281
    {
282
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
283
        $context = $primary1->getDefaultSearchContext();
284
        $primaryField = $primary1->primarySearchFieldName();
285
286
        // Matches on a variety of fields
287
        $results = $context->getResults([$primaryField => 'Primary']);
288
        $this->assertCount(2, $results);
289
        $this->assertNotContains('MatchNothing', $results->column('Name'));
290
        $results = $context->getResults([$primaryField => 'brown']);
291
        $this->assertCount(1, $results);
292
        $this->assertEquals('Primary One', $results->first()->Name);
0 ignored issues
show
Bug Best Practice introduced by
The property Name does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
293
294
        // Uses its own filter (not field filters)
295
        $results = $context->getResults([$primaryField => 'exact']);
296
        $this->assertCount(1, $results);
297
        $this->assertEquals('Primary One', $results->first()->Name);
298
299
        // Uses match_any fields
300
        $results = $context->getResults([$primaryField => 'first']);
301
        $this->assertCount(1, $results);
302
        $this->assertEquals('Primary One', $results->first()->Name);
303
        // Even across a relation
304
        $results = $context->getResults([$primaryField => 'arbitrary']);
305
        $this->assertCount(1, $results);
306
        $this->assertEquals('Primary One', $results->first()->Name);
307
    }
308
309
    public function testNonPrimarySearchFields()
310
    {
311
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
312
        $context = $primary1->getDefaultSearchContext();
313
        $primaryField = $primary1->primarySearchFieldName();
314
        $results = $context->getResults([$primaryField => $primary1->ExcludeThisField]);
0 ignored issues
show
Bug Best Practice introduced by
The property ExcludeThisField does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
315
        $this->assertNotEmpty($primary1->ExcludeThisField);
316
        $this->assertCount(0, $results);
317
    }
318
319
    public function testPrimaryOnlyUsesSearchableFields()
320
    {
321
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
322
        $context = $primary1->getDefaultSearchContext();
323
        $primaryField = $primary1->primarySearchFieldName();
324
        $results = $context->getResults([$primaryField => $primary1->DoNotUseThisField]);
0 ignored issues
show
Bug Best Practice introduced by
The property DoNotUseThisField does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
325
        $this->assertNotEmpty($primary1->DoNotUseThisField);
326
        $this->assertCount(0, $results);
327
    }
328
329
    public function testPrimarySearchSplitsTerms()
330
    {
331
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
332
        $context = $primary1->getDefaultSearchContext();
333
        $primaryField = $primary1->primarySearchFieldName();
334
335
        // These terms don't exist in a single field in this order on any object
336
        $results = $context->getResults([$primaryField => 'primary blue']);
337
        $this->assertCount(1, $results);
338
        $this->assertEquals('Primary Zero', $results->first()->Name);
0 ignored issues
show
Bug Best Practice introduced by
The property Name does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
339
340
        // These terms exist in a single field, but not in this order.
341
        $results = $context->getResults([$primaryField => 'matches partial']);
342
        $this->assertCount(1, $results);
343
        $this->assertEquals('Primary One', $results->first()->Name);
344
    }
345
346
    public function testPrimarySearchNoSplitTerms()
347
    {
348
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_split_terms', false);
349
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
350
        $context = $primary1->getDefaultSearchContext();
351
        $primaryField = $primary1->primarySearchFieldName();
352
353
        // These terms don't exist in a single field in this order on any object
354
        $results = $context->getResults([$primaryField => 'primary blue']);
355
        $this->assertCount(0, $results);
356
357
        // These terms exist in a single field, but not in this order.
358
        $results = $context->getResults([$primaryField => 'matches partial']);
359
        $this->assertCount(0, $results);
360
361
        // These terms exist in a single field in this order.
362
        $results = $context->getResults([$primaryField => 'partial matches']);
363
        $this->assertCount(1, $results);
364
        $this->assertEquals('Primary One', $results->first()->Name);
0 ignored issues
show
Bug Best Practice introduced by
The property Name does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
365
    }
366
367
    public function testGetPrimarySearchFilter()
368
    {
369
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
370
        $context = $primary1->getDefaultSearchContext();
371
        $primaryField = $primary1->primarySearchFieldName();
0 ignored issues
show
Unused Code introduced by
The assignment to $primaryField is dead and can be removed.
Loading history...
372
373
        // By default, uses the PartialMatchFilter.
374
        $this->assertSame(
375
            PartialMatchFilter::class,
376
            get_class($context->getPrimarySearchFilter($primary1->ClassName, 'ExactMatchField'))
377
        );
378
        $this->assertSame(
379
            PartialMatchFilter::class,
380
            get_class($context->getPrimarySearchFilter($primary1->ClassName, 'PartialMatchField'))
381
        );
382
383
        // Changing the config changes the filter.
384
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_field_filter', EndsWithFilter::class);
385
        $this->assertSame(
386
            EndsWithFilter::class,
387
            get_class($context->getPrimarySearchFilter($primary1->ClassName, 'ExactMatchField'))
388
        );
389
        $this->assertSame(
390
            EndsWithFilter::class,
391
            get_class($context->getPrimarySearchFilter($primary1->ClassName, 'PartialMatchField'))
392
        );
393
394
        // Removing the filter config defaults to use the field's filter.
395
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_field_filter', '');
396
        $this->assertSame(
397
            ExactMatchFilter::class,
398
            get_class($context->getPrimarySearchFilter($primary1->ClassName, 'ExactMatchField'))
399
        );
400
        $this->assertSame(
401
            PartialMatchFilter::class,
402
            get_class($context->getPrimarySearchFilter($primary1->ClassName, 'PartialMatchField'))
403
        );
404
    }
405
406
    public function testPrimarySearchFilterIsUsed()
407
    {
408
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_field_filter', '');
409
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
410
        $context = $primary1->getDefaultSearchContext();
411
        $primaryField = $primary1->primarySearchFieldName();
412
413
        // Respects ExactMatchFilter
414
        $results = $context->getResults([$primaryField => 'exact']);
415
        $this->assertCount(0, $results);
416
        // No match when splitting terms
417
        $results = $context->getResults([$primaryField => 'This requires an exact match']);
418
        $this->assertCount(0, $results);
419
420
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_split_terms', false);
421
        // Respects ExactMatchFilter
422
        $results = $context->getResults([$primaryField => 'exact']);
423
        $this->assertCount(0, $results);
424
        $results = $context->getResults([$primaryField => 'This requires an exact match']);
425
        $this->assertCount(1, $results);
426
        $this->assertEquals('Primary One', $results->first()->Name);
0 ignored issues
show
Bug Best Practice introduced by
The property Name does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
427
    }
428
429
    public function testPrimarySearchDisabled()
430
    {
431
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_field_name', '');
432
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
433
        $context = $primary1->getDefaultSearchContext();
434
        $primaryField = $primary1->primarySearchFieldName();
435
        $this->assertEmpty($primaryField);
436
437
        // Defaults to returning all objects, because the field doesn't exist in the SearchContext
438
        $numObjs = SearchContextTest\PrimarySearch::get()->count();
439
        $results = $context->getResults([$primaryField => 'Primary']);
440
        $this->assertCount($numObjs, $results);
441
        $results = $context->getResults([$primaryField => 'brown']);
442
        $this->assertCount($numObjs, $results);
443
444
        // Searching on other fields still works as expected (e.g. first field, which is the UI default in this situation)
445
        $results = $context->getResults(['Name' => 'Primary']);
446
        $this->assertCount(2, $results);
447
        $this->assertNotContains('MatchNothing', $results->column('Name'));
448
    }
449
450
    public function testPrimarySearchCustomFieldName()
451
    {
452
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_field_name', 'some_arbitrary_field_name');
453
        $obj = new SearchContextTest\PrimarySearch();
454
        $this->assertSame('some_arbitrary_field_name', $obj->primarySearchFieldName());
455
        $this->testPrimarySearch();
456
    }
457
458
    public function testprimarySearchFieldNameMustBeUnique()
459
    {
460
        Config::modify()->set(SearchContextTest\PrimarySearch::class, 'primary_search_field_name', 'MatchAny');
461
        $primary1 = $this->objFromFixture(SearchContextTest\PrimarySearch::class, 'primary1');
462
        $this->expectException(LogicException::class);
463
        $primary1->getDefaultSearchContext();
464
    }
465
466
    public function testMatchAnySearch()
467
    {
468
        $order1 = $this->objFromFixture(SearchContextTest\Order::class, 'order1');
469
        $context = $order1->getDefaultSearchContext();
470
471
        // Search should match Order's customer FirstName
472
        $results = $context->getResults(['CustomFirstName' => 'Bill']);
473
        $this->assertCount(2, $results);
474
        $this->assertListContains([
475
            ['Name' => 'Jane'],
476
            ['Name' => 'Jack'],
477
        ], $results);
478
479
        // Search should match Order's shipping address FirstName
480
        $results = $context->getResults(['CustomFirstName' => 'Bob']);
481
        $this->assertCount(2, $results);
482
        $this->assertListContains([
483
            ['Name' => 'Jane'],
484
            ['Name' => 'Jill'],
485
        ], $results);
486
487
        // Search should match Order's Name db field
488
        $results = $context->getResults(['CustomFirstName' => 'Jane']);
489
        $this->assertCount(1, $results);
490
        $this->assertSame('Jane', $results->first()->Name);
0 ignored issues
show
Bug Best Practice introduced by
The property Name does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
491
492
        // Search should not match any Order
493
        $results = $context->getResults(['CustomFirstName' => 'NoMatches']);
494
        $this->assertCount(0, $results);
495
    }
496
497
    public function testMatchAnySearchWithFilters()
498
    {
499
        $order1 = $this->objFromFixture(SearchContextTest\Order::class, 'order1');
500
        $context = $order1->getDefaultSearchContext();
501
502
        $results = $context->getResults(['ExactMatchField' => 'Bil']);
503
        $this->assertCount(0, $results);
504
        $results = $context->getResults(['PartialMatchField' => 'Bil']);
505
        $this->assertCount(2, $results);
506
507
        $results = $context->getResults(['ExactMatchField' => 'ob']);
508
        $this->assertCount(0, $results);
509
        $results = $context->getResults(['PartialMatchField' => 'ob']);
510
        $this->assertCount(2, $results);
511
512
        $results = $context->getResults(['ExactMatchField' => 'an']);
513
        $this->assertCount(0, $results);
514
        $results = $context->getResults(['PartialMatchField' => 'an']);
515
        $this->assertCount(1, $results);
516
    }
517
}
518