]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Improve ManageTagController coverage and error handling
authorArthurHoaro <arthur@hoa.ro>
Sat, 13 Jun 2020 13:37:02 +0000 (15:37 +0200)
committerArthurHoaro <arthur@hoa.ro>
Thu, 23 Jul 2020 19:19:21 +0000 (21:19 +0200)
application/front/controller/admin/ManageShaareController.php [moved from application/front/controller/admin/PostBookmarkController.php with 87% similarity]
index.php
tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php [new file with mode: 0644]
tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php [new file with mode: 0644]
tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php [new file with mode: 0644]
tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php [new file with mode: 0644]
tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php [new file with mode: 0644]
tests/front/controller/admin/PostBookmarkControllerTest.php [deleted file]

similarity index 87%
rename from application/front/controller/admin/PostBookmarkController.php
rename to application/front/controller/admin/ManageShaareController.php
index f3ee5deac844079416e9b04f785590acddaf7444..620bbc400961057c9452c62c5da45068c4f0d9de 100644 (file)
@@ -16,7 +16,7 @@ use Slim\Http\Response;
  *
  * Slim controller used to handle Shaarli create or edit bookmarks.
  */
-class PostBookmarkController extends ShaarliAdminController
+class ManageShaareController extends ShaarliAdminController
 {
     /**
      * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
@@ -33,7 +33,7 @@ class PostBookmarkController extends ShaarliAdminController
 
     /**
      * GET /admin/shaare - Displays the bookmark form for creation.
-     *               Note that if the URL is found in existing bookmarks, then it will be in edit mode.
+     *                     Note that if the URL is found in existing bookmarks, then it will be in edit mode.
      */
     public function displayCreateForm(Request $request, Response $response): Response
     {
@@ -97,14 +97,17 @@ class PostBookmarkController extends ShaarliAdminController
      */
     public function displayEditForm(Request $request, Response $response, array $args): Response
     {
-        $id = $args['id'];
+        $id = $args['id'] ?? '';
         try {
             if (false === ctype_digit($id)) {
                 throw new BookmarkNotFoundException();
             }
-            $bookmark = $this->container->bookmarkService->get($id);  // Read database
+            $bookmark = $this->container->bookmarkService->get((int) $id);  // Read database
         } catch (BookmarkNotFoundException $e) {
-            $this->saveErrorMessage(t('Bookmark not found'));
+            $this->saveErrorMessage(sprintf(
+                t('Bookmark with identifier %s could not be found.'),
+                $id
+            ));
 
             return $this->redirect($response, '/');
         }
@@ -177,10 +180,10 @@ class PostBookmarkController extends ShaarliAdminController
     {
         $this->checkToken($request);
 
-        $ids = escape(trim($request->getParam('id')));
-        if (strpos($ids, ' ') !== false) {
+        $ids = escape(trim($request->getParam('id') ?? ''));
+        if (empty($ids) || strpos($ids, ' ') !== false) {
             // multiple, space-separated ids provided
-            $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen'));
+            $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
         } else {
             $ids = [$ids];
         }
@@ -193,16 +196,28 @@ class PostBookmarkController extends ShaarliAdminController
         }
 
         $formatter = $this->container->formatterFactory->getFormatter('raw');
+        $count = 0;
         foreach ($ids as $id) {
-            $id = (int) $id;
-            // TODO: check if it exists
-            $bookmark = $this->container->bookmarkService->get($id);
+            try {
+                $bookmark = $this->container->bookmarkService->get((int) $id);
+            } catch (BookmarkNotFoundException $e) {
+                $this->saveErrorMessage(sprintf(
+                    t('Bookmark with identifier %s could not be found.'),
+                    $id
+                ));
+
+                continue;
+            }
+
             $data = $formatter->format($bookmark);
             $this->container->pluginManager->executeHooks('delete_link', $data);
             $this->container->bookmarkService->remove($bookmark, false);
+            ++ $count;
         }
 
-        $this->container->bookmarkService->save();
+        if ($count > 0) {
+            $this->container->bookmarkService->save();
+        }
 
         // If we are called from the bookmarklet, we must close the popup:
         if ($request->getParam('source') === 'bookmarklet') {
@@ -213,6 +228,11 @@ class PostBookmarkController extends ShaarliAdminController
         return $this->redirect($response, '/');
     }
 
+    /**
+     * Helper function used to display the shaare form whether it's a new or existing bookmark.
+     *
+     * @param array $link data used in template, either from parameters or from the data store
+     */
     protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
     {
         $tags = $this->container->bookmarkService->bookmarksCountPerTag();
index aa358da03aaa469353300fef91c10f3228bf43ba..12c7a8f189b98999f1871562edb4fea889a83b3b 100644 (file)
--- a/index.php
+++ b/index.php
@@ -1159,11 +1159,11 @@ $app->group('', function () {
     $this->post('/admin/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
     $this->get('/admin/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
     $this->post('/admin/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
-    $this->get('/admin/add-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:addShaare');
-    $this->get('/admin/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayCreateForm');
-    $this->get('/admin/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayEditForm');
-    $this->post('/admin/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:save');
-    $this->get('/admin/shaare/delete', '\Shaarli\Front\Controller\Admin\PostBookmarkController:deleteBookmark');
+    $this->get('/admin/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare');
+    $this->get('/admin/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm');
+    $this->get('/admin/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm');
+    $this->post('/admin/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save');
+    $this->get('/admin/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark');
 
     $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage');
     $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
new file mode 100644 (file)
index 0000000..7d5b752
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class AddShaareTest extends TestCase
+{
+    use FrontAdminControllerMockHelper;
+
+    /** @var ManageShaareController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->httpAccess = $this->createMock(HttpAccess::class);
+        $this->controller = new ManageShaareController($this->container);
+    }
+
+    /**
+     * Test displaying add link page
+     */
+    public function testAddShaare(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $result = $this->controller->addShaare($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('addlink', (string) $result->getBody());
+
+        static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
+    }
+}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
new file mode 100644 (file)
index 0000000..caaf549
--- /dev/null
@@ -0,0 +1,361 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DeleteBookmarkTest extends TestCase
+{
+    use FrontAdminControllerMockHelper;
+
+    /** @var ManageShaareController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->httpAccess = $this->createMock(HttpAccess::class);
+        $this->controller = new ManageShaareController($this->container);
+    }
+
+    /**
+     * Delete bookmark - Single bookmark with valid parameters
+     */
+    public function testDeleteSingleBookmark(): void
+    {
+        $parameters = ['id' => '123'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123');
+
+        $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
+        $this->container->bookmarkService->expects(static::once())->method('remove')->with($bookmark, false);
+        $this->container->bookmarkService->expects(static::once())->method('save');
+        $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+        $this->container->formatterFactory
+            ->expects(static::once())
+            ->method('getFormatter')
+            ->with('raw')
+            ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
+                $formatter = $this->createMock(BookmarkFormatter::class);
+
+                $formatter->expects(static::once())->method('format')->with($bookmark);
+
+                return $formatter;
+            })
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::once())
+            ->method('executeHooks')
+            ->with('delete_link')
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Delete bookmark - Multiple bookmarks with valid parameters
+     */
+    public function testDeleteMultipleBookmarks(): void
+    {
+        $parameters = ['id' => '123 456 789'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $bookmarks = [
+            (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
+            (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456'),
+            (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
+        ];
+
+        $this->container->bookmarkService
+            ->expects(static::exactly(3))
+            ->method('get')
+            ->withConsecutive([123], [456], [789])
+            ->willReturnOnConsecutiveCalls(...$bookmarks)
+        ;
+        $this->container->bookmarkService
+            ->expects(static::exactly(3))
+            ->method('remove')
+            ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+                return [$bookmark, false];
+            }, $bookmarks))
+        ;
+        $this->container->bookmarkService->expects(static::once())->method('save');
+        $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+        $this->container->formatterFactory
+            ->expects(static::once())
+            ->method('getFormatter')
+            ->with('raw')
+            ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
+                $formatter = $this->createMock(BookmarkFormatter::class);
+
+                $formatter
+                    ->expects(static::exactly(3))
+                    ->method('format')
+                    ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+                        return [$bookmark];
+                    }, $bookmarks))
+                ;
+
+                return $formatter;
+            })
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::exactly(3))
+            ->method('executeHooks')
+            ->with('delete_link')
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Delete bookmark - Single bookmark not found in the data store
+     */
+    public function testDeleteSingleBookmarkNotFound(): void
+    {
+        $parameters = ['id' => '123'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('get')
+            ->willThrowException(new BookmarkNotFoundException())
+        ;
+        $this->container->bookmarkService->expects(static::never())->method('remove');
+        $this->container->bookmarkService->expects(static::never())->method('save');
+        $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+        $this->container->formatterFactory
+            ->expects(static::once())
+            ->method('getFormatter')
+            ->with('raw')
+            ->willReturnCallback(function (): BookmarkFormatter {
+                $formatter = $this->createMock(BookmarkFormatter::class);
+
+                $formatter->expects(static::never())->method('format');
+
+                return $formatter;
+            })
+        ;
+        // Make sure that PluginManager hook is not triggered
+        $this->container->pluginManager
+            ->expects(static::never())
+            ->method('executeHooks')
+            ->with('delete_link')
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Delete bookmark - Multiple bookmarks with one not found in the data store
+     */
+    public function testDeleteMultipleBookmarksOneNotFound(): void
+    {
+        $parameters = ['id' => '123 456 789'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $bookmarks = [
+            (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
+            (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
+        ];
+
+        $this->container->bookmarkService
+            ->expects(static::exactly(3))
+            ->method('get')
+            ->withConsecutive([123], [456], [789])
+            ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
+                if ($id === 123) {
+                    return $bookmarks[0];
+                }
+                if ($id === 789) {
+                    return $bookmarks[1];
+                }
+                throw new BookmarkNotFoundException();
+            })
+        ;
+        $this->container->bookmarkService
+            ->expects(static::exactly(2))
+            ->method('remove')
+            ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+                return [$bookmark, false];
+            }, $bookmarks))
+        ;
+        $this->container->bookmarkService->expects(static::once())->method('save');
+        $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+        $this->container->formatterFactory
+            ->expects(static::once())
+            ->method('getFormatter')
+            ->with('raw')
+            ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
+                $formatter = $this->createMock(BookmarkFormatter::class);
+
+                $formatter
+                    ->expects(static::exactly(2))
+                    ->method('format')
+                    ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+                        return [$bookmark];
+                    }, $bookmarks))
+                ;
+
+                return $formatter;
+            })
+        ;
+
+        // Make sure that PluginManager hook is not triggered
+        $this->container->pluginManager
+            ->expects(static::exactly(2))
+            ->method('executeHooks')
+            ->with('delete_link')
+        ;
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Delete bookmark - Invalid ID
+     */
+    public function testDeleteInvalidId(): void
+    {
+        $parameters = ['id' => 'nope not an ID'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Delete bookmark - Empty ID
+     */
+    public function testDeleteEmptyId(): void
+    {
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Delete bookmark - from bookmarklet
+     */
+    public function testDeleteBookmarkFromBookmarklet(): void
+    {
+        $parameters = [
+            'id' => '123',
+            'source' => 'bookmarklet',
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+        $this->container->formatterFactory
+            ->expects(static::once())
+            ->method('getFormatter')
+            ->willReturn($this->createMock(BookmarkFormatter::class))
+        ;
+
+        $result = $this->controller->deleteBookmark($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('<script>self.close();</script>', (string) $result->getBody('location'));
+    }
+}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
new file mode 100644 (file)
index 0000000..777583d
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DisplayCreateFormTest extends TestCase
+{
+    use FrontAdminControllerMockHelper;
+
+    /** @var ManageShaareController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->httpAccess = $this->createMock(HttpAccess::class);
+        $this->controller = new ManageShaareController($this->container);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * Ensure that every step of the standard workflow works properly.
+     */
+    public function testDisplayCreateFormWithUrl(): void
+    {
+        $this->container->environment = [
+            'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
+        ];
+
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
+        $expectedUrl = str_replace('&utm_ad=pay', '', $url);
+        $remoteTitle = 'Remote Title';
+        $remoteDesc = 'Sometimes the meta description is relevant.';
+        $remoteTags = 'abc def';
+
+        $request = $this->createMock(Request::class);
+        $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
+            return $key === 'post' ? $url : null;
+        });
+        $response = new Response();
+
+        $this->container->httpAccess
+            ->expects(static::once())
+            ->method('getCurlDownloadCallback')
+            ->willReturnCallback(
+                function (&$charset, &$title, &$description, &$tags) use (
+                    $remoteTitle,
+                    $remoteDesc,
+                    $remoteTags
+                ): callable {
+                    return function () use (
+                        &$charset,
+                        &$title,
+                        &$description,
+                        &$tags,
+                        $remoteTitle,
+                        $remoteDesc,
+                        $remoteTags
+                    ): void {
+                        $charset = 'ISO-8859-1';
+                        $title = $remoteTitle;
+                        $description = $remoteDesc;
+                        $tags = $remoteTags;
+                    };
+                }
+            )
+        ;
+        $this->container->httpAccess
+            ->expects(static::once())
+            ->method('getHttpResponse')
+            ->with($expectedUrl, 30, 4194304)
+            ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
+                $callback();
+            })
+        ;
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('bookmarksCountPerTag')
+            ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::at(0))
+            ->method('executeHooks')
+            ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
+                static::assertSame('render_editlink', $hook);
+                static::assertSame($remoteTitle, $data['link']['title']);
+                static::assertSame($remoteDesc, $data['link']['description']);
+
+                return $data;
+            })
+        ;
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
+
+        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+        static::assertSame($remoteTitle, $assignedVariables['link']['title']);
+        static::assertSame($remoteDesc, $assignedVariables['link']['description']);
+        static::assertSame($remoteTags, $assignedVariables['link']['tags']);
+        static::assertFalse($assignedVariables['link']['private']);
+
+        static::assertTrue($assignedVariables['link_is_new']);
+        static::assertSame($referer, $assignedVariables['http_referer']);
+        static::assertSame($tags, $assignedVariables['tags']);
+        static::assertArrayHasKey('source', $assignedVariables);
+        static::assertArrayHasKey('default_private_links', $assignedVariables);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * Ensure all available query parameters are handled properly.
+     */
+    public function testDisplayCreateFormWithFullParameters(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $parameters = [
+            'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
+            'title' => 'Provided Title',
+            'description' => 'Provided description.',
+            'tags' => 'abc def',
+            'private' => '1',
+            'source' => 'apps',
+        ];
+        $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            });
+        $response = new Response();
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
+
+        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+        static::assertSame($parameters['title'], $assignedVariables['link']['title']);
+        static::assertSame($parameters['description'], $assignedVariables['link']['description']);
+        static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
+        static::assertTrue($assignedVariables['link']['private']);
+        static::assertTrue($assignedVariables['link_is_new']);
+        static::assertSame($parameters['source'], $assignedVariables['source']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * Without any parameter.
+     */
+    public function testDisplayCreateFormEmpty(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+        static::assertSame('', $assignedVariables['link']['url']);
+        static::assertSame('Note: ', $assignedVariables['link']['title']);
+        static::assertSame('', $assignedVariables['link']['description']);
+        static::assertSame('', $assignedVariables['link']['tags']);
+        static::assertFalse($assignedVariables['link']['private']);
+        static::assertTrue($assignedVariables['link_is_new']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * URL not using HTTP protocol: do not try to retrieve the title
+     */
+    public function testDisplayCreateFormNotHttp(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $url = 'magnet://kubuntu.torrent';
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($url): ?string {
+                return $key === 'post' ? $url : null;
+            });
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+        static::assertSame($url, $assignedVariables['link']['url']);
+        static::assertTrue($assignedVariables['link_is_new']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
+     */
+    public function testDisplayCreateFormWithMarkdownEnabled(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf
+            ->expects(static::atLeastOnce())
+            ->method('get')->willReturnCallback(function (string $key): ?string {
+                if ($key === 'formatter') {
+                    return 'markdown';
+                }
+
+                return $key;
+            })
+        ;
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+        static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * When an existing URL is submitted, we want to edit the existing link.
+     */
+    public function testDisplayCreateFormWithExistingUrl(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
+        $expectedUrl = str_replace('&utm_ad=pay', '', $url);
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($url): ?string {
+                return $key === 'post' ? $url : null;
+            });
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('findByUrl')
+            ->with($expectedUrl)
+            ->willReturn(
+                (new Bookmark())
+                    ->setId($id = 23)
+                    ->setUrl($expectedUrl)
+                    ->setTitle($title = 'Bookmark Title')
+                    ->setDescription($description = 'Bookmark description.')
+                    ->setTags($tags = ['abc', 'def'])
+                    ->setPrivate(true)
+                    ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
+            )
+        ;
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
+        static::assertFalse($assignedVariables['link_is_new']);
+
+        static::assertSame($id, $assignedVariables['link']['id']);
+        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+        static::assertSame($title, $assignedVariables['link']['title']);
+        static::assertSame($description, $assignedVariables['link']['description']);
+        static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+        static::assertTrue($assignedVariables['link']['private']);
+        static::assertSame($createdAt, $assignedVariables['link']['created']);
+    }
+}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php
new file mode 100644 (file)
index 0000000..1a1cdcf
--- /dev/null
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DisplayEditFormTest extends TestCase
+{
+    use FrontAdminControllerMockHelper;
+
+    /** @var ManageShaareController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->httpAccess = $this->createMock(HttpAccess::class);
+        $this->controller = new ManageShaareController($this->container);
+    }
+
+    /**
+     * Test displaying bookmark edit form
+     * When an existing ID is provided, ensure that default workflow works properly.
+     */
+    public function testDisplayEditFormDefault(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $id = 11;
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('get')
+            ->with($id)
+            ->willReturn(
+                (new Bookmark())
+                    ->setId($id)
+                    ->setUrl($url = 'http://domain.tld')
+                    ->setTitle($title = 'Bookmark Title')
+                    ->setDescription($description = 'Bookmark description.')
+                    ->setTags($tags = ['abc', 'def'])
+                    ->setPrivate(true)
+                    ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
+            )
+        ;
+
+        $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
+        static::assertFalse($assignedVariables['link_is_new']);
+
+        static::assertSame($id, $assignedVariables['link']['id']);
+        static::assertSame($url, $assignedVariables['link']['url']);
+        static::assertSame($title, $assignedVariables['link']['title']);
+        static::assertSame($description, $assignedVariables['link']['description']);
+        static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+        static::assertTrue($assignedVariables['link']['private']);
+        static::assertSame($createdAt, $assignedVariables['link']['created']);
+    }
+
+    /**
+     * Test displaying bookmark edit form
+     * Invalid ID provided.
+     */
+    public function testDisplayEditFormInvalidId(): void
+    {
+        $id = 'invalid';
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
+        ;
+
+        $result = $this->controller->displayEditForm($request, $response, ['id' => $id]);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Test displaying bookmark edit form
+     * ID not provided.
+     */
+    public function testDisplayEditFormIdNotProvided(): void
+    {
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier  could not be found.'])
+        ;
+
+        $result = $this->controller->displayEditForm($request, $response, []);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+
+    /**
+     * Test displaying bookmark edit form
+     * Bookmark not found.
+     */
+    public function testDisplayEditFormBookmarkNotFound(): void
+    {
+        $id = 123;
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('get')
+            ->with($id)
+            ->willThrowException(new BookmarkNotFoundException())
+        ;
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
+        ;
+
+        $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame(['/subfolder/'], $result->getHeader('location'));
+    }
+}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
new file mode 100644 (file)
index 0000000..dabcd60
--- /dev/null
@@ -0,0 +1,282 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class SaveBookmarkTest extends TestCase
+{
+    use FrontAdminControllerMockHelper;
+
+    /** @var ManageShaareController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->httpAccess = $this->createMock(HttpAccess::class);
+        $this->controller = new ManageShaareController($this->container);
+    }
+
+    /**
+     * Test save a new bookmark
+     */
+    public function testSaveBookmark(): void
+    {
+        $id = 21;
+        $parameters = [
+            'lf_url' => 'http://url.tld/other?part=3#hash',
+            'lf_title' => 'Provided Title',
+            'lf_description' => 'Provided description.',
+            'lf_tags' => 'abc def',
+            'lf_private' => '1',
+            'returnurl' => 'http://shaarli.tld/subfolder/admin/add-shaare'
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $checkBookmark = function (Bookmark $bookmark) use ($parameters) {
+            static::assertSame($parameters['lf_url'], $bookmark->getUrl());
+            static::assertSame($parameters['lf_title'], $bookmark->getTitle());
+            static::assertSame($parameters['lf_description'], $bookmark->getDescription());
+            static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
+            static::assertTrue($bookmark->isPrivate());
+        };
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('addOrSet')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertFalse($save);
+
+                $checkBookmark($bookmark);
+
+                $bookmark->setId($id);
+            })
+        ;
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('set')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertTrue($save);
+
+                $checkBookmark($bookmark);
+
+                static::assertSame($id, $bookmark->getId());
+            })
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::at(0))
+            ->method('executeHooks')
+            ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
+                static::assertSame('save_link', $hook);
+
+                static::assertSame($id, $data['id']);
+                static::assertSame($parameters['lf_url'], $data['url']);
+                static::assertSame($parameters['lf_title'], $data['title']);
+                static::assertSame($parameters['lf_description'], $data['description']);
+                static::assertSame($parameters['lf_tags'], $data['tags']);
+                static::assertTrue($data['private']);
+
+                return $data;
+            })
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertRegExp('@/subfolder/#[\w\-]{6}@', $result->getHeader('location')[0]);
+    }
+
+
+    /**
+     * Test save an existing bookmark
+     */
+    public function testSaveExistingBookmark(): void
+    {
+        $id = 21;
+        $parameters = [
+            'lf_id' => (string) $id,
+            'lf_url' => 'http://url.tld/other?part=3#hash',
+            'lf_title' => 'Provided Title',
+            'lf_description' => 'Provided description.',
+            'lf_tags' => 'abc def',
+            'lf_private' => '1',
+            'returnurl' => 'http://shaarli.tld/subfolder/?page=2'
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
+            static::assertSame($id, $bookmark->getId());
+            static::assertSame($parameters['lf_url'], $bookmark->getUrl());
+            static::assertSame($parameters['lf_title'], $bookmark->getTitle());
+            static::assertSame($parameters['lf_description'], $bookmark->getDescription());
+            static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
+            static::assertTrue($bookmark->isPrivate());
+        };
+
+        $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('get')
+            ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
+        ;
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('addOrSet')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertFalse($save);
+
+                $checkBookmark($bookmark);
+            })
+        ;
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('set')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertTrue($save);
+
+                $checkBookmark($bookmark);
+
+                static::assertSame($id, $bookmark->getId());
+            })
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::at(0))
+            ->method('executeHooks')
+            ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
+                static::assertSame('save_link', $hook);
+
+                static::assertSame($id, $data['id']);
+                static::assertSame($parameters['lf_url'], $data['url']);
+                static::assertSame($parameters['lf_title'], $data['title']);
+                static::assertSame($parameters['lf_description'], $data['description']);
+                static::assertSame($parameters['lf_tags'], $data['tags']);
+                static::assertTrue($data['private']);
+
+                return $data;
+            })
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertRegExp('@/subfolder/\?page=2#[\w\-]{6}@', $result->getHeader('location')[0]);
+    }
+
+    /**
+     * Test save a bookmark - try to retrieve the thumbnail
+     */
+    public function testSaveBookmarkWithThumbnail(): void
+    {
+        $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+            return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
+        });
+
+        $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
+        $this->container->thumbnailer
+            ->expects(static::once())
+            ->method('get')
+            ->with($parameters['lf_url'])
+            ->willReturn($thumb = 'http://thumb.url')
+        ;
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('addOrSet')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
+                static::assertSame($thumb, $bookmark->getThumbnail());
+            })
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+    }
+
+    /**
+     * Change the password with a wrong existing password
+     */
+    public function testSaveBookmarkFromBookmarklet(): void
+    {
+        $parameters = ['source' => 'bookmarklet'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('<script>self.close();</script>', (string) $result->getBody());
+    }
+
+    /**
+     * Change the password with a wrong existing password
+     */
+    public function testSaveBookmarkWrongToken(): void
+    {
+        $this->container->sessionManager = $this->createMock(SessionManager::class);
+        $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+        $this->container->bookmarkService->expects(static::never())->method('addOrSet');
+        $this->container->bookmarkService->expects(static::never())->method('set');
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->expectException(WrongTokenException::class);
+
+        $this->controller->save($request, $response);
+    }
+
+}
diff --git a/tests/front/controller/admin/PostBookmarkControllerTest.php b/tests/front/controller/admin/PostBookmarkControllerTest.php
deleted file mode 100644 (file)
index 8dcd1b5..0000000
+++ /dev/null
@@ -1,633 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Shaarli\Front\Controller\Admin;
-
-use PHPUnit\Framework\TestCase;
-use Shaarli\Bookmark\Bookmark;
-use Shaarli\Config\ConfigManager;
-use Shaarli\Front\Exception\WrongTokenException;
-use Shaarli\Http\HttpAccess;
-use Shaarli\Security\SessionManager;
-use Shaarli\Thumbnailer;
-use Slim\Http\Request;
-use Slim\Http\Response;
-
-class PostBookmarkControllerTest extends TestCase
-{
-    use FrontAdminControllerMockHelper;
-
-    /** @var PostBookmarkController */
-    protected $controller;
-
-    public function setUp(): void
-    {
-        $this->createContainer();
-
-        $this->container->httpAccess = $this->createMock(HttpAccess::class);
-        $this->controller = new PostBookmarkController($this->container);
-    }
-
-    /**
-     * Test displaying add link page
-     */
-    public function testAddShaare(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $request = $this->createMock(Request::class);
-        $response = new Response();
-
-        $result = $this->controller->addShaare($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('addlink', (string) $result->getBody());
-
-        static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
-    }
-
-    /**
-     * Test displaying bookmark create form
-     * Ensure that every step of the standard workflow works properly.
-     */
-    public function testDisplayCreateFormWithUrl(): void
-    {
-        $this->container->environment = [
-            'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
-        ];
-
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
-        $expectedUrl = str_replace('&utm_ad=pay', '', $url);
-        $remoteTitle = 'Remote Title';
-        $remoteDesc = 'Sometimes the meta description is relevant.';
-        $remoteTags = 'abc def';
-
-        $request = $this->createMock(Request::class);
-        $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
-            return $key === 'post' ? $url : null;
-        });
-        $response = new Response();
-
-        $this->container->httpAccess
-            ->expects(static::once())
-            ->method('getCurlDownloadCallback')
-            ->willReturnCallback(
-                function (&$charset, &$title, &$description, &$tags) use (
-                    $remoteTitle,
-                    $remoteDesc,
-                    $remoteTags
-                ): callable {
-                    return function () use (
-                        &$charset,
-                        &$title,
-                        &$description,
-                        &$tags,
-                        $remoteTitle,
-                        $remoteDesc,
-                        $remoteTags
-                    ): void {
-                        $charset = 'ISO-8859-1';
-                        $title = $remoteTitle;
-                        $description = $remoteDesc;
-                        $tags = $remoteTags;
-                    };
-                }
-            )
-        ;
-        $this->container->httpAccess
-            ->expects(static::once())
-            ->method('getHttpResponse')
-            ->with($expectedUrl, 30, 4194304)
-            ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
-                $callback();
-            })
-        ;
-
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('bookmarksCountPerTag')
-            ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
-        ;
-
-        // Make sure that PluginManager hook is triggered
-        $this->container->pluginManager
-            ->expects(static::at(0))
-            ->method('executeHooks')
-            ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
-                static::assertSame('render_editlink', $hook);
-                static::assertSame($remoteTitle, $data['link']['title']);
-                static::assertSame($remoteDesc, $data['link']['description']);
-
-                return $data;
-            })
-        ;
-
-        $result = $this->controller->displayCreateForm($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-
-        static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
-
-        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
-        static::assertSame($remoteTitle, $assignedVariables['link']['title']);
-        static::assertSame($remoteDesc, $assignedVariables['link']['description']);
-        static::assertSame($remoteTags, $assignedVariables['link']['tags']);
-        static::assertFalse($assignedVariables['link']['private']);
-
-        static::assertTrue($assignedVariables['link_is_new']);
-        static::assertSame($referer, $assignedVariables['http_referer']);
-        static::assertSame($tags, $assignedVariables['tags']);
-        static::assertArrayHasKey('source', $assignedVariables);
-        static::assertArrayHasKey('default_private_links', $assignedVariables);
-    }
-
-    /**
-     * Test displaying bookmark create form
-     * Ensure all available query parameters are handled properly.
-     */
-    public function testDisplayCreateFormWithFullParameters(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $parameters = [
-            'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
-            'title' => 'Provided Title',
-            'description' => 'Provided description.',
-            'tags' => 'abc def',
-            'private' => '1',
-            'source' => 'apps',
-        ];
-        $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
-
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($parameters): ?string {
-            return $parameters[$key] ?? null;
-        });
-        $response = new Response();
-
-        $result = $this->controller->displayCreateForm($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-
-        static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
-
-        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
-        static::assertSame($parameters['title'], $assignedVariables['link']['title']);
-        static::assertSame($parameters['description'], $assignedVariables['link']['description']);
-        static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
-        static::assertTrue($assignedVariables['link']['private']);
-        static::assertTrue($assignedVariables['link_is_new']);
-        static::assertSame($parameters['source'], $assignedVariables['source']);
-    }
-
-    /**
-     * Test displaying bookmark create form
-     * Without any parameter.
-     */
-    public function testDisplayCreateFormEmpty(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $request = $this->createMock(Request::class);
-        $response = new Response();
-
-        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
-        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
-
-        $result = $this->controller->displayCreateForm($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-        static::assertSame('', $assignedVariables['link']['url']);
-        static::assertSame('Note: ', $assignedVariables['link']['title']);
-        static::assertSame('', $assignedVariables['link']['description']);
-        static::assertSame('', $assignedVariables['link']['tags']);
-        static::assertFalse($assignedVariables['link']['private']);
-        static::assertTrue($assignedVariables['link_is_new']);
-    }
-
-    /**
-     * Test displaying bookmark create form
-     * URL not using HTTP protocol: do not try to retrieve the title
-     */
-    public function testDisplayCreateFormNotHttp(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $url = 'magnet://kubuntu.torrent';
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($url): ?string {
-                return $key === 'post' ? $url : null;
-            });
-        $response = new Response();
-
-        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
-        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
-
-        $result = $this->controller->displayCreateForm($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-        static::assertSame($url, $assignedVariables['link']['url']);
-        static::assertTrue($assignedVariables['link_is_new']);
-    }
-
-    /**
-     * Test displaying bookmark create form
-     * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
-     */
-    public function testDisplayCreateFormWithMarkdownEnabled(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $this->container->conf = $this->createMock(ConfigManager::class);
-        $this->container->conf
-            ->expects(static::atLeastOnce())
-            ->method('get')->willReturnCallback(function (string $key): ?string {
-                if ($key === 'formatter') {
-                    return 'markdown';
-                }
-
-                return $key;
-            })
-        ;
-
-        $request = $this->createMock(Request::class);
-        $response = new Response();
-
-        $result = $this->controller->displayCreateForm($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-        static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
-    }
-
-    /**
-     * Test displaying bookmark create form
-     * When an existing URL is submitted, we want to edit the existing link.
-     */
-    public function testDisplayCreateFormWithExistingUrl(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
-        $expectedUrl = str_replace('&utm_ad=pay', '', $url);
-
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($url): ?string {
-                return $key === 'post' ? $url : null;
-            });
-        $response = new Response();
-
-        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
-        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
-
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('findByUrl')
-            ->with($expectedUrl)
-            ->willReturn(
-                (new Bookmark())
-                    ->setId($id = 23)
-                    ->setUrl($expectedUrl)
-                    ->setTitle($title = 'Bookmark Title')
-                    ->setDescription($description = 'Bookmark description.')
-                    ->setTags($tags = ['abc', 'def'])
-                    ->setPrivate(true)
-                    ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
-            )
-        ;
-
-        $result = $this->controller->displayCreateForm($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-
-        static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
-        static::assertFalse($assignedVariables['link_is_new']);
-
-        static::assertSame($id, $assignedVariables['link']['id']);
-        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
-        static::assertSame($title, $assignedVariables['link']['title']);
-        static::assertSame($description, $assignedVariables['link']['description']);
-        static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
-        static::assertTrue($assignedVariables['link']['private']);
-        static::assertSame($createdAt, $assignedVariables['link']['created']);
-    }
-
-    /**
-     * Test displaying bookmark edit form
-     * When an existing ID is provided, ensure that default workflow works properly.
-     */
-    public function testDisplayEditFormDefault(): void
-    {
-        $assignedVariables = [];
-        $this->assignTemplateVars($assignedVariables);
-
-        $id = 11;
-
-        $request = $this->createMock(Request::class);
-        $response = new Response();
-
-        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
-        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
-
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('get')
-            ->with($id)
-            ->willReturn(
-                (new Bookmark())
-                    ->setId($id)
-                    ->setUrl($url = 'http://domain.tld')
-                    ->setTitle($title = 'Bookmark Title')
-                    ->setDescription($description = 'Bookmark description.')
-                    ->setTags($tags = ['abc', 'def'])
-                    ->setPrivate(true)
-                    ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
-            )
-        ;
-
-        $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('editlink', (string) $result->getBody());
-
-        static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
-        static::assertFalse($assignedVariables['link_is_new']);
-
-        static::assertSame($id, $assignedVariables['link']['id']);
-        static::assertSame($url, $assignedVariables['link']['url']);
-        static::assertSame($title, $assignedVariables['link']['title']);
-        static::assertSame($description, $assignedVariables['link']['description']);
-        static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
-        static::assertTrue($assignedVariables['link']['private']);
-        static::assertSame($createdAt, $assignedVariables['link']['created']);
-    }
-
-    /**
-     * Test save a new bookmark
-     */
-    public function testSaveBookmark(): void
-    {
-        $id = 21;
-        $parameters = [
-            'lf_url' => 'http://url.tld/other?part=3#hash',
-            'lf_title' => 'Provided Title',
-            'lf_description' => 'Provided description.',
-            'lf_tags' => 'abc def',
-            'lf_private' => '1',
-            'returnurl' => 'http://shaarli.tld/subfolder/admin/add-shaare'
-        ];
-
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($parameters): ?string {
-                return $parameters[$key] ?? null;
-            })
-        ;
-        $response = new Response();
-
-        $checkBookmark = function (Bookmark $bookmark) use ($parameters) {
-            static::assertSame($parameters['lf_url'], $bookmark->getUrl());
-            static::assertSame($parameters['lf_title'], $bookmark->getTitle());
-            static::assertSame($parameters['lf_description'], $bookmark->getDescription());
-            static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
-            static::assertTrue($bookmark->isPrivate());
-        };
-
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('addOrSet')
-            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
-                static::assertFalse($save);
-
-                $checkBookmark($bookmark);
-
-                $bookmark->setId($id);
-            })
-        ;
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('set')
-            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
-                static::assertTrue($save);
-
-                $checkBookmark($bookmark);
-
-                static::assertSame($id, $bookmark->getId());
-            })
-        ;
-
-        // Make sure that PluginManager hook is triggered
-        $this->container->pluginManager
-            ->expects(static::at(0))
-            ->method('executeHooks')
-            ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
-                static::assertSame('save_link', $hook);
-
-                static::assertSame($id, $data['id']);
-                static::assertSame($parameters['lf_url'], $data['url']);
-                static::assertSame($parameters['lf_title'], $data['title']);
-                static::assertSame($parameters['lf_description'], $data['description']);
-                static::assertSame($parameters['lf_tags'], $data['tags']);
-                static::assertTrue($data['private']);
-
-                return $data;
-            })
-        ;
-
-        $result = $this->controller->save($request, $response);
-
-        static::assertSame(302, $result->getStatusCode());
-        static::assertRegExp('@/subfolder/#[\w\-]{6}@', $result->getHeader('location')[0]);
-    }
-
-
-    /**
-     * Test save an existing bookmark
-     */
-    public function testSaveExistingBookmark(): void
-    {
-        $id = 21;
-        $parameters = [
-            'lf_id' => (string) $id,
-            'lf_url' => 'http://url.tld/other?part=3#hash',
-            'lf_title' => 'Provided Title',
-            'lf_description' => 'Provided description.',
-            'lf_tags' => 'abc def',
-            'lf_private' => '1',
-            'returnurl' => 'http://shaarli.tld/subfolder/?page=2'
-        ];
-
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($parameters): ?string {
-                return $parameters[$key] ?? null;
-            })
-        ;
-        $response = new Response();
-
-        $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
-            static::assertSame($id, $bookmark->getId());
-            static::assertSame($parameters['lf_url'], $bookmark->getUrl());
-            static::assertSame($parameters['lf_title'], $bookmark->getTitle());
-            static::assertSame($parameters['lf_description'], $bookmark->getDescription());
-            static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
-            static::assertTrue($bookmark->isPrivate());
-        };
-
-        $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('get')
-            ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
-        ;
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('addOrSet')
-            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
-                static::assertFalse($save);
-
-                $checkBookmark($bookmark);
-            })
-        ;
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('set')
-            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
-                static::assertTrue($save);
-
-                $checkBookmark($bookmark);
-
-                static::assertSame($id, $bookmark->getId());
-            })
-        ;
-
-        // Make sure that PluginManager hook is triggered
-        $this->container->pluginManager
-            ->expects(static::at(0))
-            ->method('executeHooks')
-            ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
-                static::assertSame('save_link', $hook);
-
-                static::assertSame($id, $data['id']);
-                static::assertSame($parameters['lf_url'], $data['url']);
-                static::assertSame($parameters['lf_title'], $data['title']);
-                static::assertSame($parameters['lf_description'], $data['description']);
-                static::assertSame($parameters['lf_tags'], $data['tags']);
-                static::assertTrue($data['private']);
-
-                return $data;
-            })
-        ;
-
-        $result = $this->controller->save($request, $response);
-
-        static::assertSame(302, $result->getStatusCode());
-        static::assertRegExp('@/subfolder/\?page=2#[\w\-]{6}@', $result->getHeader('location')[0]);
-    }
-
-    /**
-     * Test save a bookmark - try to retrieve the thumbnail
-     */
-    public function testSaveBookmarkWithThumbnail(): void
-    {
-        $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
-
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($parameters): ?string {
-                return $parameters[$key] ?? null;
-            })
-        ;
-        $response = new Response();
-
-        $this->container->conf = $this->createMock(ConfigManager::class);
-        $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
-            return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
-        });
-
-        $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
-        $this->container->thumbnailer
-            ->expects(static::once())
-            ->method('get')
-            ->with($parameters['lf_url'])
-            ->willReturn($thumb = 'http://thumb.url')
-        ;
-
-        $this->container->bookmarkService
-            ->expects(static::once())
-            ->method('addOrSet')
-            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
-                static::assertSame($thumb, $bookmark->getThumbnail());
-            })
-        ;
-
-        $result = $this->controller->save($request, $response);
-
-        static::assertSame(302, $result->getStatusCode());
-    }
-
-    /**
-     * Change the password with a wrong existing password
-     */
-    public function testSaveBookmarkFromBookmarklet(): void
-    {
-        $parameters = ['source' => 'bookmarklet'];
-
-        $request = $this->createMock(Request::class);
-        $request
-            ->method('getParam')
-            ->willReturnCallback(function (string $key) use ($parameters): ?string {
-                return $parameters[$key] ?? null;
-            })
-        ;
-        $response = new Response();
-
-        $result = $this->controller->save($request, $response);
-
-        static::assertSame(200, $result->getStatusCode());
-        static::assertSame('<script>self.close();</script>', (string) $result->getBody());
-    }
-
-    /**
-     * Change the password with a wrong existing password
-     */
-    public function testSaveBookmarkWrongToken(): void
-    {
-        $this->container->sessionManager = $this->createMock(SessionManager::class);
-        $this->container->sessionManager->method('checkToken')->willReturn(false);
-
-        $this->container->bookmarkService->expects(static::never())->method('addOrSet');
-        $this->container->bookmarkService->expects(static::never())->method('set');
-
-        $request = $this->createMock(Request::class);
-        $response = new Response();
-
-        $this->expectException(WrongTokenException::class);
-
-        $this->controller->save($request, $response);
-    }
-}