]> git.immae.eu Git - github/wallabag/wallabag.git/commitdiff
Rewrote Pocket Import
authorJeremy Benoist <jeremy.benoist@gmail.com>
Wed, 30 Dec 2015 11:23:51 +0000 (12:23 +0100)
committerJeremy Benoist <jeremy.benoist@gmail.com>
Sat, 2 Jan 2016 22:27:41 +0000 (23:27 +0100)
For the moment, we won't do a queue system, just a plain synchronous import.
We also use ContentProxy to grab content for each article from Pocket.
Error from Pocket are now logged using the logger.
The ImportInterface need to be simple and not related to oAuth (not all import will use that method).

src/Wallabag/CoreBundle/Tools/Utils.php
src/Wallabag/ImportBundle/Controller/ImportController.php
src/Wallabag/ImportBundle/Controller/PocketController.php
src/Wallabag/ImportBundle/Import/ImportInterface.php
src/Wallabag/ImportBundle/Import/PocketImport.php
src/Wallabag/ImportBundle/Resources/config/services.yml
src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig
src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig
src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php

index b501ce6595503a1f0cfda55aea55b3a9ea5a507c..a16baca97e0a09e27e7c35e7ecafdfdf0131416e 100644 (file)
@@ -26,16 +26,6 @@ class Utils
         return str_replace(array('+', '/'), '', $token);
     }
 
-    /**
-     * @param $words
-     *
-     * @return float
-     */
-    public static function convertWordsToMinutes($words)
-    {
-        return floor($words / 200);
-    }
-
     /**
      * For a given text, we calculate reading time for an article
      * based on 200 words per minute.
@@ -46,6 +36,6 @@ class Utils
      */
     public static function getReadingTime($text)
     {
-        return self::convertWordsToMinutes(str_word_count(strip_tags($text)));
+        return floor(str_word_count(strip_tags($text)) / 200);
     }
 }
index 6ebd6a0a936670eb417d84800797e6549dccc310..2a0d6ab5c7b1352b0c01c2c453572e479549bf21 100644 (file)
@@ -4,58 +4,14 @@ namespace Wallabag\ImportBundle\Controller;
 
 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
-use Symfony\Component\Console\Input\ArrayInput;
-use Symfony\Component\Console\Output\NullOutput;
-use Symfony\Component\HttpFoundation\Request;
-use Wallabag\ImportBundle\Command\ImportCommand;
-use Wallabag\ImportBundle\Form\Type\UploadImportType;
 
 class ImportController extends Controller
 {
     /**
      * @Route("/import", name="import")
      */
-    public function importAction(Request $request)
+    public function importAction()
     {
-        $importForm = $this->createForm(new UploadImportType());
-        $importForm->handleRequest($request);
-        $user = $this->getUser();
-
-        if ($importForm->isValid()) {
-            $file = $importForm->get('file')->getData();
-            $name = $user->getId().'.json';
-            $dir = __DIR__.'/../../../../web/uploads/import';
-
-            if (in_array($file->getMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($dir, $name)) {
-                $command = new ImportCommand();
-                $command->setContainer($this->container);
-                $input = new ArrayInput(array('userId' => $user->getId()));
-                $return = $command->run($input, new NullOutput());
-
-                if ($return == 0) {
-                    $this->get('session')->getFlashBag()->add(
-                        'notice',
-                        'Import successful'
-                    );
-                } else {
-                    $this->get('session')->getFlashBag()->add(
-                        'notice',
-                        'Import failed'
-                    );
-                }
-
-                return $this->redirect('/');
-            } else {
-                $this->get('session')->getFlashBag()->add(
-                    'notice',
-                    'Error while processing import. Please verify your import file.'
-                );
-            }
-        }
-
-        return $this->render('WallabagImportBundle:Import:index.html.twig', array(
-            'form' => array(
-                'import' => $importForm->createView(), ),
-        ));
+        return $this->render('WallabagImportBundle:Import:index.html.twig', []);
     }
 }
index 2ab062e70c151ae685effd1ceb35ae2c3aed32f4..61eeba43f9009d47bbe40709630907b9846aaaa4 100644 (file)
@@ -8,35 +8,56 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
 class PocketController extends Controller
 {
     /**
-     * @Route("/import/pocket", name="pocket_import")
+     * @Route("/import/pocket", name="import_pocket")
      */
     public function indexAction()
     {
-        return $this->render('WallabagImportBundle:Pocket:index.html.twig', array());
+        return $this->render('WallabagImportBundle:Pocket:index.html.twig', []);
     }
 
     /**
-     * @Route("/import/pocket/auth", name="pocket_auth")
+     * @Route("/import/pocket/auth", name="import_pocket_auth")
      */
     public function authAction()
     {
-        $pocket = $this->get('wallabag_import.pocket.import');
-        $authUrl = $pocket->oAuthRequest(
-            $this->generateUrl('import', array(), true),
-            $this->generateUrl('pocket_callback', array(), true)
-        );
+        $requestToken = $this->get('wallabag_import.pocket.import')
+            ->getRequestToken($this->generateUrl('import', [], true));
+
+        $this->get('session')->set('import.pocket.code', $requestToken);
 
-        return $this->redirect($authUrl, 301);
+        return $this->redirect(
+            'https://getpocket.com/auth/authorize?request_token='.$requestToken.'&redirect_uri='.$this->generateUrl('import_pocket_callback', [], true),
+            301
+        );
     }
 
     /**
-     * @Route("/import/pocket/callback", name="pocket_callback")
+     * @Route("/import/pocket/callback", name="import_pocket_callback")
      */
     public function callbackAction()
     {
+        $message = 'Import failed, please try again.';
         $pocket = $this->get('wallabag_import.pocket.import');
-        $accessToken = $pocket->oAuthAuthorize();
-        $pocket->import($accessToken);
+
+        // something bad happend on pocket side
+        if (false === $pocket->authorize($this->get('session')->get('import.pocket.code'))) {
+            $this->get('session')->getFlashBag()->add(
+                'notice',
+                $message
+            );
+
+            return $this->redirect($this->generateUrl('import_pocket'));
+        }
+
+        if (true === $pocket->import()) {
+            $summary = $pocket->getSummary();
+            $message = $summary['imported'].' entrie(s) imported, '.$summary['skipped'].' already saved.';
+        }
+
+        $this->get('session')->getFlashBag()->add(
+            'notice',
+            $message
+        );
 
         return $this->redirect($this->generateUrl('homepage'));
     }
index 0f9b3256949b81869eed65d53120afdcf7b43708..8cf238aab7a3e721d38a672d7c77869b8c50c92d 100644 (file)
@@ -2,7 +2,9 @@
 
 namespace Wallabag\ImportBundle\Import;
 
-interface ImportInterface
+use Psr\Log\LoggerAwareInterface;
+
+interface ImportInterface extends LoggerAwareInterface
 {
     /**
      * Name of the import.
@@ -19,27 +21,18 @@ interface ImportInterface
     public function getDescription();
 
     /**
-     * Return the oauth url to authenticate the client.
-     *
-     * @param string $redirectUri Redirect url in case of error
-     * @param string $callbackUri Url when the authentication is complete
-     *
-     * @return string
-     */
-    public function oAuthRequest($redirectUri, $callbackUri);
-
-    /**
-     * Usually called by the previous callback to authorize the client.
-     * Then it return a token that can be used for next requests.
+     * Import content using the user token.
      *
-     * @return string
+     * @return bool
      */
-    public function oAuthAuthorize();
+    public function import();
 
     /**
-     * Import content using the user token.
+     * Return an array with summary info about the import, with keys:
+     *     - skipped
+     *     - imported.
      *
-     * @param string $accessToken User access token
+     * @return array
      */
-    public function import($accessToken);
+    public function getSummary();
 }
index e5c86f07b5d47d9b86258bdba4185bac003dbdfc..1710d9d3dcb2653f0a12824f732a0a2a750117cf 100644 (file)
@@ -2,29 +2,39 @@
 
 namespace Wallabag\ImportBundle\Import;
 
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 use Doctrine\ORM\EntityManager;
 use GuzzleHttp\Client;
-use Symfony\Component\HttpFoundation\Session\Session;
+use GuzzleHttp\Exception\RequestException;
 use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
 use Wallabag\CoreBundle\Entity\Entry;
 use Wallabag\CoreBundle\Entity\Tag;
-use Wallabag\CoreBundle\Tools\Utils;
+use Wallabag\CoreBundle\Helper\ContentProxy;
 
 class PocketImport implements ImportInterface
 {
     private $user;
-    private $session;
     private $em;
+    private $contentProxy;
+    private $logger;
     private $consumerKey;
     private $skippedEntries = 0;
     private $importedEntries = 0;
+    protected $accessToken;
 
-    public function __construct(TokenStorageInterface $tokenStorage, Session $session, EntityManager $em, $consumerKey)
+    public function __construct(TokenStorageInterface $tokenStorage, EntityManager $em, ContentProxy $contentProxy, $consumerKey)
     {
         $this->user = $tokenStorage->getToken()->getUser();
-        $this->session = $session;
         $this->em = $em;
+        $this->contentProxy = $contentProxy;
         $this->consumerKey = $consumerKey;
+        $this->logger = new NullLogger();
+    }
+
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
     }
 
     /**
@@ -44,9 +54,13 @@ class PocketImport implements ImportInterface
     }
 
     /**
-     * {@inheritdoc}
+     * Return the oauth url to authenticate the client.
+     *
+     * @param string $redirectUri Redirect url in case of error
+     *
+     * @return string request_token for callback method
      */
-    public function oAuthRequest($redirectUri, $callbackUri)
+    public function getRequestToken($redirectUri)
     {
         $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request',
             [
@@ -57,44 +71,59 @@ class PocketImport implements ImportInterface
             ]
         );
 
-        $response = $this->client->send($request);
-        $values = $response->json();
+        try {
+            $response = $this->client->send($request);
+        } catch (RequestException $e) {
+            $this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]);
 
-        // store code in session for callback method
-        $this->session->set('pocketCode', $values['code']);
+            return false;
+        }
 
-        return 'https://getpocket.com/auth/authorize?request_token='.$values['code'].'&redirect_uri='.$callbackUri;
+        return $response->json()['code'];
     }
 
     /**
-     * {@inheritdoc}
+     * Usually called by the previous callback to authorize the client.
+     * Then it return a token that can be used for next requests.
+     *
+     * @param string $code request_token from getRequestToken
+     *
+     * @return bool
      */
-    public function oAuthAuthorize()
+    public function authorize($code)
     {
         $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize',
             [
                 'body' => json_encode([
                     'consumer_key' => $this->consumerKey,
-                    'code' => $this->session->get('pocketCode'),
+                    'code' => $code,
                 ]),
             ]
         );
 
-        $response = $this->client->send($request);
+        try {
+            $response = $this->client->send($request);
+        } catch (RequestException $e) {
+            $this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]);
 
-        return $response->json()['access_token'];
+            return false;
+        }
+
+        $this->accessToken = $response->json()['access_token'];
+
+        return true;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function import($accessToken)
+    public function import()
     {
         $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get',
             [
                 'body' => json_encode([
                     'consumer_key' => $this->consumerKey,
-                    'access_token' => $accessToken,
+                    'access_token' => $this->accessToken,
                     'detailType' => 'complete',
                     'state' => 'all',
                     'sort' => 'oldest',
@@ -102,61 +131,45 @@ class PocketImport implements ImportInterface
             ]
         );
 
-        $response = $this->client->send($request);
+        try {
+            $response = $this->client->send($request);
+        } catch (RequestException $e) {
+            $this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]);
+
+            return false;
+        }
+
         $entries = $response->json();
 
         $this->parsePocketEntries($entries['list']);
 
-        $this->session->getFlashBag()->add(
-            'notice',
-            $this->importedEntries.' entries imported, '.$this->skippedEntries.' already saved.'
-        );
+        return true;
     }
 
     /**
-     * Set the Guzzle client.
-     *
-     * @param Client $client
+     * {@inheritdoc}
      */
-    public function setClient(Client $client)
+    public function getSummary()
     {
-        $this->client = $client;
+        return [
+            'skipped' => $this->skippedEntries,
+            'imported' => $this->importedEntries,
+        ];
     }
 
     /**
-     * Returns the good title for current entry.
-     *
-     * @param $pocketEntry
+     * Set the Guzzle client.
      *
-     * @return string
+     * @param Client $client
      */
-    private function guessTitle($pocketEntry)
+    public function setClient(Client $client)
     {
-        if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') {
-            return $pocketEntry['resolved_title'];
-        } elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
-            return $pocketEntry['given_title'];
-        }
-
-        return 'Untitled';
+        $this->client = $client;
     }
 
     /**
-     * Returns the good URL for current entry.
-     *
-     * @param $pocketEntry
-     *
-     * @return string
+     * @todo move that in a more global place
      */
-    private function guessURL($pocketEntry)
-    {
-        if (isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '') {
-            return $pocketEntry['resolved_url'];
-        }
-
-        return $pocketEntry['given_url'];
-    }
-
     private function assignTagsToEntry(Entry $entry, $tags)
     {
         foreach ($tags as $tag) {
@@ -177,13 +190,16 @@ class PocketImport implements ImportInterface
     }
 
     /**
+     * @see https://getpocket.com/developer/docs/v3/retrieve
+     *
      * @param $entries
      */
     private function parsePocketEntries($entries)
     {
         foreach ($entries as $pocketEntry) {
             $entry = new Entry($this->user);
-            $url = $this->guessURL($pocketEntry);
+
+            $url = isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '' ? $pocketEntry['resolved_url'] : $pocketEntry['given_url'];
 
             $existingEntry = $this->em
                 ->getRepository('WallabagCoreBundle:Entry')
@@ -194,31 +210,33 @@ class PocketImport implements ImportInterface
                 continue;
             }
 
-            $entry->setUrl($url);
-            $entry->setDomainName(parse_url($url, PHP_URL_HOST));
+            $entry = $this->contentProxy->updateEntry($entry, $url);
 
+            // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted
             if ($pocketEntry['status'] == 1) {
                 $entry->setArchived(true);
             }
+
+            // 0 or 1 - 1 If the item is favorited
             if ($pocketEntry['favorite'] == 1) {
                 $entry->setStarred(true);
             }
 
-            $entry->setTitle($this->guessTitle($pocketEntry));
-
-            if (isset($pocketEntry['excerpt'])) {
-                $entry->setContent($pocketEntry['excerpt']);
+            $title = 'Untitled';
+            if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') {
+                $title = $pocketEntry['resolved_title'];
+            } elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
+                $title = $pocketEntry['given_title'];
             }
 
-            if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0) {
-                $entry->setPreviewPicture($pocketEntry['image']['src']);
-            }
+            $entry->setTitle($title);
 
-            if (isset($pocketEntry['word_count'])) {
-                $entry->setReadingTime(Utils::convertWordsToMinutes($pocketEntry['word_count']));
+            // 0, 1, or 2 - 1 if the item has images in it - 2 if the item is an image
+            if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0 && isset($pocketEntry['images'][1])) {
+                $entry->setPreviewPicture($pocketEntry['images'][1]['src']);
             }
 
-            if (!empty($pocketEntry['tags'])) {
+            if (isset($pocketEntry['tags']) && !empty($pocketEntry['tags'])) {
                 $this->assignTagsToEntry($entry, $pocketEntry['tags']);
             }
 
index ab516ca5ee76e1916067b946e2d87bbf006a5e83..f421821ca957a29ea2d46d66a2b94f67c1da862a 100644 (file)
@@ -1,14 +1,4 @@
 services:
-    wallabag_import.pocket.import:
-        class: Wallabag\ImportBundle\Import\PocketImport
-        arguments:
-            - "@security.token_storage"
-            - "@session"
-            - "@doctrine.orm.entity_manager"
-            - %pocket_consumer_key%
-        calls:
-            - [ setClient, [ "@wallabag_import.pocket.client" ] ]
-
     wallabag_import.pocket.client:
         class: GuzzleHttp\Client
         arguments:
@@ -17,3 +7,14 @@ services:
                     headers:
                         content-type: "application/json"
                         X-Accept: "application/json"
+
+    wallabag_import.pocket.import:
+        class: Wallabag\ImportBundle\Import\PocketImport
+        arguments:
+            - "@security.token_storage"
+            - "@doctrine.orm.entity_manager"
+            - "@wallabag_core.content_proxy"
+            - %pocket_consumer_key%
+        calls:
+            - [ setClient, [ "@wallabag_import.pocket.client" ] ]
+            - [ setLogger, [ "@logger" ]]
index ee759a526623f38d72061e943c93dba66e8aad20..b068283af13b047807a319103ef75e8e969a5648 100644 (file)
@@ -8,35 +8,9 @@
         <div class="card-panel settings">
             {% trans %}Welcome on wallabag importer. Please select your previous service that you want to migrate.{% endtrans %}
             <ul>
-                <li><a href="{{ path('pocket_import') }}">Pocket</a></li>
+                <li><a href="{{ path('import_pocket') }}">Pocket</a></li>
             </ul>
         </div>
     </div>
 </div>
-
-
-<div class="row">
-    <div class="col s12">
-        <div class="card-panel settings">
-            <div class="row">
-                <div class="col s12">
-                    <form action="{{ path('import') }}" method="post" {{ form_enctype(form.import) }}>
-                        {{ form_errors(form.import) }}
-                        <div class="row">
-                            <div class="input-field col s12">
-                                <p>{% trans %}Please select your wallabag export and click on the below button to upload and import it.{% endtrans %}</p>
-                                {{ form_errors(form.import.file) }}
-                                {{ form_widget(form.import.file) }}
-                            </div>
-                        </div>
-                        <div class="hidden">{{ form_rest(form.import) }}</div>
-                        <button class="btn waves-effect waves-light" type="submit" name="action">
-                            {% trans %}Upload file{% endtrans %}
-                        </button>
-                    </form>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
 {% endblock %}
index df64e472c06bd01e5b80ef63f67657889f1f7102..940fe4cc61848c00725ccfeb1d9fcbe1f2a6ed5a 100644 (file)
@@ -7,7 +7,7 @@
     <div class="col s12">
         <div class="card-panel settings">
             {% trans %}You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com.{% endtrans %}
-            <form method="post" action="{{ path('pocket_auth') }}">
+            <form method="post" action="{{ path('import_pocket_auth') }}">
                 <input type="submit" value="Connect to Pocket and import data" />
             </form>
         </div>
index 4c718fa3a30de9d55b3f4903bfa818bf5faa5b41..cf706fa9490bb53bad0ea2585a142557b41d42e8 100644 (file)
@@ -4,19 +4,28 @@ namespace Wallabag\ImportBundle\Tests\Import;
 
 use Wallabag\UserBundle\Entity\User;
 use Wallabag\ImportBundle\Import\PocketImport;
-use Symfony\Component\HttpFoundation\Session\Session;
-use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
 use GuzzleHttp\Client;
 use GuzzleHttp\Subscriber\Mock;
 use GuzzleHttp\Message\Response;
 use GuzzleHttp\Stream\Stream;
+use Monolog\Logger;
+use Monolog\Handler\TestHandler;
+
+class PocketImportMock extends PocketImport
+{
+    public function getAccessToken()
+    {
+        return $this->accessToken;
+    }
+}
 
 class PocketImportTest extends \PHPUnit_Framework_TestCase
 {
     protected $token;
     protected $user;
-    protected $session;
     protected $em;
+    protected $contentProxy;
+    protected $logHandler;
 
     private function getPocketImport($consumerKey = 'ConsumerKey')
     {
@@ -30,6 +39,10 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
             ->disableOriginalConstructor()
             ->getMock();
 
+        $this->contentProxy = $this->getMockBuilder('Wallabag\CoreBundle\Helper\ContentProxy')
+            ->disableOriginalConstructor()
+            ->getMock();
+
         $token->expects($this->once())
             ->method('getUser')
             ->willReturn($this->user);
@@ -38,18 +51,22 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
             ->method('getToken')
             ->willReturn($token);
 
-        $this->session = new Session(new MockArraySessionStorage());
-
         $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
             ->disableOriginalConstructor()
             ->getMock();
 
-        return new PocketImport(
+        $pocket = new PocketImportMock(
             $this->tokenStorage,
-            $this->session,
             $this->em,
+            $this->contentProxy,
             $consumerKey
         );
+
+        $this->logHandler = new TestHandler();
+        $logger = new Logger('test', array($this->logHandler));
+        $pocket->setLogger($logger);
+
+        return $pocket;
     }
 
     public function testInit()
@@ -65,7 +82,7 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
         $client = new Client();
 
         $mock = new Mock([
-            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar']))),
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))),
         ]);
 
         $client->getEmitter()->attach($mock);
@@ -73,10 +90,31 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
         $pocketImport = $this->getPocketImport();
         $pocketImport->setClient($client);
 
-        $url = $pocketImport->oAuthRequest('http://0.0.0.0./redirect', 'http://0.0.0.0./callback');
+        $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
 
-        $this->assertEquals('https://getpocket.com/auth/authorize?request_token=wunderbar&redirect_uri=http://0.0.0.0./callback', $url);
-        $this->assertEquals('wunderbar', $this->session->get('pocketCode'));
+        $this->assertEquals('wunderbar_code', $code);
+    }
+
+    public function testOAuthRequestBadResponse()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(403),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+
+        $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
+
+        $this->assertFalse($code);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('PocketImport: Failed to request token', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
     }
 
     public function testOAuthAuthorize()
@@ -84,7 +122,7 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
         $client = new Client();
 
         $mock = new Mock([
-            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar']))),
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
         ]);
 
         $client->getEmitter()->attach($mock);
@@ -92,26 +130,182 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
         $pocketImport = $this->getPocketImport();
         $pocketImport->setClient($client);
 
-        $accessToken = $pocketImport->oAuthAuthorize();
+        $res = $pocketImport->authorize('wunderbar_code');
 
-        $this->assertEquals('wunderbar', $accessToken);
+        $this->assertTrue($res);
+        $this->assertEquals('wunderbar_token', $pocketImport->getAccessToken());
     }
 
+    public function testOAuthAuthorizeBadResponse()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(403),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+
+        $res = $pocketImport->authorize('wunderbar_code');
+
+        $this->assertFalse($res);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('PocketImport: Failed to authorize client', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
+    }
+
+    /**
+     * Will sample results from https://getpocket.com/developer/docs/v3/retrieve.
+     */
     public function testImport()
     {
         $client = new Client();
 
         $mock = new Mock([
-            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['list' => []]))),
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory('
+                {
+                    "status": 1,
+                    "list": {
+                        "229279689": {
+                            "item_id": "229279689",
+                            "resolved_id": "229279689",
+                            "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
+                            "favorite": "1",
+                            "status": "1",
+                            "resolved_title": "The Massive Ryder Cup Preview",
+                            "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.",
+                            "is_article": "1",
+                            "has_video": "1",
+                            "has_image": "1",
+                            "word_count": "3197",
+                            "images": {
+                                "1": {
+                                    "item_id": "229279689",
+                                    "image_id": "1",
+                                    "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360",
+                                    "width": "0",
+                                    "height": "0",
+                                    "credit": "Jamie Squire/Getty Images",
+                                    "caption": ""
+                                }
+                            },
+                            "videos": {
+                                "1": {
+                                    "item_id": "229279689",
+                                    "video_id": "1",
+                                    "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0",
+                                    "width": "420",
+                                    "height": "315",
+                                    "type": "1",
+                                    "vid": "Er34PbFkVGk"
+                                }
+                            },
+                            "tags": {
+                                "grantland": {
+                                    "item_id": "1147652870",
+                                    "tag": "grantland"
+                                },
+                                "Ryder Cup": {
+                                    "item_id": "1147652870",
+                                    "tag": "Ryder Cup"
+                                }
+                            }
+                        },
+                        "229279690": {
+                            "item_id": "229279689",
+                            "resolved_id": "229279689",
+                            "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
+                            "favorite": "1",
+                            "status": "1",
+                            "resolved_title": "The Massive Ryder Cup Preview",
+                            "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.",
+                            "is_article": "1",
+                            "has_video": "0",
+                            "has_image": "0",
+                            "word_count": "3197"
+                        }
+                    }
+                }
+            ')),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+
+        $entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $entryRepo->expects($this->exactly(2))
+            ->method('existByUrlAndUserId')
+            ->will($this->onConsecutiveCalls(false, true));
+
+        $tag = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Tag')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $tagRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $tagRepo->expects($this->exactly(2))
+            ->method('findOneByLabelAndUserId')
+            ->will($this->onConsecutiveCalls(false, $tag));
+
+        $this->em
+            ->expects($this->any())
+            ->method('getRepository')
+            ->will($this->onConsecutiveCalls($entryRepo, $tagRepo, $tagRepo, $entryRepo));
+
+        $entry = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Entry')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->contentProxy
+            ->expects($this->once())
+            ->method('updateEntry')
+            ->willReturn($entry);
+
+        $pocketImport->setClient($client);
+        $pocketImport->authorize('wunderbar_code');
+
+        $res = $pocketImport->import();
+
+        $this->assertTrue($res);
+        $this->assertEquals(['skipped' => 1, 'imported' => 1], $pocketImport->getSummary());
+    }
+
+    public function testImportBadResponse()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
+            new Response(403),
         ]);
 
         $client->getEmitter()->attach($mock);
 
         $pocketImport = $this->getPocketImport();
         $pocketImport->setClient($client);
+        $pocketImport->authorize('wunderbar_code');
+
+        $res = $pocketImport->import();
 
-        $pocketImport->import('wunderbar');
+        $this->assertFalse($res);
 
-        $this->assertEquals('0 entries imported, 0 already saved.', $this->session->getFlashBag()->get('notice')[0]);
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('PocketImport: Failed to import', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
     }
 }