]> BookStack Code Mirror - bookstack/blob - tests/Search/EntitySearchTest.php
Fix issue BookStackApp#5542 Sorting by name
[bookstack] / tests / Search / EntitySearchTest.php
1 <?php
2
3 namespace Tests\Search;
4
5 use BookStack\Activity\Models\Tag;
6 use BookStack\Entities\Models\Book;
7 use Tests\TestCase;
8
9 class EntitySearchTest extends TestCase
10 {
11     public function test_page_search()
12     {
13         $book = $this->entities->book();
14         $page = $book->pages->first();
15
16         $search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
17         $search->assertSee('Search Results');
18         $search->assertSeeText($page->name, true);
19     }
20
21     public function test_bookshelf_search()
22     {
23         $shelf = $this->entities->shelf();
24
25         $search = $this->asEditor()->get('/search?term=' . urlencode($shelf->name) . '  {type:bookshelf}');
26         $search->assertSee('Search Results');
27         $search->assertSeeText($shelf->name, true);
28     }
29
30     public function test_invalid_page_search()
31     {
32         $resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
33         $resp->assertSee('Search Results');
34         $resp->assertStatus(200);
35         $this->get('/search?term=cat+-')->assertStatus(200);
36     }
37
38     public function test_empty_search_shows_search_page()
39     {
40         $res = $this->asEditor()->get('/search');
41         $res->assertStatus(200);
42     }
43
44     public function test_searching_accents_and_small_terms()
45     {
46         $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content a2 orange dog']);
47         $this->asEditor();
48
49         $accentSearch = $this->get('/search?term=' . urlencode('áéíí'));
50         $accentSearch->assertStatus(200)->assertSee($page->name);
51
52         $smallSearch = $this->get('/search?term=' . urlencode('a2'));
53         $smallSearch->assertStatus(200)->assertSee($page->name);
54     }
55
56     public function test_book_search()
57     {
58         $book = Book::first();
59         $page = $book->pages->last();
60         $chapter = $book->chapters->last();
61
62         $pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name));
63         $pageTestResp->assertSee($page->name);
64
65         $chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name));
66         $chapterTestResp->assertSee($chapter->name);
67     }
68
69     public function test_chapter_search()
70     {
71         $chapter = $this->entities->chapterHasPages();
72         $page = $chapter->pages[0];
73
74         $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));
75         $pageTestResp->assertSee($page->name);
76     }
77
78     public function test_tag_search()
79     {
80         $newTags = [
81             new Tag([
82                 'name'  => 'animal',
83                 'value' => 'cat',
84             ]),
85             new Tag([
86                 'name'  => 'color',
87                 'value' => 'red',
88             ]),
89         ];
90
91         $pageA = $this->entities->page();
92         $pageA->tags()->saveMany($newTags);
93
94         $pageB = $this->entities->page();
95         $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
96
97         $this->asEditor();
98         $tNameSearch = $this->get('/search?term=%5Banimal%5D');
99         $tNameSearch->assertSee($pageA->name)->assertSee($pageB->name);
100
101         $tNameSearch2 = $this->get('/search?term=%5Bcolor%5D');
102         $tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name);
103
104         $tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D');
105         $tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name);
106     }
107
108     public function test_exact_searches()
109     {
110         $page = $this->entities->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']);
111
112         $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"'));
113         $exactSearchA->assertStatus(200)->assertSee($page->name);
114
115         $exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('"story not about an orange"'));
116         $exactSearchB->assertStatus(200)->assertDontSee($page->name);
117     }
118
119     public function test_negated_searches()
120     {
121         $page = $this->entities->newPage(['name' => 'My new test negation page', 'html' => '<p>An angry tortoise wore trumpeted plimsoles</p>']);
122         $page->tags()->saveMany([new Tag(['name' => 'DonkCount', 'value' => '500'])]);
123         $page->created_by = $this->users->admin()->id;
124         $page->save();
125
126         $editor = $this->users->editor();
127         $this->actingAs($editor);
128
129         $exactSearch = $this->get('/search?term=' . urlencode('negation -"tortoise"'));
130         $exactSearch->assertStatus(200)->assertDontSeeText($page->name);
131
132         $tagSearchA = $this->get('/search?term=' . urlencode('negation [DonkCount=500]'));
133         $tagSearchA->assertStatus(200)->assertSeeText($page->name);
134         $tagSearchB = $this->get('/search?term=' . urlencode('negation -[DonkCount=500]'));
135         $tagSearchB->assertStatus(200)->assertDontSeeText($page->name);
136
137         $filterSearchA = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));
138         $filterSearchA->assertStatus(200)->assertSeeText($page->name);
139         $page->created_by = $editor->id;
140         $page->save();
141         $filterSearchB = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));
142         $filterSearchB->assertStatus(200)->assertDontSeeText($page->name);
143     }
144
145     public function test_search_terms_with_delimiters_are_converted_to_exact_matches()
146     {
147         $this->asEditor();
148         $page = $this->entities->newPage(['name' => 'Delimiter test', 'html' => '<p>1.1 2,2 3?3 4:4 5;5 (8) &lt;9&gt; "10" \'11\' `12`</p>']);
149         $terms = explode(' ', '1.1 2,2 3?3 4:4 5;5 (8) <9> "10" \'11\' `12`');
150
151         foreach ($terms as $term) {
152             $search = $this->get('/search?term=' . urlencode($term));
153             $search->assertSee($page->name);
154         }
155     }
156
157     public function test_search_filters()
158     {
159         $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
160         $editor = $this->users->editor();
161         $this->actingAs($editor);
162
163         // Viewed filter searches
164         $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
165         $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name);
166         $this->get($page->getUrl());
167         $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name);
168         $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name);
169
170         // User filters
171         $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
172         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
173         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
174         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertDontSee($page->name);
175         $page->created_by = $editor->id;
176         $page->save();
177         $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
178         $this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editor->slug . '}'))->assertSee($page->name);
179         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
180         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
181         $page->updated_by = $editor->id;
182         $page->save();
183         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
184         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertSee($page->name);
185         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
186         $page->owned_by = $editor->id;
187         $page->save();
188         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);
189         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editor->slug . '}'))->assertSee($page->name);
190
191         // Content filters
192         $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
193         $this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name);
194         $this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name);
195         $this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name);
196
197         // Restricted filter
198         $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
199         $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles->first()]);
200         $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
201
202         // Date filters
203         $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name);
204         $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name);
205         $page->updated_at = '2037-02-01';
206         $page->save();
207         $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name);
208         $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name);
209
210         $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name);
211         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name);
212         $page->created_at = '2037-02-01';
213         $page->save();
214         $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name);
215         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
216     }
217
218     public function test_entity_selector_search()
219     {
220         $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);
221         $notVisitedPage = $this->entities->page();
222
223         // Visit the page to make popular
224         $this->asEditor()->get($page->getUrl());
225
226         $normalSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
227         $normalSearch->assertSee($page->name);
228
229         $bookSearch = $this->get('/search/entity-selector?types=book&term=' . urlencode($page->name));
230         $bookSearch->assertDontSee($page->name);
231
232         $defaultListTest = $this->get('/search/entity-selector');
233         $defaultListTest->assertSee($page->name);
234         $defaultListTest->assertDontSee($notVisitedPage->name);
235     }
236
237     public function test_entity_selector_search_shows_breadcrumbs()
238     {
239         $chapter = $this->entities->chapter();
240         $page = $chapter->pages->first();
241         $this->asEditor();
242
243         $pageSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
244         $pageSearch->assertSee($page->name);
245         $pageSearch->assertSee($chapter->getShortName(42));
246         $pageSearch->assertSee($page->book->getShortName(42));
247
248         $chapterSearch = $this->get('/search/entity-selector?term=' . urlencode($chapter->name));
249         $chapterSearch->assertSee($chapter->name);
250         $chapterSearch->assertSee($chapter->book->getShortName(42));
251     }
252
253     public function test_entity_selector_shows_breadcrumbs_on_default_view()
254     {
255         $page = $this->entities->pageWithinChapter();
256         $this->asEditor()->get($page->chapter->getUrl());
257
258         $resp = $this->asEditor()->get('/search/entity-selector?types=book,chapter&permission=page-create');
259         $html = $this->withHtml($resp);
260         $html->assertElementContains('.chapter.entity-list-item', $page->chapter->name);
261         $html->assertElementContains('.chapter.entity-list-item .entity-item-snippet', $page->book->getShortName(42));
262     }
263
264     public function test_entity_selector_search_reflects_items_without_permission()
265     {
266         $page = $this->entities->page();
267         $baseSelector = 'a[data-entity-type="page"][data-entity-id="' . $page->id . '"]';
268         $searchUrl = '/search/entity-selector?permission=update&term=' . urlencode($page->name);
269
270         $resp = $this->asEditor()->get($searchUrl);
271         $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
272         $this->withHtml($resp)->assertElementNotContains($baseSelector, "You don't have the required permissions to select this item");
273
274         $resp = $this->actingAs($this->users->viewer())->get($searchUrl);
275         $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
276         $this->withHtml($resp)->assertElementContains($baseSelector, "You don't have the required permissions to select this item");
277     }
278
279     public function test_entity_template_selector_search()
280     {
281         $templatePage = $this->entities->newPage(['name' => 'Template search test', 'html' => 'template test']);
282         $templatePage->template = true;
283         $templatePage->save();
284
285         $nonTemplatePage = $this->entities->newPage(['name' => 'Nontemplate page', 'html' => 'nontemplate', 'template' => false]);
286
287         // Visit both to make popular
288         $this->asEditor()->get($templatePage->getUrl());
289         $this->get($nonTemplatePage->getUrl());
290
291         $normalSearch = $this->get('/search/entity-selector-templates?term=test');
292         $normalSearch->assertSee($templatePage->name);
293         $normalSearch->assertDontSee($nonTemplatePage->name);
294
295         $normalSearch = $this->get('/search/entity-selector-templates?term=beans');
296         $normalSearch->assertDontSee($templatePage->name);
297         $normalSearch->assertDontSee($nonTemplatePage->name);
298
299         $defaultListTest = $this->get('/search/entity-selector-templates');
300         $defaultListTest->assertSee($templatePage->name);
301         $defaultListTest->assertDontSee($nonTemplatePage->name);
302
303         $this->permissions->disableEntityInheritedPermissions($templatePage);
304
305         $normalSearch = $this->get('/search/entity-selector-templates?term=test');
306         $normalSearch->assertDontSee($templatePage->name);
307
308         $defaultListTest = $this->get('/search/entity-selector-templates');
309         $defaultListTest->assertDontSee($templatePage->name);
310     }
311
312     public function test_search_works_on_updated_page_content()
313     {
314         $page = $this->entities->page();
315         $this->asEditor();
316
317         $update = $this->put($page->getUrl(), [
318             'name' => $page->name,
319             'html' => '<p>dog pandabearmonster spaghetti</p>',
320         ]);
321
322         $search = $this->asEditor()->get('/search?term=pandabearmonster');
323         $search->assertStatus(200);
324         $search->assertSeeText($page->name);
325         $search->assertSee($page->getUrl());
326     }
327
328     public function test_search_ranks_common_words_lower()
329     {
330         $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>dog biscuit dog dog</p>']);
331         $this->entities->newPage(['name' => 'Test page B', 'html' => '<p>cat biscuit</p>']);
332
333         $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');
334         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page A');
335         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page B');
336
337         for ($i = 0; $i < 2; $i++) {
338             $this->entities->newPage(['name' => 'Test page ' . $i, 'html' => '<p>dog</p>']);
339         }
340
341         $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');
342         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page B');
343         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page A');
344     }
345
346     public function test_matching_terms_in_search_results_are_highlighted()
347     {
348         $this->entities->newPage(['name' => 'My Meowie Cat', 'html' => '<p>A superimportant page about meowieable animals</p>', 'tags' => [
349             ['name' => 'Animal', 'value' => 'MeowieCat'],
350             ['name' => 'SuperImportant'],
351         ]]);
352
353         $search = $this->asEditor()->get('/search?term=SuperImportant+Meowie');
354         // Title
355         $search->assertSee('My <strong>Meowie</strong> Cat', false);
356         // Content
357         $search->assertSee('A <strong>superimportant</strong> page about <strong>meowie</strong>able animals', false);
358         // Tag name
359         $this->withHtml($search)->assertElementContains('.tag-name.highlight', 'SuperImportant');
360         // Tag value
361         $this->withHtml($search)->assertElementContains('.tag-value.highlight', 'MeowieCat');
362     }
363
364     public function test_match_highlighting_works_with_multibyte_content()
365     {
366         $this->entities->newPage([
367             'name' => 'Test Page',
368             'html' => '<p>На мен ми трябва нещо добро test</p>',
369         ]);
370
371         $search = $this->asEditor()->get('/search?term=' . urlencode('На мен ми трябва нещо добро'));
372         $search->assertSee('<strong>На</strong> <strong>мен</strong> <strong>ми</strong> <strong>трябва</strong> <strong>нещо</strong> <strong>добро</strong> test', false);
373     }
374
375     public function test_html_entities_in_item_details_remains_escaped_in_search_results()
376     {
377         $this->entities->newPage(['name' => 'My <cool> TestPageContent', 'html' => '<p>My supercool &lt;great&gt; TestPageContent page</p>']);
378
379         $search = $this->asEditor()->get('/search?term=TestPageContent');
380         $search->assertSee('My &lt;cool&gt; <strong>TestPageContent</strong>', false);
381         $search->assertSee('My supercool &lt;great&gt; <strong>TestPageContent</strong> page', false);
382     }
383
384     public function test_words_adjacent_to_lines_breaks_can_be_matched_with_normal_terms()
385     {
386         $page = $this->entities->newPage(['name' => 'TermA', 'html' => '
387             <p>TermA<br>TermB<br>TermC</p>
388         ']);
389
390         $search = $this->asEditor()->get('/search?term=' . urlencode('TermB TermC'));
391
392         $search->assertSee($page->getUrl(), false);
393     }
394
395     public function test_backslashes_can_be_searched_upon()
396     {
397         $page = $this->entities->newPage(['name' => 'TermA', 'html' => '
398             <p>More info is at the path \\\\cat\\dog\\badger</p>
399         ']);
400         $page->tags()->save(new Tag(['name' => '\\Category', 'value' => '\\animals\\fluffy']));
401
402         $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog'));
403         $search->assertSee($page->getUrl(), false);
404
405         $search = $this->asEditor()->get('/search?term=' . urlencode('"\\dog\\\\"'));
406         $search->assertSee($page->getUrl(), false);
407
408         $search = $this->asEditor()->get('/search?term=' . urlencode('"\\badger\\\\"'));
409         $search->assertDontSee($page->getUrl(), false);
410
411         $search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]'));
412         $search->assertSee($page->getUrl(), false);
413     }
414
415     public function test_searches_with_terms_without_controls_includes_them_in_extras()
416     {
417         $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:dan} {created_by:dan} -{viewed_by_me} -[a=b] -"dog" {is_template} {sort_by:last_commented}'));
418         $this->withHtml($resp)->assertFieldHasValue('extras', '{updated_by:dan} {created_by:dan} {is_template} {sort_by:last_commented} -"dog" -[a=b] -{viewed_by_me}');
419     }
420
421     public function test_negated_searches_dont_show_in_inputs()
422     {
423         $resp = $this->asEditor()->get('/search?term=' . urlencode('-{created_by:me} -[a=b] -"dog"'));
424         $this->withHtml($resp)->assertElementNotExists('input[name="tags[]"][value="a=b"]');
425         $this->withHtml($resp)->assertElementNotExists('input[name="exact[]"][value="dog"]');
426         $this->withHtml($resp)->assertElementNotExists('input[name="filters[created_by]"][value="me"][checked="checked"]');
427     }
428
429     public function test_searches_with_user_filters_using_me_adds_them_into_advanced_search_form()
430     {
431         $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:me} {created_by:me}'));
432         $this->withHtml($resp)->assertElementExists('form input[name="filters[updated_by]"][value="me"][checked="checked"]');
433         $this->withHtml($resp)->assertElementExists('form input[name="filters[created_by]"][value="me"][checked="checked"]');
434     }
435
436     public function test_search_suggestion_endpoint()
437     {
438         $this->entities->newPage(['name' => 'My suggestion page', 'html' => '<p>My supercool suggestion page</p>']);
439
440         // Test specific search
441         $resp = $this->asEditor()->get('/search/suggest?term="supercool+suggestion"');
442         $resp->assertSee('My suggestion page');
443         $resp->assertDontSee('My supercool suggestion page');
444         $resp->assertDontSee('No items available');
445         $this->withHtml($resp)->assertElementCount('a', 1);
446
447         // Test search limit
448         $resp = $this->asEditor()->get('/search/suggest?term=et');
449         $this->withHtml($resp)->assertElementCount('a', 5);
450
451         // Test empty state
452         $resp = $this->asEditor()->get('/search/suggest?term=spaghettisaurusrex');
453         $this->withHtml($resp)->assertElementCount('a', 0);
454         $resp->assertSee('No items available');
455     }
456 }