]> BookStack Code Mirror - bookstack/blob - tests/Entity/EntitySearchTest.php
Added page content parsing to up-rank header text in search
[bookstack] / tests / Entity / EntitySearchTest.php
1 <?php
2
3 namespace Tests\Entity;
4
5 use BookStack\Actions\Tag;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Models\Chapter;
9 use BookStack\Entities\Models\Page;
10 use BookStack\Entities\Models\SearchTerm;
11 use Tests\TestCase;
12
13 class EntitySearchTest extends TestCase
14 {
15     public function test_page_search()
16     {
17         $book = Book::all()->first();
18         $page = $book->pages->first();
19
20         $search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
21         $search->assertSee('Search Results');
22         $search->assertSee($page->name);
23     }
24
25     public function test_bookshelf_search()
26     {
27         $shelf = Bookshelf::first();
28         $search = $this->asEditor()->get('/search?term=' . urlencode(mb_substr($shelf->name, 0, 3)) . '  {type:bookshelf}');
29         $search->assertStatus(200);
30         $search->assertSee($shelf->name);
31     }
32
33     public function test_invalid_page_search()
34     {
35         $resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
36         $resp->assertSee('Search Results');
37         $resp->assertStatus(200);
38         $this->get('/search?term=cat+-')->assertStatus(200);
39     }
40
41     public function test_empty_search_shows_search_page()
42     {
43         $res = $this->asEditor()->get('/search');
44         $res->assertStatus(200);
45     }
46
47     public function test_searching_accents_and_small_terms()
48     {
49         $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content a2 orange dog']);
50         $this->asEditor();
51
52         $accentSearch = $this->get('/search?term=' . urlencode('áéíí'));
53         $accentSearch->assertStatus(200)->assertSee($page->name);
54
55         $smallSearch = $this->get('/search?term=' . urlencode('a2'));
56         $smallSearch->assertStatus(200)->assertSee($page->name);
57     }
58
59     public function test_book_search()
60     {
61         $book = Book::first();
62         $page = $book->pages->last();
63         $chapter = $book->chapters->last();
64
65         $pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name));
66         $pageTestResp->assertSee($page->name);
67
68         $chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name));
69         $chapterTestResp->assertSee($chapter->name);
70     }
71
72     public function test_chapter_search()
73     {
74         $chapter = Chapter::has('pages')->first();
75         $page = $chapter->pages[0];
76
77         $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));
78         $pageTestResp->assertSee($page->name);
79     }
80
81     public function test_tag_search()
82     {
83         $newTags = [
84             new Tag([
85                 'name'  => 'animal',
86                 'value' => 'cat',
87             ]),
88             new Tag([
89                 'name'  => 'color',
90                 'value' => 'red',
91             ]),
92         ];
93
94         $pageA = Page::first();
95         $pageA->tags()->saveMany($newTags);
96
97         $pageB = Page::all()->last();
98         $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
99
100         $this->asEditor();
101         $tNameSearch = $this->get('/search?term=%5Banimal%5D');
102         $tNameSearch->assertSee($pageA->name)->assertSee($pageB->name);
103
104         $tNameSearch2 = $this->get('/search?term=%5Bcolor%5D');
105         $tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name);
106
107         $tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D');
108         $tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name);
109     }
110
111     public function test_exact_searches()
112     {
113         $page = $this->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']);
114
115         $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"'));
116         $exactSearchA->assertStatus(200)->assertSee($page->name);
117
118         $exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('"story not about an orange"'));
119         $exactSearchB->assertStatus(200)->assertDontSee($page->name);
120     }
121
122     public function test_search_filters()
123     {
124         $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
125         $this->asEditor();
126         $editorId = $this->getEditor()->id;
127         $editorSlug = $this->getEditor()->slug;
128
129         // Viewed filter searches
130         $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
131         $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name);
132         $this->get($page->getUrl());
133         $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name);
134         $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name);
135
136         // User filters
137         $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
138         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
139         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
140         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertDontSee($page->name);
141         $page->created_by = $editorId;
142         $page->save();
143         $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
144         $this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editorSlug . '}'))->assertSee($page->name);
145         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
146         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
147         $page->updated_by = $editorId;
148         $page->save();
149         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
150         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertSee($page->name);
151         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
152         $page->owned_by = $editorId;
153         $page->save();
154         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);
155         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editorSlug . '}'))->assertSee($page->name);
156
157         // Content filters
158         $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
159         $this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name);
160         $this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name);
161         $this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name);
162
163         // Restricted filter
164         $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
165         $page->restricted = true;
166         $page->save();
167         $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
168
169         // Date filters
170         $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name);
171         $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name);
172         $page->updated_at = '2037-02-01';
173         $page->save();
174         $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name);
175         $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name);
176
177         $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name);
178         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name);
179         $page->created_at = '2037-02-01';
180         $page->save();
181         $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name);
182         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
183     }
184
185     public function test_ajax_entity_search()
186     {
187         $page = $this->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);
188         $notVisitedPage = Page::first();
189
190         // Visit the page to make popular
191         $this->asEditor()->get($page->getUrl());
192
193         $normalSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
194         $normalSearch->assertSee($page->name);
195
196         $bookSearch = $this->get('/ajax/search/entities?types=book&term=' . urlencode($page->name));
197         $bookSearch->assertDontSee($page->name);
198
199         $defaultListTest = $this->get('/ajax/search/entities');
200         $defaultListTest->assertSee($page->name);
201         $defaultListTest->assertDontSee($notVisitedPage->name);
202     }
203
204     public function test_ajax_entity_serach_shows_breadcrumbs()
205     {
206         $chapter = Chapter::first();
207         $page = $chapter->pages->first();
208         $this->asEditor();
209
210         $pageSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
211         $pageSearch->assertSee($page->name);
212         $pageSearch->assertSee($chapter->getShortName(42));
213         $pageSearch->assertSee($page->book->getShortName(42));
214
215         $chapterSearch = $this->get('/ajax/search/entities?term=' . urlencode($chapter->name));
216         $chapterSearch->assertSee($chapter->name);
217         $chapterSearch->assertSee($chapter->book->getShortName(42));
218     }
219
220     public function test_sibling_search_for_pages()
221     {
222         $chapter = Chapter::query()->with('pages')->first();
223         $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling');
224         $page = $chapter->pages->first();
225
226         $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
227         $search->assertSuccessful();
228         foreach ($chapter->pages as $page) {
229             $search->assertSee($page->name);
230         }
231
232         $search->assertDontSee($chapter->name);
233     }
234
235     public function test_sibling_search_for_pages_without_chapter()
236     {
237         $page = Page::query()->where('chapter_id', '=', 0)->firstOrFail();
238         $bookChildren = $page->book->getDirectChildren();
239         $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
240
241         $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
242         $search->assertSuccessful();
243         foreach ($bookChildren as $child) {
244             $search->assertSee($child->name);
245         }
246
247         $search->assertDontSee($page->book->name);
248     }
249
250     public function test_sibling_search_for_chapters()
251     {
252         $chapter = Chapter::query()->firstOrFail();
253         $bookChildren = $chapter->book->getDirectChildren();
254         $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
255
256         $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter");
257         $search->assertSuccessful();
258         foreach ($bookChildren as $child) {
259             $search->assertSee($child->name);
260         }
261
262         $search->assertDontSee($chapter->book->name);
263     }
264
265     public function test_sibling_search_for_books()
266     {
267         $books = Book::query()->take(10)->get();
268         $book = $books->first();
269         $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling');
270
271         $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book");
272         $search->assertSuccessful();
273         foreach ($books as $expectedBook) {
274             $search->assertSee($expectedBook->name);
275         }
276     }
277
278     public function test_sibling_search_for_shelves()
279     {
280         $shelves = Bookshelf::query()->take(10)->get();
281         $shelf = $shelves->first();
282         $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling');
283
284         $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf");
285         $search->assertSuccessful();
286         foreach ($shelves as $expectedShelf) {
287             $search->assertSee($expectedShelf->name);
288         }
289     }
290
291     public function test_search_works_on_updated_page_content()
292     {
293         $page = Page::query()->first();
294         $this->asEditor();
295
296         $update = $this->put($page->getUrl(), [
297             'name' => $page->name,
298             'html' => '<p>dog pandabearmonster spaghetti</p>',
299         ]);
300
301         $search = $this->asEditor()->get('/search?term=pandabearmonster');
302         $search->assertStatus(200);
303         $search->assertSeeText($page->name);
304         $search->assertSee($page->getUrl());
305     }
306
307     public function test_search_ranks_common_words_lower()
308     {
309         $this->newPage(['name' => 'Test page A', 'html' => '<p>dog biscuit dog dog</p>']);
310         $this->newPage(['name' => 'Test page B', 'html' => '<p>cat biscuit</p>']);
311
312         $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');
313         $search->assertElementContains('.entity-list > .page', 'Test page A', 1);
314         $search->assertElementContains('.entity-list > .page', 'Test page B', 2);
315
316         for ($i = 0; $i < 2; $i++) {
317             $this->newPage(['name' => 'Test page ' . $i, 'html' => '<p>dog</p>']);
318         }
319
320         $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');
321         $search->assertElementContains('.entity-list > .page', 'Test page B', 1);
322         $search->assertElementContains('.entity-list > .page', 'Test page A', 2);
323     }
324
325     public function test_terms_in_headers_have_an_adjusted_index_score()
326     {
327         $page = $this->newPage(['name' => 'Test page A', 'html' => '
328             <p>TermA</p>
329             <h1>TermB <strong>TermNested</strong></h1>
330             <h2>TermC</h2>
331             <h3>TermD</h3>
332             <h4>TermE</h4>
333             <h5>TermF</h5>
334             <h6>TermG</h6>
335         ']);
336
337         $entityRelationCols = ['entity_id' => $page->id, 'entity_type' => 'BookStack\\Page'];
338         $scoreByTerm = SearchTerm::query()->where($entityRelationCols)->pluck('score', 'term');
339
340         $this->assertEquals(1, $scoreByTerm->get('TermA'));
341         $this->assertEquals(10, $scoreByTerm->get('TermB'));
342         $this->assertEquals(10, $scoreByTerm->get('TermNested'));
343         $this->assertEquals(5, $scoreByTerm->get('TermC'));
344         $this->assertEquals(4, $scoreByTerm->get('TermD'));
345         $this->assertEquals(3, $scoreByTerm->get('TermE'));
346         $this->assertEquals(2, $scoreByTerm->get('TermF'));
347         // Is 1.5 but stored as integer, rounding up
348         $this->assertEquals(2, $scoreByTerm->get('TermG'));
349     }
350
351     public function test_name_and_content_terms_are_merged_to_single_score()
352     {
353         $page = $this->newPage(['name' => 'TermA', 'html' => '
354             <p>TermA</p>
355         ']);
356
357         $entityRelationCols = ['entity_id' => $page->id, 'entity_type' => 'BookStack\\Page'];
358         $scoreByTerm = SearchTerm::query()->where($entityRelationCols)->pluck('score', 'term');
359
360         // Scores 40 for being in the name then 1 for being in the content
361         $this->assertEquals(41, $scoreByTerm->get('TermA'));
362     }
363 }