1
0
Fork 0
mirror of https://github.com/wallabag/wallabag.git synced 2025-10-15 19:42:08 +00:00

Move API stuff in ApiBundle

This commit is contained in:
Jeremy 2015-03-29 10:53:10 +02:00
parent e3c34bfc06
commit 769e19dc4a
25 changed files with 571 additions and 297 deletions

View file

@ -0,0 +1,370 @@
<?php
namespace Wallabag\ApiBundle\Controller;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Service\Extractor;
use Hateoas\Configuration\Route;
use Hateoas\Representation\Factory\PagerfantaFactory;
class WallabagRestController extends Controller
{
/**
* @param Entry $entry
* @param string $tags
*/
private function assignTagsToEntry(Entry $entry, $tags)
{
foreach (explode(',', $tags) as $label) {
$label = trim($label);
$tagEntity = $this
->getDoctrine()
->getRepository('WallabagCoreBundle:Tag')
->findOneByLabel($label);
if (is_null($tagEntity)) {
$tagEntity = new Tag($this->getUser());
$tagEntity->setLabel($label);
}
// only add the tag on the entry if the relation doesn't exist
if (!$entry->getTags()->contains($tagEntity)) {
$entry->addTag($tagEntity);
}
}
}
/**
* Retrieve salt for a giver user.
*
* @ApiDoc(
* parameters={
* {"name"="username", "dataType"="string", "required"=true, "description"="username"}
* }
* )
* @return array
*/
public function getSaltAction($username)
{
$user = $this
->getDoctrine()
->getRepository('WallabagCoreBundle:User')
->findOneByUsername($username);
if (is_null($user)) {
throw $this->createNotFoundException();
}
return array($user->getSalt() ?: null);
}
/**
* Retrieve all entries. It could be filtered by many options.
*
* @ApiDoc(
* parameters={
* {"name"="archive", "dataType"="boolean", "required"=false, "format"="true or false, all entries by default", "description"="filter by archived status."},
* {"name"="star", "dataType"="boolean", "required"=false, "format"="true or false, all entries by default", "description"="filter by starred status."},
* {"name"="sort", "dataType"="string", "required"=false, "format"="'created' or 'updated', default 'created'", "description"="sort entries by date."},
* {"name"="order", "dataType"="string", "required"=false, "format"="'asc' or 'desc', default 'desc'", "description"="order of sort."},
* {"name"="page", "dataType"="integer", "required"=false, "format"="default '1'", "description"="what page you want."},
* {"name"="perPage", "dataType"="integer", "required"=false, "format"="default'30'", "description"="results per page."},
* {"name"="tags", "dataType"="string", "required"=false, "format"="api%2Crest", "description"="a list of tags url encoded. Will returns entries that matches ALL tags."},
* }
* )
* @return Entry
*/
public function getEntriesAction(Request $request)
{
$isArchived = $request->query->get('archive');
$isStarred = $request->query->get('star');
$sort = $request->query->get('sort', 'created');
$order = $request->query->get('order', 'desc');
$page = (int) $request->query->get('page', 1);
$perPage = (int) $request->query->get('perPage', 30);
$tags = $request->query->get('tags', []);
$pager = $this
->getDoctrine()
->getRepository('WallabagCoreBundle:Entry')
->findEntries($this->getUser()->getId(), $isArchived, $isStarred, $sort, $order);
$pager->setCurrentPage($page);
$pager->setMaxPerPage($perPage);
$pagerfantaFactory = new PagerfantaFactory('page', 'perPage');
$paginatedCollection = $pagerfantaFactory->createRepresentation(
$pager,
new Route('api_get_entries', [], $absolute = true)
);
$json = $this->get('serializer')->serialize($paginatedCollection, 'json');
return $this->renderJsonResponse($json);
}
/**
* Retrieve a single entry
*
* @ApiDoc(
* requirements={
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* }
* )
* @return Entry
*/
public function getEntryAction(Entry $entry)
{
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId());
$json = $this->get('serializer')->serialize($entry, 'json');
return $this->renderJsonResponse($json);
}
/**
* Create an entry
*
* @ApiDoc(
* parameters={
* {"name"="url", "dataType"="string", "required"=true, "format"="http://www.test.com/article.html", "description"="Url for the entry."},
* {"name"="title", "dataType"="string", "required"=false, "description"="Optional, we'll get the title from the page."},
* {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
* }
* )
* @return Entry
*/
public function postEntriesAction(Request $request)
{
$url = $request->request->get('url');
$content = Extractor::extract($url);
$entry = new Entry($this->getUser());
$entry->setUrl($url);
$entry->setTitle($request->request->get('title') ?: $content->getTitle());
$entry->setContent($content->getBody());
$tags = $request->request->get('tags', '');
if (!empty($tags)) {
$this->assignTagsToEntry($entry, $tags);
}
$em = $this->getDoctrine()->getManager();
$em->persist($entry);
$em->flush();
$json = $this->get('serializer')->serialize($entry, 'json');
return $this->renderJsonResponse($json);
}
/**
* Change several properties of an entry
*
* @ApiDoc(
* requirements={
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* },
* parameters={
* {"name"="title", "dataType"="string", "required"=false},
* {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
* {"name"="archive", "dataType"="boolean", "required"=false, "format"="true or false", "description"="archived the entry."},
* {"name"="star", "dataType"="boolean", "required"=false, "format"="true or false", "description"="starred the entry."},
* }
* )
* @return Entry
*/
public function patchEntriesAction(Entry $entry, Request $request)
{
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId());
$title = $request->request->get("title");
$isArchived = $request->request->get("archive");
$isStarred = $request->request->get("star");
if (!is_null($title)) {
$entry->setTitle($title);
}
if (!is_null($isArchived)) {
$entry->setArchived($isArchived);
}
if (!is_null($isStarred)) {
$entry->setStarred($isStarred);
}
$tags = $request->request->get('tags', '');
if (!empty($tags)) {
$this->assignTagsToEntry($entry, $tags);
}
$em = $this->getDoctrine()->getManager();
$em->flush();
$json = $this->get('serializer')->serialize($entry, 'json');
return $this->renderJsonResponse($json);
}
/**
* Delete **permanently** an entry
*
* @ApiDoc(
* requirements={
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* }
* )
* @return Entry
*/
public function deleteEntriesAction(Entry $entry)
{
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId());
$em = $this->getDoctrine()->getManager();
$em->remove($entry);
$em->flush();
$json = $this->get('serializer')->serialize($entry, 'json');
return $this->renderJsonResponse($json);
}
/**
* Retrieve all tags for an entry
*
* @ApiDoc(
* requirements={
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* }
* )
*/
public function getEntriesTagsAction(Entry $entry)
{
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId());
$json = $this->get('serializer')->serialize($entry->getTags(), 'json');
return $this->renderJsonResponse($json);
}
/**
* Add one or more tags to an entry
*
* @ApiDoc(
* requirements={
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* },
* parameters={
* {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
* }
* )
*/
public function postEntriesTagsAction(Request $request, Entry $entry)
{
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId());
$tags = $request->request->get('tags', '');
if (!empty($tags)) {
$this->assignTagsToEntry($entry, $tags);
}
$em = $this->getDoctrine()->getManager();
$em->persist($entry);
$em->flush();
$json = $this->get('serializer')->serialize($entry, 'json');
return $this->renderJsonResponse($json);
}
/**
* Permanently remove one tag for an entry
*
* @ApiDoc(
* requirements={
* {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag ID"},
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* }
* )
*/
public function deleteEntriesTagsAction(Entry $entry, Tag $tag)
{
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId());
$entry->removeTag($tag);
$em = $this->getDoctrine()->getManager();
$em->persist($entry);
$em->flush();
$json = $this->get('serializer')->serialize($entry, 'json');
return $this->renderJsonResponse($json);
}
/**
* Retrieve all tags
*
* @ApiDoc()
*/
public function getTagsAction()
{
$json = $this->get('serializer')->serialize($this->getUser()->getTags(), 'json');
return $this->renderJsonResponse($json);
}
/**
* Permanently remove one tag from **every** entry
*
* @ApiDoc(
* requirements={
* {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag"}
* }
* )
*/
public function deleteTagAction(Tag $tag)
{
$this->validateUserAccess($tag->getUser()->getId(), $this->getUser()->getId());
$em = $this->getDoctrine()->getManager();
$em->remove($tag);
$em->flush();
$json = $this->get('serializer')->serialize($tag, 'json');
return $this->renderJsonResponse($json);
}
/**
* Validate that the first id is equal to the second one.
* If not, throw exception. It means a user try to access information from an other user
*
* @param integer $requestUserId User id from the requested source
* @param integer $currentUserId User id from the retrieved source
*/
private function validateUserAccess($requestUserId, $currentUserId)
{
if ($requestUserId != $currentUserId) {
throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$requestUserId.', logged user id: '.$currentUserId);
}
}
/**
* Send a JSON Response.
* We don't use the Symfony JsonRespone, because it takes an array as parameter instead of a JSON string
*
* @param string $json
*
* @return Response
*/
private function renderJsonResponse($json)
{
return new Response($json, 200, array('application/json'));
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Wallabag\ApiBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
*/
class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('wallabag_api');
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Wallabag\ApiBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
class WsseFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.wsse.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.wsse.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'wsse';
}
public function addConfiguration(NodeDefinition $node)
{
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Wallabag\ApiBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class WallabagApiExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
public function getAlias()
{
return 'wallabag_api';
}
}

View file

@ -0,0 +1,4 @@
entries:
type: rest
resource: "WallabagApiBundle:WallabagRest"
name_prefix: api_

View file

@ -0,0 +1,12 @@
services:
wsse.security.authentication.provider:
class: Wallabag\ApiBundle\Security\Authentication\Provider\WsseProvider
public: false
arguments: ['', '%kernel.cache_dir%/security/nonces']
wsse.security.authentication.listener:
class: Wallabag\ApiBundle\Security\Firewall\WsseListener
public: false
tags:
- { name: monolog.logger, channel: wsse }
arguments: ['@security.context', '@security.authentication.manager', '@logger']

View file

@ -0,0 +1,78 @@
<?php
namespace Wallabag\ApiBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Wallabag\ApiBundle\Security\Authentication\Token\WsseUserToken;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
// If cache directory does not exist we create it
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0777, true);
}
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
if (!$user) {
throw new AuthenticationException("Bad credentials. Did you forgot your username?");
}
if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
$authenticatedToken = new WsseUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The WSSE authentication failed.');
}
protected function validateDigest($digest, $nonce, $created, $secret)
{
// Check created time is not in the future
if (strtotime($created) > time()) {
throw new AuthenticationException("Back to the future...");
}
// Expire timestamp after 5 minutes
if (time() - strtotime($created) > 300) {
throw new AuthenticationException("Too late for this timestamp... Watch your watch.");
}
// Validate nonce is unique within 5 minutes
if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
throw new NonceExpiredException('Previously used nonce detected');
}
file_put_contents($this->cacheDir.'/'.$nonce, time());
// Validate Secret
$expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
if ($digest !== $expected) {
throw new AuthenticationException("Bad credentials ! Digest is not as expected.");
}
return $digest === $expected;
}
public function supports(TokenInterface $token)
{
return $token instanceof WsseUserToken;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Wallabag\ApiBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WsseUserToken extends AbstractToken
{
public $created;
public $digest;
public $nonce;
public function __construct(array $roles = array())
{
parent::__construct($roles);
$this->setAuthenticated(count($roles) > 0);
}
public function getCredentials()
{
return '';
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Wallabag\ApiBundle\Security\Firewall;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Wallabag\ApiBundle\Security\Authentication\Token\WsseUserToken;
use Psr\Log\LoggerInterface;
class WsseListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
protected $logger;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, LoggerInterface $logger)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) {
return;
}
$token = new WsseUserToken();
$token->setUser($matches[1]);
$token->digest = $matches[2];
$token->nonce = $matches[3];
$token->created = $matches[4];
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
return;
} catch (AuthenticationException $failed) {
$failedMessage = 'WSSE Login failed for '.$token->getUsername().'. Why ? '.$failed->getMessage();
$this->logger->err($failedMessage);
// Deny authentication with a '403 Forbidden' HTTP response
$response = new Response();
$response->setStatusCode(403);
$response->setContent($failedMessage);
$event->setResponse($response);
return;
}
}
}

View file

@ -0,0 +1,410 @@
<?php
namespace Wallabag\CoreBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class WallabagRestControllerTest extends WebTestCase
{
protected static $salt;
/**
* Grab the salt once and store it to be available for all tests
*/
public static function setUpBeforeClass()
{
$client = self::createClient();
$user = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:User')
->findOneByUsername('admin');
self::$salt = $user->getSalt();
}
/**
* Generate HTTP headers for authenticate user on API
*
* @param string $username
* @param string $password
*
* @return array
*/
private function generateHeaders($username, $password)
{
$encryptedPassword = sha1($password.$username.self::$salt);
$nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
$now = new \DateTime('now', new \DateTimeZone('UTC'));
$created = (string) $now->format('Y-m-d\TH:i:s\Z');
$digest = base64_encode(sha1(base64_decode($nonce).$created.$encryptedPassword, true));
return array(
'HTTP_AUTHORIZATION' => 'Authorization profile="UsernameToken"',
'HTTP_x-wsse' => 'X-WSSE: UsernameToken Username="'.$username.'", PasswordDigest="'.$digest.'", Nonce="'.$nonce.'", Created="'.$created.'"',
);
}
public function testGetSalt()
{
$client = $this->createClient();
$client->request('GET', '/api/salts/admin.json');
$user = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:User')
->findOneByUsername('admin');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey(0, $content);
$this->assertEquals($user->getSalt(), $content[0]);
$client->request('GET', '/api/salts/notfound.json');
$this->assertEquals(404, $client->getResponse()->getStatusCode());
}
public function testWithBadHeaders()
{
$client = $this->createClient();
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneByIsArchived(false);
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$badHeaders = array(
'HTTP_AUTHORIZATION' => 'Authorization profile="UsernameToken"',
'HTTP_x-wsse' => 'X-WSSE: UsernameToken Username="admin", PasswordDigest="Wr0ngDig3st", Nonce="n0Nc3", Created="2015-01-01T13:37:00Z"',
);
$client->request('GET', '/api/entries/'.$entry->getId().'.json', array(), array(), $badHeaders);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function testGetOneEntry()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneBy(array('user' => 1, 'isArchived' => false));
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$client->request('GET', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertEquals($entry->getTitle(), $content['title']);
$this->assertEquals($entry->getUrl(), $content['url']);
$this->assertCount(count($entry->getTags()), $content['tags']);
$this->assertTrue(
$client->getResponse()->headers->contains(
'Content-Type',
'application/json'
)
);
}
public function testGetOneEntryWrongUser()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneBy(array('user' => 2, 'isArchived' => false));
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$client->request('GET', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function testGetEntries()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/entries', array(), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThanOrEqual(1, count($content));
$this->assertNotEmpty($content['_embedded']['items']);
$this->assertGreaterThanOrEqual(1, $content['total']);
$this->assertEquals(1, $content['page']);
$this->assertGreaterThanOrEqual(1, $content['pages']);
$this->assertTrue(
$client->getResponse()->headers->contains(
'Content-Type',
'application/json'
)
);
}
public function testGetStarredEntries()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/entries', array('archive' => 1), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThanOrEqual(1, count($content));
$this->assertEmpty($content['_embedded']['items']);
$this->assertEquals(0, $content['total']);
$this->assertEquals(1, $content['page']);
$this->assertEquals(1, $content['pages']);
$this->assertTrue(
$client->getResponse()->headers->contains(
'Content-Type',
'application/json'
)
);
}
public function testDeleteEntry()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1);
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$client->request('DELETE', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertEquals($entry->getTitle(), $content['title']);
$this->assertEquals($entry->getUrl(), $content['url']);
// We'll try to delete this entry again
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('DELETE', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers);
$this->assertEquals(404, $client->getResponse()->getStatusCode());
}
public function testPostEntry()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('POST', '/api/entries.json', array(
'url' => 'http://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html',
'tags' => 'google',
), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThan(0, $content['id']);
$this->assertEquals('http://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html', $content['url']);
$this->assertEquals(false, $content['is_archived']);
$this->assertEquals(false, $content['is_starred']);
$this->assertCount(1, $content['tags']);
}
public function testPatchEntry()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1);
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
// hydrate the tags relations
$nbTags = count($entry->getTags());
$client->request('PATCH', '/api/entries/'.$entry->getId().'.json', array(
'title' => 'New awesome title',
'tags' => 'new tag '.uniqid(),
'star' => true,
'archive' => false,
), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertEquals($entry->getId(), $content['id']);
$this->assertEquals($entry->getUrl(), $content['url']);
$this->assertEquals('New awesome title', $content['title']);
$this->assertGreaterThan($nbTags, count($content['tags']));
}
public function testGetTagsEntry()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneWithTags(1);
$entry = $entry[0];
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$tags = array();
foreach ($entry->getTags() as $tag) {
$tags[] = array('id' => $tag->getId(), 'label' => $tag->getLabel());
}
$client->request('GET', '/api/entries/'.$entry->getId().'/tags', array(), array(), $headers);
$this->assertEquals(json_encode($tags, JSON_HEX_QUOT), $client->getResponse()->getContent());
}
public function testPostTagsOnEntry()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1);
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$nbTags = count($entry->getTags());
$newTags = 'tag1,tag2,tag3';
$client->request('POST', '/api/entries/'.$entry->getId().'/tags', array('tags' => $newTags), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('tags', $content);
$this->assertEquals($nbTags+3, count($content['tags']));
$entryDB = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->find($entry->getId());
$tagsInDB = array();
foreach ($entryDB->getTags()->toArray() as $tag) {
$tagsInDB[$tag->getId()] = $tag->getLabel();
}
foreach (explode(',', $newTags) as $tag) {
$this->assertContains($tag, $tagsInDB);
}
}
public function testDeleteOneTagEntrie()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1);
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
// hydrate the tags relations
$nbTags = count($entry->getTags());
$tag = $entry->getTags()[0];
$client->request('DELETE', '/api/entries/'.$entry->getId().'/tags/'.$tag->getId().'.json', array(), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('tags', $content);
$this->assertEquals($nbTags-1, count($content['tags']));
}
public function testGetUserTags()
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/tags.json', array(), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThan(0, $content);
$this->assertArrayHasKey('id', $content[0]);
$this->assertArrayHasKey('label', $content[0]);
return end($content);
}
/**
* @depends testGetUserTags
*/
public function testDeleteUserTag($tag)
{
$client = $this->createClient();
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('DELETE', '/api/tags/'.$tag['id'].'.json', array(), array(), $headers);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('label', $content);
$this->assertEquals($tag['label'], $content['label']);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Wallabag\ApiBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Wallabag\ApiBundle\DependencyInjection\Security\Factory\WsseFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class WallabagApiBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new WsseFactory());
}
}