Add ETag to FeedController
This commit is contained in:
parent
dc216a0464
commit
72d48de3ae
|
@ -48,6 +48,7 @@ return [
|
||||||
|
|
||||||
// Changes of request/response parameters
|
// Changes of request/response parameters
|
||||||
\Engelsystem\Middleware\SetLocale::class,
|
\Engelsystem\Middleware\SetLocale::class,
|
||||||
|
\Engelsystem\Middleware\ETagHandler::class,
|
||||||
\Engelsystem\Middleware\AddHeaders::class,
|
\Engelsystem\Middleware\AddHeaders::class,
|
||||||
|
|
||||||
// The application code
|
// The application code
|
||||||
|
|
|
@ -33,7 +33,7 @@ class FeedController extends BaseController
|
||||||
{
|
{
|
||||||
$news = $this->getNews();
|
$news = $this->getNews();
|
||||||
|
|
||||||
return $this->response
|
return $this->withEtag($news)
|
||||||
->withHeader('content-type', 'application/atom+xml; charset=utf-8')
|
->withHeader('content-type', 'application/atom+xml; charset=utf-8')
|
||||||
->withView('api/atom', ['news' => $news]);
|
->withView('api/atom', ['news' => $news]);
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class FeedController extends BaseController
|
||||||
{
|
{
|
||||||
$news = $this->getNews();
|
$news = $this->getNews();
|
||||||
|
|
||||||
return $this->response
|
return $this->withEtag($news)
|
||||||
->withHeader('content-type', 'application/rss+xml; charset=utf-8')
|
->withHeader('content-type', 'application/rss+xml; charset=utf-8')
|
||||||
->withView('api/rss', ['news' => $news]);
|
->withView('api/rss', ['news' => $news]);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ class FeedController extends BaseController
|
||||||
{
|
{
|
||||||
$shifts = $this->getShifts();
|
$shifts = $this->getShifts();
|
||||||
|
|
||||||
return $this->response
|
return $this->withEtag($shifts)
|
||||||
->withHeader('content-type', 'text/calendar; charset=utf-8')
|
->withHeader('content-type', 'text/calendar; charset=utf-8')
|
||||||
->withHeader('content-disposition', 'attachment; filename=shifts.ics')
|
->withHeader('content-disposition', 'attachment; filename=shifts.ics')
|
||||||
->withView('api/ical', ['shiftEntries' => $shifts]);
|
->withView('api/ical', ['shiftEntries' => $shifts]);
|
||||||
|
@ -121,11 +121,18 @@ class FeedController extends BaseController
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->response
|
return $this->withEtag($response)
|
||||||
->withAddedHeader('content-type', 'application/json; charset=utf-8')
|
->withAddedHeader('content-type', 'application/json; charset=utf-8')
|
||||||
->withContent(json_encode($response));
|
->withContent(json_encode($response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function withEtag(mixed $value): Response
|
||||||
|
{
|
||||||
|
$hash = md5(json_encode($value));
|
||||||
|
|
||||||
|
return $this->response->setEtag($hash);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getNews(): Collection
|
protected function getNews(): Collection
|
||||||
{
|
{
|
||||||
$news = $this->request->has('meetings')
|
$news = $this->request->has('meetings')
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Engelsystem\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Nyholm\Psr7\Stream;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class ETagHandler implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Compare the response ETag to a requested If-None-Match header and send a 304 "not modified" if they match
|
||||||
|
*/
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
$etagMatch = $request->getHeader('If-None-Match');
|
||||||
|
$etag = $response->getHeader('ETag');
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$etagMatch
|
||||||
|
|| !$etag
|
||||||
|
|| !Str::contains(implode(', ', $etagMatch), trim($etag[0], '"'))
|
||||||
|
) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response
|
||||||
|
->withStatus(Response::HTTP_NOT_MODIFIED)
|
||||||
|
->withBody(Stream::create());
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
/**
|
/**
|
||||||
* @covers \Engelsystem\Controllers\FeedController::__construct
|
* @covers \Engelsystem\Controllers\FeedController::__construct
|
||||||
* @covers \Engelsystem\Controllers\FeedController::atom
|
* @covers \Engelsystem\Controllers\FeedController::atom
|
||||||
|
* @covers \Engelsystem\Controllers\FeedController::withEtag
|
||||||
*/
|
*/
|
||||||
public function testAtom(): void
|
public function testAtom(): void
|
||||||
{
|
{
|
||||||
|
@ -33,6 +34,12 @@ class FeedControllerTest extends ControllerTest
|
||||||
['content-type', 'application/atom+xml; charset=utf-8'],
|
['content-type', 'application/atom+xml; charset=utf-8'],
|
||||||
$this->response
|
$this->response
|
||||||
);
|
);
|
||||||
|
$this->response->expects($this->once())
|
||||||
|
->method('setEtag')
|
||||||
|
->willReturnCallback(function ($etag) {
|
||||||
|
$this->assertNotEmpty($etag);
|
||||||
|
return $this->response;
|
||||||
|
});
|
||||||
$this->response->expects($this->once())
|
$this->response->expects($this->once())
|
||||||
->method('withView')
|
->method('withView')
|
||||||
->willReturnCallback(function ($view, $data) {
|
->willReturnCallback(function ($view, $data) {
|
||||||
|
@ -58,6 +65,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
['content-type', 'application/rss+xml; charset=utf-8'],
|
['content-type', 'application/rss+xml; charset=utf-8'],
|
||||||
$this->response
|
$this->response
|
||||||
);
|
);
|
||||||
|
$this->setExpects($this->response, 'setEtag', null, $this->response);
|
||||||
$this->response->expects($this->once())
|
$this->response->expects($this->once())
|
||||||
->method('withView')
|
->method('withView')
|
||||||
->willReturnCallback(function ($view, $data) {
|
->willReturnCallback(function ($view, $data) {
|
||||||
|
@ -66,6 +74,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
|
|
||||||
return $this->response;
|
return $this->response;
|
||||||
});
|
});
|
||||||
|
|
||||||
$controller->rss();
|
$controller->rss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +104,8 @@ class FeedControllerTest extends ControllerTest
|
||||||
)
|
)
|
||||||
->willReturn($this->response);
|
->willReturn($this->response);
|
||||||
|
|
||||||
|
$this->setExpects($this->response, 'setEtag', null, $this->response);
|
||||||
|
|
||||||
$this->response->expects($this->once())
|
$this->response->expects($this->once())
|
||||||
->method('withView')
|
->method('withView')
|
||||||
->willReturnCallback(function ($view, $data) {
|
->willReturnCallback(function ($view, $data) {
|
||||||
|
@ -109,6 +120,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
|
|
||||||
return $this->response;
|
return $this->response;
|
||||||
});
|
});
|
||||||
|
|
||||||
$controller->ical();
|
$controller->ical();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +149,8 @@ class FeedControllerTest extends ControllerTest
|
||||||
$this->response
|
$this->response
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->setExpects($this->response, 'setEtag', null, $this->response);
|
||||||
|
|
||||||
$this->response->expects($this->once())
|
$this->response->expects($this->once())
|
||||||
->method('withContent')
|
->method('withContent')
|
||||||
->willReturnCallback(function ($jsonData) {
|
->willReturnCallback(function ($jsonData) {
|
||||||
|
@ -162,6 +176,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
|
|
||||||
return $this->response;
|
return $this->response;
|
||||||
});
|
});
|
||||||
|
|
||||||
$controller->shifts();
|
$controller->shifts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,6 +199,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
|
|
||||||
$this->request->attributes->set('meetings', $isMeeting);
|
$this->request->attributes->set('meetings', $isMeeting);
|
||||||
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
||||||
|
$this->setExpects($this->response, 'setEtag', null, $this->response);
|
||||||
$this->response->expects($this->once())
|
$this->response->expects($this->once())
|
||||||
->method('withView')
|
->method('withView')
|
||||||
->willReturnCallback(function ($view, $data) use ($isMeeting) {
|
->willReturnCallback(function ($view, $data) use ($isMeeting) {
|
||||||
|
@ -210,6 +226,7 @@ class FeedControllerTest extends ControllerTest
|
||||||
$controller = new FeedController($this->auth, $this->request, $this->response);
|
$controller = new FeedController($this->auth, $this->request, $this->response);
|
||||||
|
|
||||||
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
||||||
|
$this->setExpects($this->response, 'setEtag', null, $this->response);
|
||||||
$this->response->expects($this->once())
|
$this->response->expects($this->once())
|
||||||
->method('withView')
|
->method('withView')
|
||||||
->willReturnCallback(function ($view, $data) {
|
->willReturnCallback(function ($view, $data) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Engelsystem\Test\Unit\Middleware;
|
||||||
|
|
||||||
|
use Engelsystem\Http\Request;
|
||||||
|
use Engelsystem\Http\Response;
|
||||||
|
use Engelsystem\Middleware\ETagHandler;
|
||||||
|
use Engelsystem\Test\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
class ETagHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @covers \Engelsystem\Middleware\ETagHandler::process
|
||||||
|
*/
|
||||||
|
public function testRegister(): void
|
||||||
|
{
|
||||||
|
/** @var RequestHandlerInterface|MockObject $handler */
|
||||||
|
$handler = $this->getMockForAbstractClass(RequestHandlerInterface::class);
|
||||||
|
$request = Request::create('https://localhost')
|
||||||
|
->withHeader('If-None-Match', 'FooBarBaz');
|
||||||
|
$originalResponse = (new Response())
|
||||||
|
->withHeader('ETag', '"FooBarBaz"')
|
||||||
|
->withHeader('original-header', 'value')
|
||||||
|
->withContent('Foo bar!');
|
||||||
|
$this->setExpects($handler, 'handle', [$request], $originalResponse);
|
||||||
|
|
||||||
|
$middleware = new ETagHandler();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
|
||||||
|
$this->assertTrue($response->hasHeader('original-header'));
|
||||||
|
$this->assertEquals('value', $response->getHeader('original-header')[0]);
|
||||||
|
|
||||||
|
$this->assertEquals(304, $response->getStatusCode());
|
||||||
|
$this->assertEquals('', (string) $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \Engelsystem\Middleware\ETagHandler::process
|
||||||
|
*/
|
||||||
|
public function testRegisterNoChange(): void
|
||||||
|
{
|
||||||
|
/** @var RequestHandlerInterface|MockObject $handler */
|
||||||
|
$handler = $this->getMockForAbstractClass(RequestHandlerInterface::class);
|
||||||
|
$request = Request::create('https://localhost');
|
||||||
|
$originalResponse = new Response();
|
||||||
|
$this->setExpects($handler, 'handle', [$request], $originalResponse);
|
||||||
|
|
||||||
|
$middleware = new ETagHandler();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
|
||||||
|
$this->assertEquals($originalResponse, $response);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue