Skip to content

Commit

Permalink
add AcceptAndContentTypeMiddleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominik Zogg committed May 11, 2019
1 parent 675d657 commit f3d3c0e
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 5 deletions.
6 changes: 4 additions & 2 deletions README.md
Expand Up @@ -16,16 +16,18 @@ A simple http handler implementation for API.

* php: ^7.1
* chubbyphp/chubbyphp-deserialization: ^2.0
* chubbyphp/chubbyphp-negotiation: ^1.0
* chubbyphp/chubbyphp-serialization: ^2.0
* psr/http-factory: ^1.0
* psr/http-factory: ^1.0.1
* psr/http-message: ^1.0.1
* psr/http-server-middleware: ^1.0.1

## Installation

Through [Composer](http://getcomposer.org) as [chubbyphp/chubbyphp-api-http][1].

```sh
composer require chubbyphp/chubbyphp-api-http "^3.0"
composer require chubbyphp/chubbyphp-api-http "^3.1"
```

## Usage
Expand Down
8 changes: 5 additions & 3 deletions composer.json
Expand Up @@ -12,9 +12,11 @@
"require": {
"php": "^7.1",
"chubbyphp/chubbyphp-deserialization": "^2.0",
"chubbyphp/chubbyphp-negotiation": "^1.0",
"chubbyphp/chubbyphp-serialization": "^2.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0.1"
"psr/http-factory": "^1.0.1",
"psr/http-message": "^1.0.1",
"psr/http-server-middleware": "^1.0.1"
},
"require-dev": {
"chubbyphp/chubbyphp-mock": "^1.4",
Expand All @@ -32,7 +34,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.1-dev"
}
}
}
86 changes: 86 additions & 0 deletions src/Middleware/AcceptAndContentTypeMiddleware.php
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Chubbyphp\ApiHttp\Middleware;

use Chubbyphp\ApiHttp\ApiProblem\ClientError\NotAcceptable;
use Chubbyphp\ApiHttp\ApiProblem\ClientError\UnsupportedMediaType;
use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface;
use Chubbyphp\Negotiation\AcceptNegotiatorInterface;
use Chubbyphp\Negotiation\ContentTypeNegotiatorInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class AcceptAndContentTypeMiddleware implements MiddlewareInterface
{
/**
* @var AcceptNegotiatorInterface
*/
private $acceptNegotiator;

/**
* @var ContentTypeNegotiatorInterface
*/
private $contentTypeNegotiator;

/**
* @var ResponseManagerInterface
*/
private $responseManager;

/**
* @param AcceptNegotiatorInterface $acceptNegotiator
* @param ContentTypeNegotiatorInterface $contentTypeNegotiator
* @param ResponseManagerInterface $responseManager
*/
public function __construct(
AcceptNegotiatorInterface $acceptNegotiator,
ContentTypeNegotiatorInterface $contentTypeNegotiator,
ResponseManagerInterface $responseManager
) {
$this->acceptNegotiator = $acceptNegotiator;
$this->contentTypeNegotiator = $contentTypeNegotiator;
$this->responseManager = $responseManager;
}

/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (null === $accept = $this->acceptNegotiator->negotiate($request)) {
$supportedMediaTypes = $this->acceptNegotiator->getSupportedMediaTypes();
return $this->responseManager->createFromApiProblem(
new NotAcceptable(
$request->getHeaderLine('Accept'),
$supportedMediaTypes
),
$supportedMediaTypes[0]
);
}

$request = $request->withAttribute('accept', $accept->getValue());

if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true)) {
if (null === $contentType = $this->contentTypeNegotiator->negotiate($request)) {
return $this->responseManager->createFromApiProblem(
new UnsupportedMediaType(
$request->getHeaderLine('Content-Type'),
$this->contentTypeNegotiator->getSupportedMediaTypes()
),
$accept->getValue()
);
}

$request = $request->withAttribute('contentType', $contentType->getValue());
}

return $handler->handle($request);
}
}
261 changes: 261 additions & 0 deletions tests/Middleware/AcceptAndContentTypeMiddlewareTest.php
@@ -0,0 +1,261 @@
<?php

declare(strict_types=1);

namespace Chubbyphp\Tests\ApiHttp\Middleware;

use Chubbyphp\ApiHttp\ApiProblem\ClientError\NotAcceptable;
use Chubbyphp\ApiHttp\ApiProblem\ClientError\UnsupportedMediaType;
use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface;
use Chubbyphp\ApiHttp\Middleware\AcceptAndContentTypeMiddleware;
use Chubbyphp\Mock\Argument\ArgumentCallback;
use Chubbyphp\Mock\Call;
use Chubbyphp\Mock\MockByCallsTrait;
use Chubbyphp\Negotiation\AcceptNegotiatorInterface;
use Chubbyphp\Negotiation\ContentTypeNegotiatorInterface;
use Chubbyphp\Negotiation\NegotiatedValueInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* @covers \Chubbyphp\ApiHttp\Middleware\AcceptAndContentTypeMiddleware
*/
class AcceptAndContentTypeMiddlewareTest extends TestCase
{
use MockByCallsTrait;

public function testWithoutAccept(): void
{
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockByCalls(ServerRequestInterface::class, [
Call::create('getHeaderLine')->with('Accept')->willReturn('application/xml'),
]);

/** @var ResponseInterface|MockObject $response */
$response = $this->getMockByCalls(ResponseInterface::class, []);

$requestHandler = new class() implements RequestHandlerInterface {
/**
* @param ServerRequestInterface $request
*
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
self::fail('should not be called');
}
};

/** @var AcceptNegotiatorInterface|MockObject $acceptNegotiator */
$acceptNegotiator = $this->getMockByCalls(AcceptNegotiatorInterface::class, [
Call::create('negotiate')->with($request)->willReturn(null),
Call::create('getSupportedMediaTypes')->with()->willReturn(['application/json']),
]);

/** @var ContentTypeNegotiatorInterface|MockObject $contentTypeNegotiator */
$contentTypeNegotiator = $this->getMockByCalls(ContentTypeNegotiatorInterface::class, []);

/** @var ResponseManagerInterface|MockObject $responseManager */
$responseManager = $this->getMockByCalls(ResponseManagerInterface::class, [
Call::create('createFromApiProblem')
->with(
new ArgumentCallback(function (NotAcceptable $apiProblem) {
self::assertSame('application/xml', $apiProblem->getAccept());
self::assertSame(['application/json'], $apiProblem->getAcceptables());
}),
'application/json',
null
)
->willReturn($response),
]);

$middleware = new AcceptAndContentTypeMiddleware($acceptNegotiator, $contentTypeNegotiator, $responseManager);

self::assertSame($response, $middleware->process($request, $requestHandler));
}

public function testWithAccept(): void
{
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockByCalls(ServerRequestInterface::class, [
Call::create('withAttribute')->with('accept', 'application/json')->willReturnSelf(),
Call::create('getMethod')->with()->willReturn('GET'),
]);

/** @var ResponseInterface|MockObject $response */
$response = $this->getMockByCalls(ResponseInterface::class, []);

$requestHandler = new class($response) implements RequestHandlerInterface {
/**
* @var ResponseInterface
*/
private $response;

/**
* @param ResponseInterface $response
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

/**
* @param ServerRequestInterface $request
*
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->response;
}
};

/** @var NegotiatedValueInterface|MockObject $accept */
$accept = $this->getMockByCalls(NegotiatedValueInterface::class, [
Call::create('getValue')->with()->willReturn('application/json'),
]);

/** @var AcceptNegotiatorInterface|MockObject $acceptNegotiator */
$acceptNegotiator = $this->getMockByCalls(AcceptNegotiatorInterface::class, [
Call::create('negotiate')->with($request)->willReturn($accept),
]);

/** @var ContentTypeNegotiatorInterface|MockObject $contentTypeNegotiator */
$contentTypeNegotiator = $this->getMockByCalls(ContentTypeNegotiatorInterface::class, []);

/** @var ResponseManagerInterface|MockObject $responseManager */
$responseManager = $this->getMockByCalls(ResponseManagerInterface::class, []);

$middleware = new AcceptAndContentTypeMiddleware($acceptNegotiator, $contentTypeNegotiator, $responseManager);

self::assertSame($response, $middleware->process($request, $requestHandler));
}

public function testWithoutContentType(): void
{
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockByCalls(ServerRequestInterface::class, [
Call::create('withAttribute')->with('accept', 'application/json')->willReturnSelf(),
Call::create('getMethod')->with()->willReturn('POST'),
Call::create('getHeaderLine')->with('Content-Type')->willReturn('application/xml'),
]);

/** @var ResponseInterface|MockObject $response */
$response = $this->getMockByCalls(ResponseInterface::class, []);

$requestHandler = new class() implements RequestHandlerInterface {
/**
* @param ServerRequestInterface $request
*
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
self::fail('should not be called');
}
};

/** @var NegotiatedValueInterface|MockObject $accept */
$accept = $this->getMockByCalls(NegotiatedValueInterface::class, [
Call::create('getValue')->with()->willReturn('application/json'),
Call::create('getValue')->with()->willReturn('application/json'),
]);

/** @var AcceptNegotiatorInterface $acceptNegotiator */
$acceptNegotiator = $this->getMockByCalls(AcceptNegotiatorInterface::class, [
Call::create('negotiate')->with($request)->willReturn($accept),
]);

/** @var ContentTypeNegotiatorInterface|MockObject $contentTypeNegotiator */
$contentTypeNegotiator = $this->getMockByCalls(ContentTypeNegotiatorInterface::class, [
Call::create('negotiate')->with($request)->willReturn(null),
Call::create('getSupportedMediaTypes')->with()->willReturn(['application/json']),
]);

/** @var ResponseManagerInterface|MockObject $responseManager */
$responseManager = $this->getMockByCalls(ResponseManagerInterface::class, [
Call::create('createFromApiProblem')
->with(
new ArgumentCallback(function (UnsupportedMediaType $apiProblem) {
self::assertSame('application/xml', $apiProblem->getMediaType());
self::assertSame(['application/json'], $apiProblem->getSupportedMediaTypes());
}),
'application/json',
null
)
->willReturn($response),
]);

$middleware = new AcceptAndContentTypeMiddleware($acceptNegotiator, $contentTypeNegotiator, $responseManager);

self::assertSame($response, $middleware->process($request, $requestHandler));
}

public function testWithContentType(): void
{
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockByCalls(ServerRequestInterface::class, [
Call::create('withAttribute')->with('accept', 'application/json')->willReturnSelf(),
Call::create('getMethod')->with()->willReturn('POST'),
Call::create('withAttribute')->with('contentType', 'application/json')->willReturnSelf(),
]);

/** @var ResponseInterface|MockObject $response */
$response = $this->getMockByCalls(ResponseInterface::class, []);

$requestHandler = new class($response) implements RequestHandlerInterface {
/**
* @var ResponseInterface
*/
private $response;

/**
* @param ResponseInterface $response
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

/**
* @param ServerRequestInterface $request
*
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->response;
}
};

/** @var NegotiatedValueInterface|MockObject $accept */
$accept = $this->getMockByCalls(NegotiatedValueInterface::class, [
Call::create('getValue')->with()->willReturn('application/json'),
]);

/** @var AcceptNegotiatorInterface|MockObject $acceptNegotiator */
$acceptNegotiator = $this->getMockByCalls(AcceptNegotiatorInterface::class, [
Call::create('negotiate')->with($request)->willReturn($accept),
]);

/** @var NegotiatedValueInterface|MockObject $contentType */
$contentType = $this->getMockByCalls(NegotiatedValueInterface::class, [
Call::create('getValue')->with()->willReturn('application/json'),
]);

/** @var ContentTypeNegotiatorInterface|MockObject $contentTypeNegotiator */
$contentTypeNegotiator = $this->getMockByCalls(ContentTypeNegotiatorInterface::class, [
Call::create('negotiate')->with($request)->willReturn($contentType),
]);

/** @var ResponseManagerInterface|MockObject $responseManager */
$responseManager = $this->getMockByCalls(ResponseManagerInterface::class, []);

$middleware = new AcceptAndContentTypeMiddleware($acceptNegotiator, $contentTypeNegotiator, $responseManager);

self::assertSame($response, $middleware->process($request, $requestHandler));
}
}

0 comments on commit f3d3c0e

Please sign in to comment.