3 namespace Tests\Activity;
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;
19 class WatchTest extends TestCase
21 public function test_watch_action_exists_on_entity_unless_active()
23 $editor = $this->users->editor();
24 $this->actingAs($editor);
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');
32 $watchOptions = new UserEntityWatchOptions($editor, $entity);
33 $watchOptions->updateLevelByValue(WatchLevels::COMMENTS);
35 $resp = $this->get($entity->getUrl());
36 $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
40 public function test_watch_action_only_shows_with_permission()
42 $viewer = $this->users->viewer();
43 $this->actingAs($viewer);
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');
52 $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
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');
61 public function test_watch_update()
63 $editor = $this->users->editor();
64 $book = $this->entities->book();
66 $this->actingAs($editor)->get($book->getUrl());
67 $resp = $this->put('/watching/update', [
68 'type' => $book->getMorphClass(),
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,
82 $resp = $this->put('/watching/update', [
83 'type' => $book->getMorphClass(),
87 $resp->assertRedirect($book->getUrl());
88 $this->assertDatabaseMissing('watches', [
89 'watchable_id' => $book->id,
90 'watchable_type' => $book->getMorphClass(),
91 'user_id' => $editor->id,
95 public function test_watch_update_fails_for_guest()
97 $this->setSettings(['app-public' => 'true']);
98 $guest = $this->users->guest();
99 $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
100 $book = $this->entities->book();
102 $resp = $this->put('/watching/update', [
103 'type' => $book->getMorphClass(),
105 'level' => 'comments'
108 $this->assertPermissionError($resp);
109 $guest->unsetRelations();
112 public function test_watch_detail_display_reflects_state()
114 $editor = $this->users->editor();
115 $book = $this->entities->bookHasChaptersAndPages();
116 $chapter = $book->chapters()->first();
117 $page = $chapter->pages()->first();
119 (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::UPDATES);
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');
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');
129 (new UserEntityWatchOptions($editor, $page))->updateLevelByValue(WatchLevels::UPDATES);
130 $this->get($page->getUrl())->assertSee('Watching new pages and updates');
133 public function test_watch_detail_ignore_indicator_cascades()
135 $editor = $this->users->editor();
136 $book = $this->entities->bookHasChaptersAndPages();
137 (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);
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');
144 public function test_watch_option_menu_shows_current_active_state()
146 $editor = $this->users->editor();
147 $book = $this->entities->book();
148 $options = new UserEntityWatchOptions($editor, $book);
150 $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
151 $respHtml->assertElementNotExists('form[action$="/watching/update"] svg[data-icon="check-circle"]');
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"]');
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"]');
162 public function test_watch_option_menu_limits_options_for_pages()
164 $editor = $this->users->editor();
165 $book = $this->entities->bookHasChaptersAndPages();
166 (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);
168 $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
169 $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="new"]');
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"]');
176 public function test_notify_own_page_changes()
178 $editor = $this->users->editor();
179 $entities = $this->entities->createChainBelongingToUser($editor);
180 $prefs = new UserNotificationPreferences($editor);
181 $prefs->updateFromSettingsArray(['own-page-changes' => 'true']);
183 $notifications = Notification::fake();
186 $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
187 $notifications->assertSentTo($editor, PageUpdateNotification::class);
190 public function test_notify_own_page_comments()
192 $editor = $this->users->editor();
193 $entities = $this->entities->createChainBelongingToUser($editor);
194 $prefs = new UserNotificationPreferences($editor);
195 $prefs->updateFromSettingsArray(['own-page-comments' => 'true']);
197 $notifications = Notification::fake();
199 $this->asAdmin()->post("/comment/{$entities['page']->id}", [
200 'text' => 'My new comment'
202 $notifications->assertSentTo($editor, CommentCreationNotification::class);
205 public function test_notify_comment_replies()
207 $editor = $this->users->editor();
208 $entities = $this->entities->createChainBelongingToUser($editor);
209 $prefs = new UserNotificationPreferences($editor);
210 $prefs->updateFromSettingsArray(['comment-replies' => 'true']);
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]);
218 $notifications = Notification::fake();
220 $this->actingAs($editor)->post("/comment/{$entities['page']->id}", [
221 'text' => 'My new comment'
223 $comment = $entities['page']->comments()->orderBy('id', 'desc')->first();
225 $this->asAdmin()->post("/comment/{$entities['page']->id}", [
226 'text' => 'My new comment response',
227 'parent_id' => $comment->local_id,
229 $notifications->assertSentTo($editor, CommentCreationNotification::class);
232 public function test_notify_watch_parent_book_ignore()
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]);
241 $notifications = Notification::fake();
243 $this->asAdmin()->post("/comment/{$entities['page']->id}", [
244 'text' => 'My new comment response',
246 $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
247 $notifications->assertNothingSent();
250 public function test_notify_watch_parent_book_comments()
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);
260 $this->actingAs($admin)->post("/comment/{$entities['page']->id}", [
261 'text' => 'My new comment response',
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');
276 public function test_notify_watch_parent_book_updates()
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);
285 $this->actingAs($admin);
286 $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
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');
300 $notifications = Notification::fake();
301 $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
302 $notifications->assertNothingSentTo($editor);
305 public function test_notify_watch_parent_book_new()
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);
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']);
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);
329 public function test_notifications_sent_in_right_language()
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);
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(),
347 $notifications = Notification::fake();
348 $logger = app()->make(ActivityLogger::class);
349 $this->actingAs($admin);
351 foreach ($activities as $activityType => $detail) {
352 $logger->add($activityType, $detail);
355 $sent = $notifications->sentNotifications()[get_class($editor)][$editor->id];
356 $this->assertCount(3, $sent);
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);
369 public function test_notifications_not_sent_if_lacking_view_permission_for_related_item()
371 $notifications = Notification::fake();
372 $editor = $this->users->editor();
373 $page = $this->entities->page();
375 $watches = new UserEntityWatchOptions($editor, $page);
376 $watches->updateLevelByValue(WatchLevels::COMMENTS);
377 $this->permissions->disableEntityInheritedPermissions($page);
379 $this->asAdmin()->post("/comment/{$page->id}", [
380 'text' => 'My new comment response',
383 $notifications->assertNothingSentTo($editor);
386 public function test_watches_deleted_on_user_delete()
388 $editor = $this->users->editor();
389 $page = $this->entities->page();
391 $watches = new UserEntityWatchOptions($editor, $page);
392 $watches->updateLevelByValue(WatchLevels::COMMENTS);
393 $this->assertDatabaseHas('watches', ['user_id' => $editor->id]);
395 $this->asAdmin()->delete($editor->getEditUrl());
397 $this->assertDatabaseMissing('watches', ['user_id' => $editor->id]);
400 public function test_watches_deleted_on_item_delete()
402 $editor = $this->users->editor();
403 $page = $this->entities->page();
405 $watches = new UserEntityWatchOptions($editor, $page);
406 $watches->updateLevelByValue(WatchLevels::COMMENTS);
407 $this->assertDatabaseHas('watches', ['watchable_type' => 'page', 'watchable_id' => $page->id]);
409 $this->entities->destroy($page);
411 $this->assertDatabaseMissing('watches', ['watchable_type' => 'page', 'watchable_id' => $page->id]);
414 public function test_page_path_in_notifications_limited_by_permissions()
416 $chapter = $this->entities->chapterHasPages();
417 $page = $chapter->pages()->first();
418 $book = $chapter->book;
419 $notification = new PageCreationNotification($page, $this->users->editor());
421 $viewer = $this->users->viewer();
422 $viewerRole = $viewer->roles()->first();
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);
427 $this->permissions->setEntityPermissions($page, ['view'], [$viewerRole]);
428 $this->permissions->setEntityPermissions($chapter, [], [$viewerRole]);
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);
434 $this->permissions->setEntityPermissions($book, [], [$viewerRole]);
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);