Notifications: Add phpunit test for notification sending

Covers core case scenarios, and check of notification content.
This commit is contained in:
Dan Brown 2023-08-16 16:02:00 +01:00
parent bc6e19b2a1
commit 565908ef52
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
8 changed files with 208 additions and 8 deletions

View File

@ -2,7 +2,6 @@
namespace BookStack\Activity\Controllers; namespace BookStack\Activity\Controllers;
use BookStack\Activity\Models\Watch;
use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
@ -15,7 +14,9 @@ class WatchController extends Controller
{ {
public function update(Request $request) public function update(Request $request)
{ {
// TODO - Require notification permission $this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$requestData = $this->validate($request, [ $requestData = $this->validate($request, [
'level' => ['required', 'string'], 'level' => ['required', 'string'],
]); ]);

View File

@ -8,6 +8,7 @@ use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Messages\CommentCreationNotification; use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
use BookStack\Activity\Tools\EntityWatchers; use BookStack\Activity\Tools\EntityWatchers;
use BookStack\Activity\WatchLevels; use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\Page;
use BookStack\Settings\UserNotificationPreferences; use BookStack\Settings\UserNotificationPreferences;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
@ -20,15 +21,16 @@ class CommentCreationNotificationHandler extends BaseNotificationHandler
} }
// Main watchers // Main watchers
/** @var Page $page */
$page = $detail->entity; $page = $detail->entity;
$watchers = new EntityWatchers($page, WatchLevels::COMMENTS); $watchers = new EntityWatchers($page, WatchLevels::COMMENTS);
$watcherIds = $watchers->getWatcherUserIds(); $watcherIds = $watchers->getWatcherUserIds();
// Page owner if user preferences allow // Page owner if user preferences allow
if (!$watchers->isUserIgnoring($detail->created_by) && $detail->createdBy) { if (!$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
$userNotificationPrefs = new UserNotificationPreferences($detail->createdBy); $userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);
if ($userNotificationPrefs->notifyOnOwnPageComments()) { if ($userNotificationPrefs->notifyOnOwnPageComments()) {
$watcherIds[] = $detail->created_by; $watcherIds[] = $page->owned_by;
} }
} }

View File

@ -66,6 +66,16 @@ abstract class Controller extends BaseController
} }
} }
/**
* Prevent access for guest users beyond this point.
*/
protected function preventGuestAccess(): void
{
if (!signedInUser()) {
$this->showPermissionError();
}
}
/** /**
* Check the current user's permissions against an ownable item otherwise throw an exception. * Check the current user's permissions against an ownable item otherwise throw an exception.
*/ */

View File

@ -62,6 +62,7 @@ class UserPreferencesController extends Controller
public function showNotifications(PermissionApplicator $permissions) public function showNotifications(PermissionApplicator $permissions)
{ {
$this->checkPermission('receive-notifications'); $this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$preferences = (new UserNotificationPreferences(user())); $preferences = (new UserNotificationPreferences(user()));
@ -81,6 +82,7 @@ class UserPreferencesController extends Controller
public function updateNotifications(Request $request) public function updateNotifications(Request $request)
{ {
$this->checkPermission('receive-notifications'); $this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$data = $this->validate($request, [ $data = $this->validate($request, [
'preferences' => ['required', 'array'], 'preferences' => ['required', 'array'],
'preferences.*' => ['required', 'string'], 'preferences.*' => ['required', 'string'],

View File

@ -27,7 +27,7 @@ class DummyContentSeeder extends Seeder
// Create an editor user // Create an editor user
$editorUser = User::factory()->create(); $editorUser = User::factory()->create();
$editorRole = Role::getRole('editor'); $editorRole = Role::getRole('editor');
$additionalEditorPerms = ['receive-notifications']; $additionalEditorPerms = ['receive-notifications', 'comment-create-all'];
$editorRole->permissions()->syncWithoutDetaching(RolePermission::whereIn('name', $additionalEditorPerms)->pluck('id')); $editorRole->permissions()->syncWithoutDetaching(RolePermission::whereIn('name', $additionalEditorPerms)->pluck('id'));
$editorUser->attachRole($editorRole); $editorUser->attachRole($editorRole);

View File

@ -2,9 +2,14 @@
namespace Tests\Activity; namespace Tests\Activity;
use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
use BookStack\Activity\Notifications\Messages\PageCreationNotification;
use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Activity\WatchLevels; use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Settings\UserNotificationPreferences;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase; use Tests\TestCase;
class WatchTest extends TestCase class WatchTest extends TestCase
@ -83,6 +88,22 @@ class WatchTest extends TestCase
]); ]);
} }
public function test_watch_update_fails_for_guest()
{
$this->setSettings(['app-public' => 'true']);
$guest = $this->users->guest();
$this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
$book = $this->entities->book();
$resp = $this->put('/watching/update', [
'type' => get_class($book),
'id' => $book->id,
'level' => 'comments'
]);
$this->assertPermissionError($resp);
}
public function test_watch_detail_display_reflects_state() public function test_watch_detail_display_reflects_state()
{ {
$editor = $this->users->editor(); $editor = $this->users->editor();
@ -147,6 +168,147 @@ class WatchTest extends TestCase
$respHtml->assertElementNotExists('form[action$="/watching/update"] button[name="level"][value="new"]'); $respHtml->assertElementNotExists('form[action$="/watching/update"] button[name="level"][value="new"]');
} }
// TODO - Guest user cannot see/set notifications public function test_notify_own_page_changes()
// TODO - Actual notification testing {
$editor = $this->users->editor();
$entities = $this->entities->createChainBelongingToUser($editor);
$prefs = new UserNotificationPreferences($editor);
$prefs->updateFromSettingsArray(['own-page-changes' => 'true']);
$notifications = Notification::fake();
$this->asAdmin();
$this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
$notifications->assertSentTo($editor, PageUpdateNotification::class);
}
public function test_notify_own_page_comments()
{
$editor = $this->users->editor();
$entities = $this->entities->createChainBelongingToUser($editor);
$prefs = new UserNotificationPreferences($editor);
$prefs->updateFromSettingsArray(['own-page-comments' => 'true']);
$notifications = Notification::fake();
$this->asAdmin()->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment'
]);
$notifications->assertSentTo($editor, CommentCreationNotification::class);
}
public function test_notify_comment_replies()
{
$editor = $this->users->editor();
$entities = $this->entities->createChainBelongingToUser($editor);
$prefs = new UserNotificationPreferences($editor);
$prefs->updateFromSettingsArray(['comment-replies' => 'true']);
$notifications = Notification::fake();
$this->actingAs($editor)->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment'
]);
$comment = $entities['page']->comments()->first();
$this->asAdmin()->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment response',
'parent_id' => $comment->id,
]);
$notifications->assertSentTo($editor, CommentCreationNotification::class);
}
public function test_notify_watch_parent_book_ignore()
{
$editor = $this->users->editor();
$entities = $this->entities->createChainBelongingToUser($editor);
$watches = new UserEntityWatchOptions($editor, $entities['book']);
$prefs = new UserNotificationPreferences($editor);
$watches->updateWatchLevel('ignore');
$prefs->updateFromSettingsArray(['own-page-changes' => 'true', 'own-page-comments' => true]);
$notifications = Notification::fake();
$this->asAdmin()->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment response',
]);
$this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
$notifications->assertNothingSent();
}
public function test_notify_watch_parent_book_comments()
{
$notifications = Notification::fake();
$editor = $this->users->editor();
$admin = $this->users->admin();
$entities = $this->entities->createChainBelongingToUser($editor);
$watches = new UserEntityWatchOptions($editor, $entities['book']);
$watches->updateWatchLevel('comments');
// Comment post
$this->actingAs($admin)->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment response',
]);
$notifications->assertSentTo($editor, function (CommentCreationNotification $notification) use ($editor, $admin, $entities) {
$mail = $notification->toMail($editor);
$mailContent = html_entity_decode(strip_tags($mail->render()));
return $mail->subject === 'New comment on page: ' . $entities['page']->getShortName()
&& str_contains($mailContent, 'View Comment')
&& str_contains($mailContent, 'Page Name: ' . $entities['page']->name)
&& str_contains($mailContent, 'Commenter: ' . $admin->name)
&& str_contains($mailContent, 'Comment: My new comment response');
});
}
public function test_notify_watch_parent_book_updates()
{
$notifications = Notification::fake();
$editor = $this->users->editor();
$admin = $this->users->admin();
$entities = $this->entities->createChainBelongingToUser($editor);
$watches = new UserEntityWatchOptions($editor, $entities['book']);
$watches->updateWatchLevel('updates');
$this->actingAs($admin);
$this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
$notifications->assertSentTo($editor, function (PageUpdateNotification $notification) use ($editor, $admin) {
$mail = $notification->toMail($editor);
$mailContent = html_entity_decode(strip_tags($mail->render()));
return $mail->subject === 'Updated page: Updated page'
&& str_contains($mailContent, 'View Page')
&& str_contains($mailContent, 'Page Name: Updated page')
&& str_contains($mailContent, 'Updated By: ' . $admin->name)
&& str_contains($mailContent, 'you won\'t be sent notifications for further edits to this page by the same editor');
});
// Test debounce
$notifications = Notification::fake();
$this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
$notifications->assertNothingSentTo($editor);
}
public function test_notify_watch_parent_book_new()
{
$notifications = Notification::fake();
$editor = $this->users->editor();
$admin = $this->users->admin();
$entities = $this->entities->createChainBelongingToUser($editor);
$watches = new UserEntityWatchOptions($editor, $entities['book']);
$watches->updateWatchLevel('new');
$this->actingAs($admin)->get($entities['chapter']->getUrl('/create-page'));
$page = $entities['chapter']->pages()->where('draft', '=', true)->first();
$this->post($page->getUrl(), ['name' => 'My new page', 'html' => 'My new page content']);
$notifications->assertSentTo($editor, function (PageCreationNotification $notification) use ($editor, $admin) {
$mail = $notification->toMail($editor);
$mailContent = html_entity_decode(strip_tags($mail->render()));
return $mail->subject === 'New page: My new page'
&& str_contains($mailContent, 'View Page')
&& str_contains($mailContent, 'Page Name: My new page')
&& str_contains($mailContent, 'Created By: ' . $admin->name);
});
}
} }

View File

@ -50,6 +50,14 @@ class UserRoleProvider
return $user; return $user;
} }
/**
* Get the system "guest" user.
*/
public function guest(): User
{
return User::where('system_name', '=', 'public')->firstOrFail();
}
/** /**
* Create a new fresh user without any relations. * Create a new fresh user without any relations.
*/ */

View File

@ -121,6 +121,21 @@ class UserPreferencesTest extends TestCase
$resp->assertDontSee('All Page Updates & Comments'); $resp->assertDontSee('All Page Updates & Comments');
} }
public function test_notification_preferences_not_accessible_to_guest()
{
$this->setSettings(['app-public' => 'true']);
$guest = $this->users->guest();
$this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
$resp = $this->get('/preferences/notifications');
$this->assertPermissionError($resp);
$resp = $this->put('/preferences/notifications', [
'preferences' => ['comment-replies' => 'true'],
]);
$this->assertPermissionError($resp);
}
public function test_update_sort_preference() public function test_update_sort_preference()
{ {
$editor = $this->users->editor(); $editor = $this->users->editor();