API: Split to multiple controllers, removed / from routes

This commit is contained in:
Igor Scheller 2023-07-22 20:35:45 +02:00 committed by Michael Weimann
parent ca0a69b17d
commit b5d94971bc
9 changed files with 228 additions and 146 deletions

View File

@ -111,24 +111,24 @@ $route->addGroup(
$route->addGroup( $route->addGroup(
'/api', '/api',
function (RouteCollector $route): void { function (RouteCollector $route): void {
$route->get('/', 'ApiController@index'); $route->get('', 'Api\IndexController@index');
$route->addGroup( $route->addGroup(
'/v0-beta', '/v0-beta',
function (RouteCollector $route): void { function (RouteCollector $route): void {
$route->addRoute(['OPTIONS'], '[/{resource:.+}]', 'ApiController@options'); $route->addRoute(['OPTIONS'], '[/{resource:.+}]', 'Api\IndexController@options');
$route->get('/', 'ApiController@indexV0'); $route->get('', 'Api\IndexController@indexV0');
$route->get('/news', 'ApiController@news'); $route->get('/news', 'Api\NewsController@index');
$route->addRoute( $route->addRoute(
['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'],
'[/{resource:.+}]', '/[{resource:.+}]',
'ApiController@notImplemented' 'Api\IndexController@notImplemented'
); );
} }
); );
$route->get('[/{resource:.+}]', 'ApiController@notImplemented'); $route->get('/[{resource:.+}]', 'Api\IndexController@notImplemented');
} }
); );

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Controllers\Api;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Http\Response;
abstract class ApiController extends BaseController
{
public array $permissions = [
'api',
];
public function __construct(protected Response $response)
{
$this->response = $this->response
->withHeader('content-type', 'application/json')
// Using * here to "skip" all other headers on browser requests
->withHeader('access-control-allow-origin', '*');
}
}

View File

@ -2,24 +2,17 @@
declare(strict_types=1); declare(strict_types=1);
namespace Engelsystem\Controllers; namespace Engelsystem\Controllers\Api;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Models\News;
class ApiController extends BaseController class IndexController extends ApiController
{ {
public array $permissions = [ public array $permissions = [
'api', 'index' => 'api',
'indexV0' => 'api',
]; ];
public function __construct(protected Response $response)
{
$this->response = $this->response
->withHeader('content-type', 'application/json')
->withHeader('Access-Control-Allow-Origin', '*');
}
public function index(): Response public function index(): Response
{ {
return $this->response return $this->response
@ -44,10 +37,11 @@ class ApiController extends BaseController
public function options(): Response public function options(): Response
{ {
// Respond to browser preflight options requests
return $this->response return $this->response
->setStatusCode(204) ->setStatusCode(204)
->withHeader('Allow', 'OPTIONS, HEAD, GET') ->withHeader('allow', 'OPTIONS, HEAD, GET')
->withHeader('Access-Control-Allow-Headers', 'Authorization'); ->withHeader('access-control-allow-headers', 'Authorization');
} }
public function notImplemented(): Response public function notImplemented(): Response
@ -56,16 +50,4 @@ class ApiController extends BaseController
->setStatusCode(501) ->setStatusCode(501)
->withContent(json_encode(['error' => 'Not implemented'])); ->withContent(json_encode(['error' => 'Not implemented']));
} }
public function news(): Response
{
$news = News::query()
->orderByDesc('updated_at')
->orderByDesc('created_at')
->get(['id', 'title', 'text', 'is_meeting', 'is_pinned', 'is_highlighted', 'created_at', 'updated_at']);
$data = ['data' => $news];
return $this->response
->withContent(json_encode($data));
}
} }

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Controllers\Api;
use Engelsystem\Http\Response;
use Engelsystem\Models\News;
class NewsController extends ApiController
{
public function index(): Response
{
$news = News::query()
->orderByDesc('updated_at')
->orderByDesc('created_at')
->get(['id', 'title', 'text', 'is_meeting', 'is_pinned', 'is_highlighted', 'created_at', 'updated_at']);
$data = ['data' => $news];
return $this->response
->withContent(json_encode($data));
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Api;
use Engelsystem\Test\Unit\Controllers\ControllerTest as TestCase;
use League\OpenAPIValidation\PSR7\OperationAddress as OpenApiAddress;
use League\OpenAPIValidation\PSR7\ResponseValidator as OpenApiResponseValidator;
use League\OpenAPIValidation\PSR7\ValidatorBuilder as OpenApiValidatorBuilder;
use Psr\Http\Message\ResponseInterface;
abstract class ApiBaseControllerTest extends TestCase
{
protected OpenApiResponseValidator $validator;
protected function validateApiResponse(string $path, string $method, ResponseInterface $response): void
{
$operation = new OpenApiAddress($path, $method);
$this->validator->validate($operation, $response);
}
public function setUp(): void
{
parent::setUp();
$this->initDatabase();
$openApiDefinition = $this->app->get('path.resources.api') . '/openapi.yml';
$this->validator = (new OpenApiValidatorBuilder())
->fromYamlFile($openApiDefinition)
->getResponseValidator();
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Api;
use Engelsystem\Controllers\Api\ApiController;
use Engelsystem\Http\Response;
class ApiControllerTest extends ApiBaseControllerTest
{
/**
* @covers \Engelsystem\Controllers\Api\ApiController::__construct
*/
public function testConstruct(): void
{
$controller = new class (new Response('{"some":"json"}')) extends ApiController {
public function getResponse(): Response
{
return $this->response;
}
};
$response = $controller->getResponse();
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertJson($response->getContent());
$this->assertEquals(['*'], $response->getHeader('access-control-allow-origin'));
$this->assertJson($response->getContent());
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Api;
use Engelsystem\Controllers\Api\IndexController;
use Engelsystem\Http\Response;
class IndexControllerTest extends ApiBaseControllerTest
{
/**
* @covers \Engelsystem\Controllers\Api\IndexController::__construct
* @covers \Engelsystem\Controllers\Api\IndexController::index
*/
public function testIndex(): void
{
$controller = new IndexController(new Response());
$response = $controller->index();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertEquals(['*'], $response->getHeader('access-control-allow-origin'));
$this->assertJson($response->getContent());
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('versions', $data);
}
/**
* @covers \Engelsystem\Controllers\Api\IndexController::indexV0
*/
public function testIndexV0(): void
{
$controller = new IndexController(new Response());
$response = $controller->indexV0();
$this->assertEquals(200, $response->getStatusCode());
$this->assertJson($response->getContent());
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('version', $data);
$this->assertArrayHasKey('paths', $data);
}
/**
* @covers \Engelsystem\Controllers\Api\IndexController::options
*/
public function testOptions(): void
{
$controller = new IndexController(new Response());
$response = $controller->options();
$this->assertEquals(204, $response->getStatusCode());
$this->assertNotEmpty($response->getHeader('allow'));
$this->assertNotEmpty($response->getHeader('access-control-allow-headers'));
}
/**
* @covers \Engelsystem\Controllers\Api\IndexController::notImplemented
*/
public function testNotImplemented(): void
{
$controller = new IndexController(new Response());
$response = $controller->notImplemented();
$this->assertEquals(501, $response->getStatusCode());
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertJson($response->getContent());
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Api;
use Engelsystem\Controllers\Api\NewsController;
use Engelsystem\Http\Response;
use Engelsystem\Models\News;
class NewsControllerTest extends ApiBaseControllerTest
{
/**
* @covers \Engelsystem\Controllers\Api\NewsController::index
*/
public function testIndex(): void
{
News::factory(3)->create();
$controller = new NewsController(new Response());
$response = $controller->index();
$this->validateApiResponse('/news', 'get', $response);
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertJson($response->getContent());
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('data', $data);
$this->assertCount(3, $data['data']);
}
}

View File

@ -1,114 +0,0 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Controllers\ApiController;
use Engelsystem\Http\Response;
use Engelsystem\Models\News;
use League\OpenAPIValidation\PSR7\OperationAddress as OpenApiAddress;
use League\OpenAPIValidation\PSR7\ResponseValidator as OpenApiResponseValidator;
use League\OpenAPIValidation\PSR7\ValidatorBuilder as OpenApiValidatorBuilder;
class ApiControllerTest extends ControllerTest
{
protected OpenApiResponseValidator $validator;
/**
* @covers \Engelsystem\Controllers\ApiController::__construct
* @covers \Engelsystem\Controllers\ApiController::index
*/
public function testIndex(): void
{
$controller = new ApiController(new Response());
$response = $controller->index();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertEquals(['*'], $response->getHeader('Access-Control-Allow-Origin'));
$this->assertJson($response->getContent());
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('versions', $data);
}
/**
* @covers \Engelsystem\Controllers\ApiController::indexV0
*/
public function testIndexV0(): void
{
$controller = new ApiController(new Response());
$response = $controller->indexV0();
$this->assertEquals(200, $response->getStatusCode());
$this->assertJson($response->getContent());
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('version', $data);
$this->assertArrayHasKey('paths', $data);
}
/**
* @covers \Engelsystem\Controllers\ApiController::options
*/
public function testOptions(): void
{
$controller = new ApiController(new Response());
$response = $controller->options();
$this->assertEquals(204, $response->getStatusCode());
$this->assertNotEmpty($response->getHeader('Allow'));
$this->assertNotEmpty($response->getHeader('Access-Control-Allow-Headers'));
}
/**
* @covers \Engelsystem\Controllers\ApiController::notImplemented
*/
public function testNotImplemented(): void
{
$controller = new ApiController(new Response());
$response = $controller->notImplemented();
$this->assertEquals(501, $response->getStatusCode());
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertJson($response->getContent());
}
/**
* @covers \Engelsystem\Controllers\ApiController::news
*/
public function testNews(): void
{
$this->initDatabase();
News::factory(3)->create();
$controller = new ApiController(new Response());
$response = $controller->news();
$operation = new OpenApiAddress('/news', 'get');
$this->validator->validate($operation, $response);
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertJson($response->getContent());
$data = json_decode($response->getContent(), true);
$this->assertArrayHasKey('data', $data);
$this->assertCount(3, $data['data']);
}
public function setUp(): void
{
parent::setUp();
$openApiDefinition = $this->app->get('path.resources.api') . '/openapi.yml';
$this->validator = (new OpenApiValidatorBuilder())
->fromYamlFile($openApiDefinition)
->getResponseValidator();
}
}