Passed
Pull Request — 4 (#10382)
by Guy
07:27
created

SearchContextTest::testPrimarySearch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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