]> BookStack Code Mirror - bookstack/commitdiff
Started testing work for recycle bin implementation
authorDan Brown <redacted>
Fri, 6 Nov 2020 12:54:39 +0000 (12:54 +0000)
committerDan Brown <redacted>
Fri, 6 Nov 2020 12:54:39 +0000 (12:54 +0000)
18 files changed:
app/Entities/Entity.php
app/Entities/Managers/TrashCan.php
app/Http/Controllers/AuditLogController.php
app/Http/Controllers/BookController.php
app/Http/Controllers/BookshelfController.php
app/Http/Controllers/ChapterController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/RecycleBinController.php
tests/AuditLogTest.php
tests/Entity/BookShelfTest.php
tests/Entity/BookTest.php [new file with mode: 0644]
tests/Entity/ChapterTest.php [new file with mode: 0644]
tests/Entity/EntityTest.php
tests/Entity/PageTest.php [new file with mode: 0644]
tests/RecycleBinTest.php [new file with mode: 0644]
tests/SharedTestHelpers.php
tests/TestResponse.php
tests/Uploads/AttachmentTest.php

index ed304092919c880a093854bc00e55f23edfb7067..34cdb4b8c49e7722cf7f540c276f16f794889b9e 100644 (file)
@@ -295,7 +295,7 @@ class Entity extends Ownable
     public function getParent(): ?Entity
     {
         if ($this->isA('page')) {
-            return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book->withTrashed()->first();
+            return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
         }
         if ($this->isA('chapter')) {
             return $this->book->withTrashed()->first();
index c567edaf37d33b9084e5eab3e9e90f35fbaa4526..686918ce26cdd8c0d6ce4dc3987dac456dcea89c 100644 (file)
@@ -90,7 +90,7 @@ class TrashCan
      * Remove a bookshelf from the system.
      * @throws Exception
      */
-    public function destroyShelf(Bookshelf $shelf): int
+    protected function destroyShelf(Bookshelf $shelf): int
     {
         $this->destroyCommonRelations($shelf);
         $shelf->forceDelete();
@@ -102,7 +102,7 @@ class TrashCan
      * Destroys any child chapters and pages.
      * @throws Exception
      */
-    public function destroyBook(Book $book): int
+    protected function destroyBook(Book $book): int
     {
         $count = 0;
         $pages = $book->pages()->withTrashed()->get();
@@ -127,7 +127,7 @@ class TrashCan
      * Destroys all pages within.
      * @throws Exception
      */
-    public function destroyChapter(Chapter $chapter): int
+    protected function destroyChapter(Chapter $chapter): int
     {
         $count = 0;
         $pages = $chapter->pages()->withTrashed()->get();
@@ -147,7 +147,7 @@ class TrashCan
      * Remove a page from the system.
      * @throws Exception
      */
-    public function destroyPage(Page $page): int
+    protected function destroyPage(Page $page): int
     {
         $this->destroyCommonRelations($page);
 
@@ -182,7 +182,7 @@ class TrashCan
      * Destroy all items that have pending deletions.
      * @throws Exception
      */
-    public function destroyFromAllDeletions(): int
+    public function empty(): int
     {
         $deletions = Deletion::all();
         $deleteCount = 0;
index a3ef01baa472ec923de70068abcc84ee2fe09439..fad4e8d38a0db99ef3802ea64c2500529b3a4713 100644 (file)
@@ -23,7 +23,12 @@ class AuditLogController extends Controller
         ];
 
         $query = Activity::query()
-            ->with(['entity', 'user'])
+            ->with([
+                'entity' => function ($query) {
+                    $query->withTrashed();
+                },
+                'user'
+            ])
             ->orderBy($listDetails['sort'], $listDetails['order']);
 
         if ($listDetails['event']) {
index 1643c62f980cd151dabd3fedf0031084bd336c78..25dc651945363e38140609d54772f78957f684c0 100644 (file)
@@ -181,14 +181,13 @@ class BookController extends Controller
     /**
      * Remove the specified book from the system.
      * @throws Throwable
-     * @throws NotifyException
      */
     public function destroy(string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
 
-        Activity::addMessage('book_delete', $book->name);
+        Activity::add($book, 'book_delete', $book->id);
         $this->bookRepo->destroy($book);
 
         return redirect('/books');
index f2cc11c7ba16126eb6f7fef610c8662525d190bf..efe280235bad18e75249b7a58ac50ff25a205d62 100644 (file)
@@ -182,7 +182,7 @@ class BookshelfController extends Controller
         $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
-        Activity::addMessage('bookshelf_delete', $shelf->name);
+        Activity::add($shelf, 'bookshelf_delete');
         $this->bookshelfRepo->destroy($shelf);
 
         return redirect('/shelves');
index 1355979107eb0181d272e3610511688d5772b7b7..5d8631154c3fddfebb9fbe3b44b09d65f8587558 100644 (file)
@@ -128,7 +128,7 @@ class ChapterController extends Controller
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
-        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
+        Activity::add($chapter, 'chapter_delete', $chapter->book->id);
         $this->chapterRepo->destroy($chapter);
 
         return redirect($chapter->book->getUrl());
index ee998996f6540933470adfab2d83241f87f28cfd..6396da23ed15b7e414221065766b489e6a689135 100644 (file)
@@ -308,9 +308,8 @@ class PageController extends Controller
         $book = $page->book;
         $parent = $page->chapter ?? $book;
         $this->pageRepo->destroy($page);
-        Activity::addMessage('page_delete', $page->name, $book->id);
+        Activity::add($page, 'page_delete', $page->book_id);
 
-        $this->showSuccessNotification(trans('entities.pages_delete_success'));
         return redirect($parent->getUrl());
     }
 
index 64459da23d2307c88988a7964bbd764164cb0b4b..459dbb39dfd93585ed3f5b2b6913bc814bd8bc22 100644 (file)
@@ -14,7 +14,6 @@ class RecycleBinController extends Controller
      */
     public function __construct()
     {
-        // TODO - Check this is enforced.
         $this->middleware(function ($request, $next) {
             $this->checkPermission('settings-manage');
             $this->checkPermission('restrictions-manage-all');
@@ -96,7 +95,7 @@ class RecycleBinController extends Controller
      */
     public function empty()
     {
-        $deleteCount = (new TrashCan())->destroyFromAllDeletions();
+        $deleteCount = (new TrashCan())->empty();
 
         $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
         return redirect($this->recycleBinBaseUrl);
index a2cdc33ffda04aa1895ce0566e123167b984b9ce..94eb02599ca45b85e8f2e2b6524bb45ebb44ef01 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Actions\Activity;
 use BookStack\Actions\ActivityService;
 use BookStack\Auth\UserRepo;
+use BookStack\Entities\Managers\TrashCan;
 use BookStack\Entities\Page;
 use BookStack\Entities\Repos\PageRepo;
 use Carbon\Carbon;
@@ -40,7 +41,7 @@ class AuditLogTest extends TestCase
         $resp->assertSeeText($page->name);
         $resp->assertSeeText('page_create');
         $resp->assertSeeText($activity->created_at->toDateTimeString());
-        $resp->assertElementContains('.audit-log-user', $admin->name);
+        $resp->assertElementContains('.table-user-item', $admin->name);
     }
 
     public function test_shows_name_for_deleted_items()
@@ -51,6 +52,7 @@ class AuditLogTest extends TestCase
         app(ActivityService::class)->add($page, 'page_create', $page->book->id);
 
         app(PageRepo::class)->destroy($page);
+        app(TrashCan::class)->empty();
 
         $resp = $this->get('settings/audit');
         $resp->assertSeeText('Deleted Item');
index cb3acfb1e8eb8724d3a927e0b4c8c1a5a11d9823..c1748281e7755b7f5568ecf4c3f19418cd4b0a4e 100644 (file)
@@ -222,16 +222,25 @@ class BookShelfTest extends TestCase
 
     public function test_shelf_delete()
     {
-        $shelf = Bookshelf::first();
-        $resp = $this->asEditor()->get($shelf->getUrl('/delete'));
-        $resp->assertSeeText('Delete Bookshelf');
-        $resp->assertSee("action=\"{$shelf->getUrl()}\"");
-
-        $resp = $this->delete($shelf->getUrl());
-        $resp->assertRedirect('/shelves');
-        $this->assertDatabaseMissing('bookshelves', ['id' => $shelf->id]);
-        $this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]);
-        $this->assertSessionHas('success');
+        $shelf = Bookshelf::query()->whereHas('books')->first();
+        $this->assertNull($shelf->deleted_at);
+        $bookCount = $shelf->books()->count();
+
+        $deleteViewReq = $this->asEditor()->get($shelf->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this bookshelf?');
+
+        $deleteReq = $this->delete($shelf->getUrl());
+        $deleteReq->assertRedirect(url('/shelves'));
+        $this->assertActivityExists('bookshelf_delete', $shelf);
+
+        $shelf->refresh();
+        $this->assertNotNull($shelf->deleted_at);
+
+        $this->assertTrue($shelf->books()->count() === $bookCount);
+        $this->assertTrue($shelf->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Bookshelf Successfully Deleted');
     }
 
     public function test_shelf_copy_permissions()
diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php
new file mode 100644 (file)
index 0000000..b502bdc
--- /dev/null
@@ -0,0 +1,34 @@
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Book;
+use Tests\TestCase;
+
+class BookTest extends TestCase
+{
+    public function test_book_delete()
+    {
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
+        $this->assertNull($book->deleted_at);
+        $pageCount = $book->pages()->count();
+        $chapterCount = $book->chapters()->count();
+
+        $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
+
+        $deleteReq = $this->delete($book->getUrl());
+        $deleteReq->assertRedirect(url('/books'));
+        $this->assertActivityExists('book_delete', $book);
+
+        $book->refresh();
+        $this->assertNotNull($book->deleted_at);
+
+        $this->assertTrue($book->pages()->count() === 0);
+        $this->assertTrue($book->chapters()->count() === 0);
+        $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
+        $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
+        $this->assertTrue($book->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Book Successfully Deleted');
+    }
+}
\ No newline at end of file
diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php
new file mode 100644 (file)
index 0000000..d072f8d
--- /dev/null
@@ -0,0 +1,31 @@
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Chapter;
+use Tests\TestCase;
+
+class ChapterTest extends TestCase
+{
+    public function test_chapter_delete()
+    {
+        $chapter = Chapter::query()->whereHas('pages')->first();
+        $this->assertNull($chapter->deleted_at);
+        $pageCount = $chapter->pages()->count();
+
+        $deleteViewReq = $this->asEditor()->get($chapter->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this chapter?');
+
+        $deleteReq = $this->delete($chapter->getUrl());
+        $deleteReq->assertRedirect($chapter->getParent()->getUrl());
+        $this->assertActivityExists('chapter_delete', $chapter);
+
+        $chapter->refresh();
+        $this->assertNotNull($chapter->deleted_at);
+
+        $this->assertTrue($chapter->pages()->count() === 0);
+        $this->assertTrue($chapter->pages()->withTrashed()->count() === $pageCount);
+        $this->assertTrue($chapter->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Chapter Successfully Deleted');
+    }
+}
\ No newline at end of file
index de1e025ade6a1e832cad59834f0c0774abf8ad1e..4aad6622ff3e374633e0491e017ac1af01fab25f 100644 (file)
@@ -7,7 +7,6 @@ use BookStack\Entities\Page;
 use BookStack\Auth\UserRepo;
 use BookStack\Entities\Repos\PageRepo;
 use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
 use Tests\BrowserKitTest;
 
 class EntityTest extends BrowserKitTest
@@ -18,27 +17,10 @@ class EntityTest extends BrowserKitTest
         // Test Creation
         $book = $this->bookCreation();
         $chapter = $this->chapterCreation($book);
-        $page = $this->pageCreation($chapter);
+        $this->pageCreation($chapter);
 
         // Test Updating
-        $book = $this->bookUpdate($book);
-
-        // Test Deletion
-        $this->bookDelete($book);
-    }
-
-    public function bookDelete(Book $book)
-    {
-        $this->asAdmin()
-            ->visit($book->getUrl())
-            // Check link works correctly
-            ->click('Delete')
-            ->seePageIs($book->getUrl() . '/delete')
-            // Ensure the book name is show to user
-            ->see($book->name)
-            ->press('Confirm')
-            ->seePageIs('/books')
-            ->notSeeInDatabase('books', ['id' => $book->id]);
+        $this->bookUpdate($book);
     }
 
     public function bookUpdate(Book $book)
@@ -332,34 +314,4 @@ class EntityTest extends BrowserKitTest
             ->seePageIs($chapter->getUrl());
     }
 
-    public function test_page_delete_removes_entity_from_its_activity()
-    {
-        $page = Page::query()->first();
-
-        $this->asEditor()->put($page->getUrl(), [
-            'name' => 'My updated page',
-            'html' => '<p>updated content</p>',
-        ]);
-        $page->refresh();
-
-        $this->seeInDatabase('activities', [
-            'entity_id' => $page->id,
-            'entity_type' => $page->getMorphClass(),
-        ]);
-
-        $resp = $this->delete($page->getUrl());
-        $resp->assertResponseStatus(302);
-
-        $this->dontSeeInDatabase('activities', [
-            'entity_id' => $page->id,
-            'entity_type' => $page->getMorphClass(),
-        ]);
-
-        $this->seeInDatabase('activities', [
-            'extra' => 'My updated page',
-            'entity_id' => 0,
-            'entity_type' => '',
-        ]);
-    }
-
 }
diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php
new file mode 100644 (file)
index 0000000..742fd11
--- /dev/null
@@ -0,0 +1,27 @@
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Page;
+use Tests\TestCase;
+
+class PageTest extends TestCase
+{
+    public function test_page_delete()
+    {
+        $page = Page::query()->first();
+        $this->assertNull($page->deleted_at);
+
+        $deleteViewReq = $this->asEditor()->get($page->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this page?');
+
+        $deleteReq = $this->delete($page->getUrl());
+        $deleteReq->assertRedirect($page->getParent()->getUrl());
+        $this->assertActivityExists('page_delete', $page);
+
+        $page->refresh();
+        $this->assertNotNull($page->deleted_at);
+        $this->assertTrue($page->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Page Successfully Deleted');
+    }
+}
\ No newline at end of file
diff --git a/tests/RecycleBinTest.php b/tests/RecycleBinTest.php
new file mode 100644 (file)
index 0000000..086f636
--- /dev/null
@@ -0,0 +1,68 @@
+<?php namespace Tests;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Deletion;
+use BookStack\Entities\Page;
+
+class RecycleBinTest extends TestCase
+{
+    // TODO - Test activity updating on destroy
+
+    public function test_recycle_bin_routes_permissions()
+    {
+        $page = Page::query()->first();
+        $editor = $this->getEditor();
+        $this->actingAs($editor)->delete($page->getUrl());
+        $deletion = Deletion::query()->firstOrFail();
+
+        $routes = [
+            'GET:/settings/recycle-bin',
+            'POST:/settings/recycle-bin/empty',
+            "GET:/settings/recycle-bin/{$deletion->id}/destroy",
+            "GET:/settings/recycle-bin/{$deletion->id}/restore",
+            "POST:/settings/recycle-bin/{$deletion->id}/restore",
+            "DELETE:/settings/recycle-bin/{$deletion->id}",
+        ];
+
+        foreach($routes as $route) {
+            [$method, $url] = explode(':', $route);
+            $resp = $this->call($method, $url);
+            $this->assertPermissionError($resp);
+        }
+
+        $this->giveUserPermissions($editor, ['restrictions-manage-all']);
+
+        foreach($routes as $route) {
+            [$method, $url] = explode(':', $route);
+            $resp = $this->call($method, $url);
+            $this->assertPermissionError($resp);
+        }
+
+        $this->giveUserPermissions($editor, ['settings-manage']);
+
+        foreach($routes as $route) {
+            \DB::beginTransaction();
+            [$method, $url] = explode(':', $route);
+            $resp = $this->call($method, $url);
+            $this->assertNotPermissionError($resp);
+            \DB::rollBack();
+        }
+
+    }
+
+    public function test_recycle_bin_view()
+    {
+        $page = Page::query()->first();
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
+        $editor = $this->getEditor();
+        $this->actingAs($editor)->delete($page->getUrl());
+        $this->actingAs($editor)->delete($book->getUrl());
+
+        $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
+        $viewReq->assertElementContains('table.table', $page->name);
+        $viewReq->assertElementContains('table.table', $editor->name);
+        $viewReq->assertElementContains('table.table', $book->name);
+        $viewReq->assertElementContains('table.table', $book->pages_count . ' Pages');
+        $viewReq->assertElementContains('table.table', $book->chapters_count . ' Chapters');
+    }
+}
\ No newline at end of file
index c7659a02dabae0168348553d4f0efd360f5598d9..1ba474d76456245631d2b147360ec7c8fe97ca0d 100644 (file)
@@ -15,12 +15,14 @@ use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\HttpFetcher;
+use Illuminate\Http\Response;
 use Illuminate\Support\Env;
 use Illuminate\Support\Facades\Log;
 use Mockery;
 use Monolog\Handler\TestHandler;
 use Monolog\Logger;
 use Throwable;
+use Illuminate\Foundation\Testing\Assert as PHPUnit;
 
 trait SharedTestHelpers
 {
@@ -270,14 +272,25 @@ trait SharedTestHelpers
      */
     protected function assertPermissionError($response)
     {
-        if ($response instanceof BrowserKitTest) {
-            $response = \Illuminate\Foundation\Testing\TestResponse::fromBaseResponse($response->response);
-        }
+        PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response contains a permission error.");
+    }
 
-        $response->assertRedirect('/');
-        $this->assertSessionHas('error');
-        $error = session()->pull('error');
-        $this->assertStringStartsWith('You do not have permission to access', $error);
+    /**
+     * Assert a permission error has occurred.
+     */
+    protected function assertNotPermissionError($response)
+    {
+        PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response does not contain a permission error.");
+    }
+
+    /**
+     * Check if the given response is a permission error.
+     */
+    private function isPermissionError($response): bool
+    {
+        return $response->status() === 302
+            && $response->headers->get('Location') === url('/')
+            && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0;
     }
 
     /**
index a68a5783fa044c881bfbf8fa39b66355128ae8be..9c6b78782b4c91bb8576bfb535666559a588d772 100644 (file)
@@ -15,9 +15,8 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Get the DOM Crawler for the response content.
-     * @return Crawler
      */
-    protected function crawler()
+    protected function crawler(): Crawler
     {
         if (!is_object($this->crawlerInstance)) {
             $this->crawlerInstance = new Crawler($this->getContent());
@@ -27,7 +26,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response contains the specified element.
-     * @param string $selector
      * @return $this
      */
     public function assertElementExists(string $selector)
@@ -45,7 +43,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response does not contain the specified element.
-     * @param string $selector
      * @return $this
      */
     public function assertElementNotExists(string $selector)
@@ -63,8 +60,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response includes a specific element containing the given text.
-     * @param string $selector
-     * @param string $text
      * @return $this
      */
     public function assertElementContains(string $selector, string $text)
@@ -95,8 +90,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response does not include a specific element containing the given text.
-     * @param string $selector
-     * @param string $text
      * @return $this
      */
     public function assertElementNotContains(string $selector, string $text)
@@ -125,12 +118,20 @@ class TestResponse extends BaseTestResponse {
         return $this;
     }
 
+    /**
+     * Assert there's a notification within the view containing the given text.
+     * @return $this
+     */
+    public function assertNotificationContains(string $text)
+    {
+        return $this->assertElementContains('[notification]', $text);
+    }
+
     /**
      * Get the escaped text pattern for the constraint.
-     * @param  string  $text
      * @return string
      */
-    protected function getEscapedPattern($text)
+    protected function getEscapedPattern(string $text)
     {
         $rawPattern = preg_quote($text, '/');
         $escapedPattern = preg_quote(e($text), '/');
index a7efe08abb981d24d55e2a528d3cf2017c942033..4614c8e22489b7eaec565aaa2649772a0344d868 100644 (file)
@@ -1,5 +1,7 @@
 <?php namespace Tests\Uploads;
 
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Entities\Repos\PageRepo;
 use BookStack\Uploads\Attachment;
 use BookStack\Entities\Page;
 use BookStack\Auth\Permissions\PermissionService;
@@ -208,7 +210,8 @@ class AttachmentTest extends TestCase
             'name' => $fileName
         ]);
 
-        $this->call('DELETE', $page->getUrl());
+        app(PageRepo::class)->destroy($page);
+        app(TrashCan::class)->empty();
 
         $this->assertDatabaseMissing('attachments', [
             'name' => $fileName