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