]> BookStack Code Mirror - bookstack/blob - tests/Activity/WatchTest.php
Update URL Command: Added revisions table support
[bookstack] / tests / Activity / WatchTest.php
1 <?php
2
3 namespace Tests\Activity;
4
5 use BookStack\Activity\ActivityType;
6 use BookStack\Activity\Models\Comment;
7 use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
8 use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
9 use BookStack\Activity\Notifications\Messages\PageCreationNotification;
10 use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
11 use BookStack\Activity\Tools\ActivityLogger;
12 use BookStack\Activity\Tools\UserEntityWatchOptions;
13 use BookStack\Activity\WatchLevels;
14 use BookStack\Entities\Models\Entity;
15 use BookStack\Settings\UserNotificationPreferences;
16 use Illuminate\Support\Facades\Notification;
17 use Tests\TestCase;
18
19 class WatchTest extends TestCase
20 {
21     public function test_watch_action_exists_on_entity_unless_active()
22     {
23         $editor = $this->users->editor();
24         $this->actingAs($editor);
25
26         $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];
27         /** @var Entity $entity */
28         foreach ($entities as $entity) {
29             $resp = $this->get($entity->getUrl());
30             $this->withHtml($resp)->assertElementContains('form[action$="/watching/update"] button.icon-list-item', 'Watch');
31
32             $watchOptions = new UserEntityWatchOptions($editor, $entity);
33             $watchOptions->updateLevelByValue(WatchLevels::COMMENTS);
34
35             $resp = $this->get($entity->getUrl());
36             $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
37         }
38     }
39
40     public function test_watch_action_only_shows_with_permission()
41     {
42         $viewer = $this->users->viewer();
43         $this->actingAs($viewer);
44
45         $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];
46         /** @var Entity $entity */
47         foreach ($entities as $entity) {
48             $resp = $this->get($entity->getUrl());
49             $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
50         }
51
52         $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
53
54         /** @var Entity $entity */
55         foreach ($entities as $entity) {
56             $resp = $this->get($entity->getUrl());
57             $this->withHtml($resp)->assertElementExists('form[action$="/watching/update"] button.icon-list-item');
58         }
59     }
60
61     public function test_watch_update()
62     {
63         $editor = $this->users->editor();
64         $book = $this->entities->book();
65
66         $resp = $this->actingAs($editor)->put('/watching/update', [
67             'type' => $book->getMorphClass(),
68             'id' => $book->id,
69             'level' => 'comments'
70         ]);
71
72         $resp->assertRedirect($book->getUrl());
73         $this->assertSessionHas('success');
74         $this->assertDatabaseHas('watches', [
75             'watchable_id' => $book->id,
76             'watchable_type' => $book->getMorphClass(),
77             'user_id' => $editor->id,
78             'level' => WatchLevels::COMMENTS,
79         ]);
80
81         $resp = $this->put('/watching/update', [
82             'type' => $book->getMorphClass(),
83             'id' => $book->id,
84             'level' => 'default'
85         ]);
86         $resp->assertRedirect($book->getUrl());
87         $this->assertDatabaseMissing('watches', [
88             'watchable_id' => $book->id,
89             'watchable_type' => $book->getMorphClass(),
90             'user_id' => $editor->id,
91         ]);
92     }
93
94     public function test_watch_update_fails_for_guest()
95     {
96         $this->setSettings(['app-public' => 'true']);
97         $guest = $this->users->guest();
98         $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
99         $book = $this->entities->book();
100
101         $resp = $this->put('/watching/update', [
102             'type' => $book->getMorphClass(),
103             'id' => $book->id,
104             'level' => 'comments'
105         ]);
106
107         $this->assertPermissionError($resp);
108         $guest->unsetRelations();
109     }
110
111     public function test_watch_detail_display_reflects_state()
112     {
113         $editor = $this->users->editor();
114         $book = $this->entities->bookHasChaptersAndPages();
115         $chapter = $book->chapters()->first();
116         $page = $chapter->pages()->first();
117
118         (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::UPDATES);
119
120         $this->actingAs($editor)->get($book->getUrl())->assertSee('Watching new pages and updates');
121         $this->get($chapter->getUrl())->assertSee('Watching via parent book');
122         $this->get($page->getUrl())->assertSee('Watching via parent book');
123
124         (new UserEntityWatchOptions($editor, $chapter))->updateLevelByValue(WatchLevels::COMMENTS);
125         $this->get($chapter->getUrl())->assertSee('Watching new pages, updates & comments');
126         $this->get($page->getUrl())->assertSee('Watching via parent chapter');
127
128         (new UserEntityWatchOptions($editor, $page))->updateLevelByValue(WatchLevels::UPDATES);
129         $this->get($page->getUrl())->assertSee('Watching new pages and updates');
130     }
131
132     public function test_watch_detail_ignore_indicator_cascades()
133     {
134         $editor = $this->users->editor();
135         $book = $this->entities->bookHasChaptersAndPages();
136         (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);
137
138         $this->actingAs($editor)->get($book->getUrl())->assertSee('Ignoring notifications');
139         $this->get($book->chapters()->first()->getUrl())->assertSee('Ignoring via parent book');
140         $this->get($book->pages()->first()->getUrl())->assertSee('Ignoring via parent book');
141     }
142
143     public function test_watch_option_menu_shows_current_active_state()
144     {
145         $editor = $this->users->editor();
146         $book = $this->entities->book();
147         $options = new UserEntityWatchOptions($editor, $book);
148
149         $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
150         $respHtml->assertElementNotExists('form[action$="/watching/update"] svg[data-icon="check-circle"]');
151
152         $options->updateLevelByValue(WatchLevels::COMMENTS);
153         $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
154         $respHtml->assertElementExists('form[action$="/watching/update"] button[value="comments"] svg[data-icon="check-circle"]');
155
156         $options->updateLevelByValue(WatchLevels::IGNORE);
157         $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
158         $respHtml->assertElementExists('form[action$="/watching/update"] button[value="ignore"] svg[data-icon="check-circle"]');
159     }
160
161     public function test_watch_option_menu_limits_options_for_pages()
162     {
163         $editor = $this->users->editor();
164         $book = $this->entities->bookHasChaptersAndPages();
165         (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);
166
167         $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
168         $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="new"]');
169
170         $respHtml = $this->withHtml($this->get($book->pages()->first()->getUrl()));
171         $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="updates"]');
172         $respHtml->assertElementNotExists('form[action$="/watching/update"] button[name="level"][value="new"]');
173     }
174
175     public function test_notify_own_page_changes()
176     {
177         $editor = $this->users->editor();
178         $entities = $this->entities->createChainBelongingToUser($editor);
179         $prefs = new UserNotificationPreferences($editor);
180         $prefs->updateFromSettingsArray(['own-page-changes' => 'true']);
181
182         $notifications = Notification::fake();
183
184         $this->asAdmin();
185         $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
186         $notifications->assertSentTo($editor, PageUpdateNotification::class);
187     }
188
189     public function test_notify_own_page_comments()
190     {
191         $editor = $this->users->editor();
192         $entities = $this->entities->createChainBelongingToUser($editor);
193         $prefs = new UserNotificationPreferences($editor);
194         $prefs->updateFromSettingsArray(['own-page-comments' => 'true']);
195
196         $notifications = Notification::fake();
197
198         $this->asAdmin()->post("/comment/{$entities['page']->id}", [
199             'html' => '<p>My new comment</p>'
200         ]);
201         $notifications->assertSentTo($editor, CommentCreationNotification::class);
202     }
203
204     public function test_notify_comment_replies()
205     {
206         $editor = $this->users->editor();
207         $entities = $this->entities->createChainBelongingToUser($editor);
208         $prefs = new UserNotificationPreferences($editor);
209         $prefs->updateFromSettingsArray(['comment-replies' => 'true']);
210
211         // Create some existing comments to pad IDs to help potentially error
212         // on mis-identification of parent via ids used.
213         Comment::factory()->count(5)
214             ->for($entities['page'], 'entity')
215             ->create(['created_by' => $this->users->admin()->id]);
216
217         $notifications = Notification::fake();
218
219         $this->actingAs($editor)->post("/comment/{$entities['page']->id}", [
220             'html' => '<p>My new comment</p>'
221         ]);
222         $comment = $entities['page']->comments()->orderBy('id', 'desc')->first();
223
224         $this->asAdmin()->post("/comment/{$entities['page']->id}", [
225             'html' => '<p>My new comment response</p>',
226             'parent_id' => $comment->local_id,
227         ]);
228         $notifications->assertSentTo($editor, CommentCreationNotification::class);
229     }
230
231     public function test_notify_watch_parent_book_ignore()
232     {
233         $editor = $this->users->editor();
234         $entities = $this->entities->createChainBelongingToUser($editor);
235         $watches = new UserEntityWatchOptions($editor, $entities['book']);
236         $prefs = new UserNotificationPreferences($editor);
237         $watches->updateLevelByValue(WatchLevels::IGNORE);
238         $prefs->updateFromSettingsArray(['own-page-changes' => 'true', 'own-page-comments' => true]);
239
240         $notifications = Notification::fake();
241
242         $this->asAdmin()->post("/comment/{$entities['page']->id}", [
243             'text' => 'My new comment response',
244         ]);
245         $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
246         $notifications->assertNothingSent();
247     }
248
249     public function test_notify_watch_parent_book_comments()
250     {
251         $notifications = Notification::fake();
252         $editor = $this->users->editor();
253         $admin = $this->users->admin();
254         $entities = $this->entities->createChainBelongingToUser($editor);
255         $watches = new UserEntityWatchOptions($editor, $entities['book']);
256         $watches->updateLevelByValue(WatchLevels::COMMENTS);
257
258         // Comment post
259         $this->actingAs($admin)->post("/comment/{$entities['page']->id}", [
260             'html' => '<p>My new comment response</p>',
261         ]);
262
263         $notifications->assertSentTo($editor, function (CommentCreationNotification $notification) use ($editor, $admin, $entities) {
264             $mail = $notification->toMail($editor);
265             $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);
266             return $mail->subject === 'New comment on page: ' . $entities['page']->getShortName()
267                 && str_contains($mailContent, 'View Comment')
268                 && str_contains($mailContent, 'Page Name: ' . $entities['page']->name)
269                 && str_contains($mailContent, 'Page Path: ' . $entities['book']->getShortName(24) . ' > ' . $entities['chapter']->getShortName(24))
270                 && str_contains($mailContent, 'Commenter: ' . $admin->name)
271                 && str_contains($mailContent, 'Comment: My new comment response');
272         });
273     }
274
275     public function test_notify_watch_parent_book_updates()
276     {
277         $notifications = Notification::fake();
278         $editor = $this->users->editor();
279         $admin = $this->users->admin();
280         $entities = $this->entities->createChainBelongingToUser($editor);
281         $watches = new UserEntityWatchOptions($editor, $entities['book']);
282         $watches->updateLevelByValue(WatchLevels::UPDATES);
283
284         $this->actingAs($admin);
285         $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
286
287         $notifications->assertSentTo($editor, function (PageUpdateNotification $notification) use ($editor, $admin, $entities) {
288             $mail = $notification->toMail($editor);
289             $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);
290             return $mail->subject === 'Updated page: Updated page'
291                 && str_contains($mailContent, 'View Page')
292                 && str_contains($mailContent, 'Page Name: Updated page')
293                 && str_contains($mailContent, 'Page Path: ' . $entities['book']->getShortName(24) . ' > ' . $entities['chapter']->getShortName(24))
294                 && str_contains($mailContent, 'Updated By: ' . $admin->name)
295                 && str_contains($mailContent, 'you won\'t be sent notifications for further edits to this page by the same editor');
296         });
297
298         // Test debounce
299         $notifications = Notification::fake();
300         $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
301         $notifications->assertNothingSentTo($editor);
302     }
303
304     public function test_notify_watch_parent_book_new()
305     {
306         $notifications = Notification::fake();
307         $editor = $this->users->editor();
308         $admin = $this->users->admin();
309         $entities = $this->entities->createChainBelongingToUser($editor);
310         $watches = new UserEntityWatchOptions($editor, $entities['book']);
311         $watches->updateLevelByValue(WatchLevels::NEW);
312
313         $this->actingAs($admin)->get($entities['chapter']->getUrl('/create-page'));
314         $page = $entities['chapter']->pages()->where('draft', '=', true)->first();
315         $this->post($page->getUrl(), ['name' => 'My new page', 'html' => 'My new page content']);
316
317         $notifications->assertSentTo($editor, function (PageCreationNotification $notification) use ($editor, $admin, $entities) {
318             $mail = $notification->toMail($editor);
319             $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);
320             return $mail->subject === 'New page: My new page'
321                 && str_contains($mailContent, 'View Page')
322                 && str_contains($mailContent, 'Page Name: My new page')
323                 && str_contains($mailContent, 'Page Path: ' . $entities['book']->getShortName(24) . ' > ' . $entities['chapter']->getShortName(24))
324                 && str_contains($mailContent, 'Created By: ' . $admin->name);
325         });
326     }
327
328     public function test_notifications_sent_in_right_language()
329     {
330         $editor = $this->users->editor();
331         $admin = $this->users->admin();
332         setting()->putUser($editor, 'language', 'de');
333         $entities = $this->entities->createChainBelongingToUser($editor);
334         $watches = new UserEntityWatchOptions($editor, $entities['book']);
335         $watches->updateLevelByValue(WatchLevels::COMMENTS);
336
337         $activities = [
338             ActivityType::PAGE_CREATE => $entities['page'],
339             ActivityType::PAGE_UPDATE => $entities['page'],
340             ActivityType::COMMENT_CREATE => Comment::factory()->make([
341                 'entity_id' => $entities['page']->id,
342                 'entity_type' => $entities['page']->getMorphClass(),
343             ]),
344         ];
345
346         $notifications = Notification::fake();
347         $logger = app()->make(ActivityLogger::class);
348         $this->actingAs($admin);
349
350         foreach ($activities as $activityType => $detail) {
351             $logger->add($activityType, $detail);
352         }
353
354         $sent = $notifications->sentNotifications()[get_class($editor)][$editor->id];
355         $this->assertCount(3, $sent);
356
357         foreach ($sent as $notificationInfo) {
358             $notification = $notificationInfo[0]['notification'];
359             $this->assertInstanceOf(BaseActivityNotification::class, $notification);
360             $mail = $notification->toMail($editor);
361             $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES);
362             $this->assertStringContainsString('Name der Seite:', $mailContent);
363             $this->assertStringContainsString('Diese Benachrichtigung wurde', $mailContent);
364             $this->assertStringContainsString('Sollte es beim Anklicken der Schaltfläche', $mailContent);
365         }
366     }
367
368     public function test_notifications_not_sent_if_lacking_view_permission_for_related_item()
369     {
370         $notifications = Notification::fake();
371         $editor = $this->users->editor();
372         $page = $this->entities->page();
373
374         $watches = new UserEntityWatchOptions($editor, $page);
375         $watches->updateLevelByValue(WatchLevels::COMMENTS);
376         $this->permissions->disableEntityInheritedPermissions($page);
377
378         $this->asAdmin()->post("/comment/{$page->id}", [
379             'html' => '<p>My new comment response</p>',
380         ])->assertOk();
381
382         $notifications->assertNothingSentTo($editor);
383     }
384
385     public function test_watches_deleted_on_user_delete()
386     {
387         $editor = $this->users->editor();
388         $page = $this->entities->page();
389
390         $watches = new UserEntityWatchOptions($editor, $page);
391         $watches->updateLevelByValue(WatchLevels::COMMENTS);
392         $this->assertDatabaseHas('watches', ['user_id' => $editor->id]);
393
394         $this->asAdmin()->delete($editor->getEditUrl());
395
396         $this->assertDatabaseMissing('watches', ['user_id' => $editor->id]);
397     }
398
399     public function test_watches_deleted_on_item_delete()
400     {
401         $editor = $this->users->editor();
402         $page = $this->entities->page();
403
404         $watches = new UserEntityWatchOptions($editor, $page);
405         $watches->updateLevelByValue(WatchLevels::COMMENTS);
406         $this->assertDatabaseHas('watches', ['watchable_type' => 'page', 'watchable_id' => $page->id]);
407
408         $this->entities->destroy($page);
409
410         $this->assertDatabaseMissing('watches', ['watchable_type' => 'page', 'watchable_id' => $page->id]);
411     }
412
413     public function test_page_path_in_notifications_limited_by_permissions()
414     {
415         $chapter = $this->entities->chapterHasPages();
416         $page = $chapter->pages()->first();
417         $book = $chapter->book;
418         $notification = new PageCreationNotification($page, $this->users->editor());
419
420         $viewer = $this->users->viewer();
421         $viewerRole = $viewer->roles()->first();
422
423         $content = html_entity_decode(strip_tags($notification->toMail($viewer)->render()), ENT_QUOTES);
424         $this->assertStringContainsString('Page Path: ' . $book->getShortName(24) . ' > ' . $chapter->getShortName(24), $content);
425
426         $this->permissions->setEntityPermissions($page, ['view'], [$viewerRole]);
427         $this->permissions->setEntityPermissions($chapter, [], [$viewerRole]);
428
429         $content = html_entity_decode(strip_tags($notification->toMail($viewer)->render()), ENT_QUOTES);
430         $this->assertStringContainsString('Page Path: ' . $book->getShortName(24), $content);
431         $this->assertStringNotContainsString(' > ' . $chapter->getShortName(24), $content);
432
433         $this->permissions->setEntityPermissions($book, [], [$viewerRole]);
434
435         $content = html_entity_decode(strip_tags($notification->toMail($viewer)->render()), ENT_QUOTES);
436         $this->assertStringNotContainsString('Page Path:', $content);
437         $this->assertStringNotContainsString($book->getShortName(24), $content);
438         $this->assertStringNotContainsString($chapter->getShortName(24), $content);
439     }
440 }