From bc6e19b2a1ef424d6901cf07c179e445deede08e Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Tue, 15 Aug 2023 20:08:27 +0100
Subject: [PATCH] Notifications: Added testing to cover controls

---
 app/Activity/Controllers/WatchController.php |   1 +
 database/seeders/DummyContentSeeder.php      |   2 +
 tests/Activity/WatchTest.php                 | 152 +++++++++++++++++++
 tests/User/UserPreferencesTest.php           |  21 ++-
 4 files changed, 175 insertions(+), 1 deletion(-)
 create mode 100644 tests/Activity/WatchTest.php

diff --git a/app/Activity/Controllers/WatchController.php b/app/Activity/Controllers/WatchController.php
index e0596864c..e2e27ca4a 100644
--- a/app/Activity/Controllers/WatchController.php
+++ b/app/Activity/Controllers/WatchController.php
@@ -15,6 +15,7 @@ class WatchController extends Controller
 {
     public function update(Request $request)
     {
+        // TODO - Require notification permission
         $requestData = $this->validate($request, [
             'level' => ['required', 'string'],
         ]);
diff --git a/database/seeders/DummyContentSeeder.php b/database/seeders/DummyContentSeeder.php
index 0f030d671..f0d3ffcdb 100644
--- a/database/seeders/DummyContentSeeder.php
+++ b/database/seeders/DummyContentSeeder.php
@@ -27,6 +27,8 @@ class DummyContentSeeder extends Seeder
         // Create an editor user
         $editorUser = User::factory()->create();
         $editorRole = Role::getRole('editor');
+        $additionalEditorPerms = ['receive-notifications'];
+        $editorRole->permissions()->syncWithoutDetaching(RolePermission::whereIn('name', $additionalEditorPerms)->pluck('id'));
         $editorUser->attachRole($editorRole);
 
         // Create a viewer user
diff --git a/tests/Activity/WatchTest.php b/tests/Activity/WatchTest.php
new file mode 100644
index 000000000..919e52608
--- /dev/null
+++ b/tests/Activity/WatchTest.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Tests\Activity;
+
+use BookStack\Activity\Tools\UserEntityWatchOptions;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\Entity;
+use Tests\TestCase;
+
+class WatchTest extends TestCase
+{
+    public function test_watch_action_exists_on_entity_unless_active()
+    {
+        $editor = $this->users->editor();
+        $this->actingAs($editor);
+
+        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];
+        /** @var Entity $entity */
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementContains('form[action$="/watching/update"] button.icon-list-item', 'Watch');
+
+            $watchOptions = new UserEntityWatchOptions($editor, $entity);
+            $watchOptions->updateWatchLevel('comments');
+
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
+        }
+    }
+
+    public function test_watch_action_only_shows_with_permission()
+    {
+        $viewer = $this->users->viewer();
+        $this->actingAs($viewer);
+
+        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];
+        /** @var Entity $entity */
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
+        }
+
+        $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
+
+        /** @var Entity $entity */
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementExists('form[action$="/watching/update"] button.icon-list-item');
+        }
+    }
+
+    public function test_watch_update()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->book();
+
+        $this->actingAs($editor)->get($book->getUrl());
+        $resp = $this->put('/watching/update', [
+            'type' => get_class($book),
+            'id' => $book->id,
+            'level' => 'comments'
+        ]);
+
+        $resp->assertRedirect($book->getUrl());
+        $this->assertSessionHas('success');
+        $this->assertDatabaseHas('watches', [
+            'watchable_id' => $book->id,
+            'watchable_type' => $book->getMorphClass(),
+            'user_id' => $editor->id,
+            'level' => WatchLevels::COMMENTS,
+        ]);
+
+        $resp = $this->put('/watching/update', [
+            'type' => get_class($book),
+            'id' => $book->id,
+            'level' => 'default'
+        ]);
+        $resp->assertRedirect($book->getUrl());
+        $this->assertDatabaseMissing('watches', [
+            'watchable_id' => $book->id,
+            'watchable_type' => $book->getMorphClass(),
+            'user_id' => $editor->id,
+        ]);
+    }
+
+    public function test_watch_detail_display_reflects_state()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->bookHasChaptersAndPages();
+        $chapter = $book->chapters()->first();
+        $page = $chapter->pages()->first();
+
+        (new UserEntityWatchOptions($editor, $book))->updateWatchLevel('updates');
+
+        $this->actingAs($editor)->get($book->getUrl())->assertSee('Watching new pages and updates');
+        $this->get($chapter->getUrl())->assertSee('Watching via parent book');
+        $this->get($page->getUrl())->assertSee('Watching via parent book');
+
+        (new UserEntityWatchOptions($editor, $chapter))->updateWatchLevel('comments');
+        $this->get($chapter->getUrl())->assertSee('Watching new pages, updates & comments');
+        $this->get($page->getUrl())->assertSee('Watching via parent chapter');
+
+        (new UserEntityWatchOptions($editor, $page))->updateWatchLevel('updates');
+        $this->get($page->getUrl())->assertSee('Watching new pages and updates');
+    }
+
+    public function test_watch_detail_ignore_indicator_cascades()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->bookHasChaptersAndPages();
+        (new UserEntityWatchOptions($editor, $book))->updateWatchLevel('ignore');
+
+        $this->actingAs($editor)->get($book->getUrl())->assertSee('Ignoring notifications');
+        $this->get($book->chapters()->first()->getUrl())->assertSee('Ignoring via parent book');
+        $this->get($book->pages()->first()->getUrl())->assertSee('Ignoring via parent book');
+    }
+
+    public function test_watch_option_menu_shows_current_active_state()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->book();
+        $options = new UserEntityWatchOptions($editor, $book);
+
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementNotExists('form[action$="/watching/update"] svg[data-icon="check-circle"]');
+
+        $options->updateWatchLevel('comments');
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[value="comments"] svg[data-icon="check-circle"]');
+
+        $options->updateWatchLevel('ignore');
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[value="ignore"] svg[data-icon="check-circle"]');
+    }
+
+    public function test_watch_option_menu_limits_options_for_pages()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->bookHasChaptersAndPages();
+        (new UserEntityWatchOptions($editor, $book))->updateWatchLevel('ignore');
+
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="new"]');
+
+        $respHtml = $this->withHtml($this->get($book->pages()->first()->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="updates"]');
+        $respHtml->assertElementNotExists('form[action$="/watching/update"] button[name="level"][value="new"]');
+    }
+
+    // TODO - Guest user cannot see/set notifications
+    // TODO - Actual notification testing
+}
diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php
index a30484bd2..a0e7e063f 100644
--- a/tests/User/UserPreferencesTest.php
+++ b/tests/User/UserPreferencesTest.php
@@ -2,6 +2,7 @@
 
 namespace Tests\User;
 
+use BookStack\Activity\Tools\UserEntityWatchOptions;
 use Tests\TestCase;
 
 class UserPreferencesTest extends TestCase
@@ -79,7 +80,6 @@ class UserPreferencesTest extends TestCase
     public function test_notification_preferences_updating()
     {
         $editor = $this->users->editor();
-        $this->permissions->grantUserRolePermissions($editor, ['receive-notifications']);
 
         // View preferences with defaults
         $resp = $this->actingAs($editor)->get('/preferences/notifications');
@@ -102,6 +102,25 @@ class UserPreferencesTest extends TestCase
         $html->assertFieldHasValue('preferences[comment-replies]', 'true');
     }
 
+    public function test_notification_preferences_show_watches()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->book();
+
+        $options = new UserEntityWatchOptions($editor, $book);
+        $options->updateWatchLevel('comments');
+
+        $resp = $this->actingAs($editor)->get('/preferences/notifications');
+        $resp->assertSee($book->name);
+        $resp->assertSee('All Page Updates & Comments');
+
+        $options->updateWatchLevel('default');
+
+        $resp = $this->actingAs($editor)->get('/preferences/notifications');
+        $resp->assertDontSee($book->name);
+        $resp->assertDontSee('All Page Updates & Comments');
+    }
+
     public function test_update_sort_preference()
     {
         $editor = $this->users->editor();