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