Optimized REST API and tests

This commit is contained in:
Kevin Frantz 2019-02-19 19:36:02 +01:00
parent cbb1c76640
commit 1beae0cdc0
19 changed files with 183 additions and 132 deletions

View File

@ -1,6 +0,0 @@
# REST API Controller
The controllers use the [FOSRestBundle](https://symfony.com/doc/master/bundles/FOSRestBundle/index.html).
## Workflow
The abstract workflow of the REST API controllers for a singular entity looks like this:
![REST API Workflow](.meta/workflow.svg)
Special actions, e.g. lists are not shown in this diagram. This diagram also shows downstream procedures, to remember to implement them. Feel free to remove them from the diagram, as soon as they are documented somewhere else.

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -1,6 +1,6 @@
<?php
namespace Infinito\Controller\API\Meta;
namespace Infinito\Controller\API\Rest;
use Infinito\Controller\API\AbstractAPIController;
use Symfony\Component\HttpFoundation\Request;
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
*
* @todo Implement!
*/
class RightApiController extends AbstractAPIController
final class HeredityController extends AbstractAPIController
{
public function read(Request $request, $identifier): Response
{

View File

@ -1,6 +1,6 @@
<?php
namespace Infinito\Controller\API\Meta;
namespace Infinito\Controller\API\Rest;
use Infinito\Controller\API\AbstractAPIController;
use Symfony\Component\HttpFoundation\Request;
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
*
* @todo Implement!
*/
class LawApiController extends AbstractAPIController
final class LawController extends AbstractAPIController
{
public function read(Request $request, $identifier): Response
{

View File

@ -1,6 +1,6 @@
<?php
namespace Infinito\Controller\API\Meta;
namespace Infinito\Controller\API\Rest;
use Infinito\Controller\API\AbstractAPIController;
use Symfony\Component\HttpFoundation\Request;
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
*
* @todo Implement!
*/
class MemberApiController extends AbstractAPIController
final class MemberController extends AbstractAPIController
{
public function read(Request $request, $identifier): Response
{

View File

@ -0,0 +1,24 @@
# REST API Controller
The controllers use the [FOSRestBundle](https://symfony.com/doc/master/bundles/FOSRestBundle/index.html).
## Url Scheme
The scheme for the rest api is the following:
| Url | Methods | Function |
|---|---|---|
| api/rest/{entity}.{format} | HEAD | Returns the create information for a specific entity |
| api/rest/{entity}.{format} | POST | Creates a specific entity and returns it. |
| api/rest/{entity}/{uri}.{format} | GET | Returns a specific entity. Including all actions |
| api/rest/{entity}/{uri}/{action}.{format} | GET | Returns the result for an action of an specific entity. |
| api/rest/{entity}/{uri}.{format} | PUT, PATCH | Updates a specific entity and returns it. |
| api/rest/{entity}/{uri}.{format} | DELETE | Deletes a specific entity|
If an concrete entity doesn't implement an method it should redirect to the connected entity which is responsible for this method.
In the future it would make sense to implement [more methods](https://de.wikipedia.org/wiki/Representational_State_Transfer#Umsetzung).
The standard format of an entity MUST be JSON.
## Workflow
The abstract workflow of the REST API controllers for a singular entity looks like this:
![REST API Workflow](.meta/workflow.svg)
Special actions, e.g. lists are not shown in this diagram. This diagram also shows downstream procedures, to remember to implement them. Feel free to remove them from the diagram, as soon as they are documented somewhere else.

View File

@ -1,6 +1,6 @@
<?php
namespace Infinito\Controller\API\Meta;
namespace Infinito\Controller\API\Rest;
use Infinito\Controller\API\AbstractAPIController;
use Symfony\Component\HttpFoundation\Request;
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
*
* @todo Implement!
*/
class HeredityApiController extends AbstractAPIController
final class RightController extends AbstractAPIController
{
public function read(Request $request, $identifier): Response
{

View File

@ -1,6 +1,6 @@
<?php
namespace Infinito\Controller\API\Source;
namespace Infinito\Controller\API\Rest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -18,20 +18,19 @@ use Infinito\DBAL\Types\Meta\Right\LayerType;
* @see https://symfony.com/blog/new-in-symfony-4-1-internationalized-routing
* @Route(
* {
* "en":"/source/{identity}.{_format}",
* "de":"/quelle/{identity}.{_format}",
* "eo":"/fonto/{identity}.{_format}",
* "es":"/fontanar/{identity}.{_format}",
* "nl":"/bron/{identity}.{_format}"
* "en":"/api/rest/source/{identity}.{_format}",
* "de":"/api/rest/quelle/{identity}.{_format}",
* "eo":"/api/rest/fonto/{identity}.{_format}",
* "es":"/api/rest/fontanar/{identity}.{_format}",
* "nl":"/api/rest/bron/{identity}.{_format}"
* },
* defaults={
* "identity"="",
* "_format"="json"
* } ,
* name="source_"
* }
* )
*/
class SourceApiController extends AbstractAPIController
final class SourceController extends AbstractAPIController
{
/**
* @Route(

View File

@ -12,7 +12,7 @@ use Infinito\DBAL\Types\Meta\Right\CRUDType;
final class ActionType extends CRUDType
{
/**
* @var string
* @var string this action executes an entity
*/
const EXECUTE = 'execute';

View File

@ -10,20 +10,22 @@ use Symfony\Component\HttpFoundation\Request;
*/
final class ActionHttpMethodMap extends AbstractMap implements ActionHttpMethodMapInterface
{
/**
* @var array
*/
const ACTION_HTTP_METHOD_MAP = [
ActionType::READ => [
Request::METHOD_GET,
],
ActionType::CREATE => [
Request::METHOD_POST,
Request::METHOD_GET,
Request::METHOD_HEAD,
],
ActionType::UPDATE => [
Request::METHOD_PUT,
Request::METHOD_GET,
Request::METHOD_PATCH,
],
ActionType::DELETE => [
Request::METHOD_GET,
Request::METHOD_DELETE,
],
ActionType::EXECUTE => [

View File

@ -13,11 +13,11 @@ use Infinito\Entity\Source\AbstractSource;
use Infinito\Exception\NotSetException;
use Infinito\Repository\RepositoryInterface;
use Infinito\Entity\Source\SourceInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Infinito\Attribut\ClassAttribut;
use Infinito\Exception\AllreadyDefinedException;
use Infinito\Domain\RequestManagement\Right\RequestedRightInterface;
use Infinito\Domain\RepositoryManagement\LayerRepositoryFactoryService;
use Infinito\Exception\EntityNotFoundHttpException;
/**
* @author kevinfrantz
@ -51,12 +51,12 @@ class RequestedEntity extends AbstractEntity implements RequestedEntityInterface
/**
* @param EntityInterface|null $entity
*
* @throws NotFoundHttpException
* @throws EntityNotFoundHttpException
*/
private function validateLoadedEntity(?EntityInterface $entity): void
{
if (!$entity) {
throw new NotFoundHttpException('Entity with {id:"'.$this->id.'",slug:"'.$this->slug.'"} not found');
throw new EntityNotFoundHttpException('Entity with {id:"'.$this->id.'",slug:"'.$this->slug.'"} not found');
}
}
@ -97,7 +97,7 @@ class RequestedEntity extends AbstractEntity implements RequestedEntityInterface
}
/**
* @throws NotFoundHttpException
* @throws NotSetException
*/
private function validateLayerRepositoryFactoryService(): void
{

View File

@ -0,0 +1,12 @@
<?php
namespace Infinito\Exception;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @author kevinfrantz
*/
final class EntityNotFoundHttpException extends NotFoundHttpException
{
}

View File

@ -0,0 +1,4 @@
# API
This folder contains the different API controllers.
Right now just an REST API is implemented.
In the future it would also make sense to implement an [GraphQL](https://graphql.org/) API.

View File

@ -0,0 +1,20 @@
<?php
namespace tests\Integration\Controller\API\Rest;
use PHPUnit\Framework\TestCase;
use Infinito\DBAL\Types\Meta\Right\LayerType;
/**
* @author kevinfrantz
*/
class ControllerLayerIntegrationTest extends TestCase
{
public function testThatControllerForEachLayerExist(): void
{
foreach (LayerType::getChoices() as $layer) {
$className = 'Infinito\\Controller\\API\\Rest\\'.ucfirst($layer).'Controller';
$this->assertTrue(class_exists($className));
}
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Tests\Integration\Controller;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Infinito\DBAL\Types\Meta\Right\LayerType;
use Infinito\DBAL\Types\RESTResponseType;
use Symfony\Component\HttpFoundation\Request;
use Infinito\Domain\LayerManagement\LayerActionMap;
use Infinito\DBAL\Types\ActionType;
use Infinito\Domain\MapManagement\ActionHttpMethodMap;
use Symfony\Component\HttpFoundation\Response;
/**
* @author kevinfrantz
*
* @todo Implement more tests for success etc.
*/
class RestRoutesReachableIntegrationTest extends KernelTestCase
{
/**
* {@inheritdoc}
*
* @see \PHPUnit\Framework\TestCase::setUp()
*/
public function setUp(): void
{
self::bootKernel();
}
public function testAllRoutePossibilities(): void
{
foreach ([
'12314123',
'testslug',
] as $uri) {
foreach (RESTResponseType::getChoices() as $format) {
foreach (LayerType::getChoices() as $layer) {
$actions = LayerActionMap::getActions($layer);
foreach ($actions as $action) {
foreach (ActionHttpMethodMap::getHttpMethods($action) as $method) {
$baseUrl = "api/rest/$layer";
switch ($action) {
case ActionType::CREATE:
$url = "$baseUrl.$format";
$this->routeAssert($url, $method);
break;
case ActionType::EXECUTE:
$url = "$baseUrl/$uri/action/execute.$format";
$this->routeAssert($url, $method);
break;
default:
$url = "$baseUrl/$uri.$format";
$this->routeAssert($url, $method);
}
}
}
}
}
}
}
/**
* @param string $url
* @param string $method
*/
private function routeAssert(string $url, string $method): void
{
$request = new Request([], [], [], [], [], [
'REQUEST_URI' => $url,
]);
$request->setMethod($method);
$response = static::$kernel->handle($request);
$this->assertTrue($this->isResponseValid($response), "Route $url with Method $method sends an 404 response and doesn't throw an EntityNotFoundHttpException!");
}
/**
* @param Response $response
*
* @return bool
*/
private function isResponseValid(Response $response): bool
{
$is404 = 404 === $response->getStatusCode();
$isEntityNotFoundHttpException = strpos($response->getContent(), 'EntityNotFoundHttpException');
return !$is404 || $isEntityNotFoundHttpException;
}
}

View File

@ -1,98 +0,0 @@
<?php
namespace Tests\Integration\Controller;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Infinito\DBAL\Types\LanguageType;
use Infinito\DBAL\Types\Meta\Right\LayerType;
use Infinito\DBAL\Types\RESTResponseType;
use Symfony\Component\HttpFoundation\Request;
/**
* @author kevinfrantz
*
* @todo Implement more tests for success etc.
*/
class RoutesReachableIntegrationTest extends KernelTestCase
{
/**
* {@inheritdoc}
*
* @see \PHPUnit\Framework\TestCase::setUp()
*/
public function setUp(): void
{
self::bootKernel();
}
public function testAllRoutePossibilities()
{
foreach (LayerType::getChoices() as $layer) {
$this->controller($layer);
}
}
/**
* @param string $entity
*/
private function controller(string $entity): void
{
$this->language($entity, Request::METHOD_GET);
$this->language($entity, Request::METHOD_POST);
$this->language($entity.'s', Request::METHOD_GET);
$this->slugAndId($entity, Request::METHOD_PUT);
$this->slugAndId($entity, Request::METHOD_GET);
$this->slugAndId($entity, Request::METHOD_DELETE);
}
/**
* @param string $route
* @param string $method
*/
private function slugAndId(string $route, string $method): void
{
$this->language("$route/12345", $method);
$this->language("$route/asdfg", $method);
}
/**
* @todo Implement routing without i18l part!
*
* @param string $entity
* @param string $method
*/
private function language(string $entity, string $method): void
{
//$this->type('api/'.$entity, $method);
foreach (LanguageType::getChoices() as $language) {
$this->type("$language/api/$entity", $method);
$this->type("$language/html/$entity", $method);
}
}
/**
* @param string $route
* @param string $method
*/
private function type(string $route, string $method): void
{
$this->routeAssert($route, $method);
foreach (RESTResponseType::getChoices() as $restResponseType => $value) {
$this->routeAssert("$route.$restResponseType", $method);
}
}
/**
* @param string $url
* @param string $method
*/
private function routeAssert(string $url, string $method): void
{
$request = new Request([], [], [], [], [], [
'REQUEST_URI' => $url,
]);
$request->setMethod($method);
$response = static::$kernel->handle($request);
$this->assertNotEquals(404, $response->getStatusCode(), "Route $url with Method $method sends an 404 response!");
}
}

View File

@ -10,6 +10,11 @@ use Symfony\Component\HttpFoundation\Request;
*/
class ImprintFixtureSourceTest extends KernelTestCase
{
/**
* {@inheritdoc}
*
* @see \PHPUnit\Framework\TestCase::setUp()
*/
public function setUp(): void
{
self::bootKernel();
@ -18,7 +23,7 @@ class ImprintFixtureSourceTest extends KernelTestCase
public function testImprintSourceReachable(): void
{
$request = new Request([], [], [], [], [], [
'REQUEST_URI' => 'source/imprint.html',
'REQUEST_URI' => 'api/rest/source/imprint.html',
]);
$request->setMethod(Request::METHOD_GET);
$response = static::$kernel->handle($request);

View File

@ -25,7 +25,7 @@ class ActionHttpMethodTest extends TestCase
public function testCreateActionTrue(): void
{
$subset = [Request::METHOD_GET, Request::METHOD_POST];
$subset = [Request::METHOD_POST, Request::METHOD_HEAD];
$action = ActionType::CREATE;
$haystack = ActionHttpMethodMap::getHttpMethods($action);
$this->assertSubsetInArray($subset, $haystack, true);
@ -34,7 +34,7 @@ class ActionHttpMethodTest extends TestCase
public function testCreateActionFalse(): void
{
$subset = [Request::METHOD_GET, Request::METHOD_POST];
$subset = [Request::METHOD_POST, Request::METHOD_HEAD];
$action = 'wrong value';
$haystack = ActionHttpMethodMap::getHttpMethods($action);
$this->assertSubsetInArray($subset, $haystack, false);