diff --git a/.gitignore b/.gitignore index 87904ec..30e934c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/.phpcs-cache +/.phpunit.result.cache /clover.xml /composer.lock /coveralls-upload.json diff --git a/.travis.yml b/.travis.yml index e810782..385dde6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,24 +12,24 @@ env: matrix: fast_finish: true include: - - php: 7.1 + - php: 7.3 env: - DEPS=lowest - - php: 7.1 + - php: 7.3 env: - DEPS=latest - - CS_CHECK=true - - TEST_COVERAGE=true - - php: 7.2 + - php: 7.4 env: - DEPS=lowest - - php: 7.2 + - php: 7.4 env: - DEPS=latest - - php: 7.3 + - CS_CHECK=true + - TEST_COVERAGE=true + - php: 8.0 env: - DEPS=lowest - - php: 7.3 + - php: 8.0 env: - DEPS=latest diff --git a/composer.json b/composer.json index 4875051..448be49 100644 --- a/composer.json +++ b/composer.json @@ -22,16 +22,12 @@ "sort-packages": true }, "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev", - "dev-develop": "1.2.x-dev" - }, "laminas": { "config-provider": "Mezzio\\ProblemDetails\\ConfigProvider" } }, "require": { - "php": "^7.1", + "php": "^7.3 || ~8.0.0", "ext-json": "*", "fig/http-message-util": "^1.1.2", "laminas/laminas-zendframework-bridge": "^1.0", @@ -39,12 +35,11 @@ "psr/http-message": "^1.0", "psr/http-server-middleware": "^1.0", "spatie/array-to-xml": "^2.3", - "willdurand/negotiation": "^2.3" + "willdurand/negotiation": "^3.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~1.0.0", - "phpspec/prophecy": "^1.8.0", - "phpunit/phpunit": "^7.0.1" + "laminas/laminas-coding-standard": "~2.1.0", + "phpunit/phpunit": "^9.3" }, "autoload": { "psr-4": { diff --git a/phpcs.xml b/phpcs.xml index 4da1eed..1efe663 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,8 +1,20 @@ - - + + + + + + + + + + src test + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7fb861f..3639332 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,17 +1,13 @@ - - - - ./test - - - - - - ./src - - + + + + ./src + + + + + ./test + + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 69ccc93..91aa89a 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -10,6 +10,10 @@ namespace Mezzio\ProblemDetails; +use Zend\ProblemDetails\ProblemDetailsMiddleware as LegacyProblemDetailsMiddleware; +use Zend\ProblemDetails\ProblemDetailsNotFoundHandler as LegacyProblemDetailsNotFoundHandler; +use Zend\ProblemDetails\ProblemDetailsResponseFactory as LegacyProblemDetailsResponseFactory; + /** * Configuration provider for the package. * @@ -20,7 +24,7 @@ class ConfigProvider /** * Returns the configuration array. */ - public function __invoke() : array + public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), @@ -30,17 +34,17 @@ public function __invoke() : array /** * Returns the container dependencies. */ - public function getDependencies() : array + public function getDependencies(): array { return [ // Legacy Zend Framework aliases - 'aliases' => [ - \Zend\ProblemDetails\ProblemDetailsMiddleware::class => ProblemDetailsMiddleware::class, - \Zend\ProblemDetails\ProblemDetailsNotFoundHandler::class => ProblemDetailsNotFoundHandler::class, - \Zend\ProblemDetails\ProblemDetailsResponseFactory::class => ProblemDetailsResponseFactory::class, + 'aliases' => [ + LegacyProblemDetailsMiddleware::class => ProblemDetailsMiddleware::class, + LegacyProblemDetailsNotFoundHandler::class => ProblemDetailsNotFoundHandler::class, + LegacyProblemDetailsResponseFactory::class => ProblemDetailsResponseFactory::class, ], - 'factories' => [ - ProblemDetailsMiddleware::class => ProblemDetailsMiddlewareFactory::class, + 'factories' => [ + ProblemDetailsMiddleware::class => ProblemDetailsMiddlewareFactory::class, ProblemDetailsNotFoundHandler::class => ProblemDetailsNotFoundHandlerFactory::class, ProblemDetailsResponseFactory::class => ProblemDetailsResponseFactoryFactory::class, ], diff --git a/src/Exception/CommonProblemDetailsExceptionTrait.php b/src/Exception/CommonProblemDetailsExceptionTrait.php index ee2e0ae..3f07e7b 100644 --- a/src/Exception/CommonProblemDetailsExceptionTrait.php +++ b/src/Exception/CommonProblemDetailsExceptionTrait.php @@ -25,52 +25,42 @@ */ trait CommonProblemDetailsExceptionTrait { - /** - * @var int - */ + /** @var int */ private $status; - /** - * @var string - */ + /** @var string */ private $detail; - /** - * @var string - */ + /** @var string */ private $title; - /** - * @var string - */ + /** @var string */ private $type; - /** - * @var array - */ + /** @var array */ private $additional = []; - public function getStatus() : int + public function getStatus(): int { return $this->status; } - public function getType() : string + public function getType(): string { return $this->type; } - public function getTitle() : string + public function getTitle(): string { return $this->title; } - public function getDetail() : string + public function getDetail(): string { return $this->detail; } - public function getAdditionalData() : array + public function getAdditionalData(): array { return $this->additional; } @@ -81,7 +71,7 @@ public function getAdditionalData() : array * Likely useful for the JsonSerializable implementation, but also * for cases where the XML variant is desired. */ - public function toArray() : array + public function toArray(): array { $problem = [ 'status' => $this->status, diff --git a/src/Exception/ProblemDetailsExceptionInterface.php b/src/Exception/ProblemDetailsExceptionInterface.php index b72c472..622a1e8 100644 --- a/src/Exception/ProblemDetailsExceptionInterface.php +++ b/src/Exception/ProblemDetailsExceptionInterface.php @@ -18,15 +18,15 @@ */ interface ProblemDetailsExceptionInterface extends JsonSerializable, Throwable { - public function getStatus() : int; + public function getStatus(): int; - public function getType() : string; + public function getType(): string; - public function getTitle() : string; + public function getTitle(): string; - public function getDetail() : string; + public function getDetail(): string; - public function getAdditionalData() : array; + public function getAdditionalData(): array; /** * Serialize the exception to an array of problem details. @@ -34,5 +34,5 @@ public function getAdditionalData() : array; * Likely useful for the JsonSerializable implementation, but also * for cases where the XML variant is desired. */ - public function toArray() : array; + public function toArray(): array; } diff --git a/src/ProblemDetailsMiddleware.php b/src/ProblemDetailsMiddleware.php index c5dc05b..ec37005 100644 --- a/src/ProblemDetailsMiddleware.php +++ b/src/ProblemDetailsMiddleware.php @@ -30,14 +30,10 @@ */ class ProblemDetailsMiddleware implements MiddlewareInterface { - /** - * @var callable[] - */ + /** @var callable[] */ private $listeners = []; - /** - * @var ProblemDetailsResponseFactory - */ + /** @var ProblemDetailsResponseFactory */ private $responseFactory; public function __construct(ProblemDetailsResponseFactory $responseFactory) @@ -48,7 +44,7 @@ public function __construct(ProblemDetailsResponseFactory $responseFactory) /** * {@inheritDoc} */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // If we cannot provide a representation, act as a no-op. if (! $this->canActAsErrorHandler($request)) { @@ -81,7 +77,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * listeners are ignored; use listeners for reporting purposes * only. */ - public function attachListener(callable $listener) : void + public function attachListener(callable $listener): void { if (in_array($listener, $this->listeners, true)) { return; @@ -95,7 +91,7 @@ public function attachListener(callable $listener) : void * * Returns a boolean false if negotiation fails. */ - private function canActAsErrorHandler(ServerRequestInterface $request) : bool + private function canActAsErrorHandler(ServerRequestInterface $request): bool { $accept = $request->getHeaderLine('Accept') ?: '*/*'; @@ -108,17 +104,16 @@ private function canActAsErrorHandler(ServerRequestInterface $request) : bool * * Only raises exceptions for errors that are within the error_reporting mask. */ - private function createErrorHandler() : callable + private function createErrorHandler(): callable { /** * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline - * @return void * @throws ErrorException if error is not within the error_reporting mask. */ - return function (int $errno, string $errstr, string $errfile, int $errline) : void { + return function (int $errno, string $errstr, string $errfile, int $errline): void { if (! (error_reporting() & $errno)) { // error_reporting does not include this error return; @@ -135,7 +130,7 @@ private function triggerListeners( Throwable $error, ServerRequestInterface $request, ResponseInterface $response - ) : void { + ): void { array_walk($this->listeners, function ($listener) use ($error, $request, $response) { $listener($error, $request, $response); }); diff --git a/src/ProblemDetailsMiddlewareFactory.php b/src/ProblemDetailsMiddlewareFactory.php index b2da786..c9d9710 100644 --- a/src/ProblemDetailsMiddlewareFactory.php +++ b/src/ProblemDetailsMiddlewareFactory.php @@ -14,7 +14,7 @@ class ProblemDetailsMiddlewareFactory { - public function __invoke(ContainerInterface $container) : ProblemDetailsMiddleware + public function __invoke(ContainerInterface $container): ProblemDetailsMiddleware { return new ProblemDetailsMiddleware($container->get(ProblemDetailsResponseFactory::class)); } diff --git a/src/ProblemDetailsNotFoundHandler.php b/src/ProblemDetailsNotFoundHandler.php index 20f8555..8b02268 100644 --- a/src/ProblemDetailsNotFoundHandler.php +++ b/src/ProblemDetailsNotFoundHandler.php @@ -20,9 +20,7 @@ class ProblemDetailsNotFoundHandler implements MiddlewareInterface { - /** - * @var ProblemDetailsResponseFactory - */ + /** @var ProblemDetailsResponseFactory */ private $responseFactory; /** @@ -37,7 +35,7 @@ public function __construct(ProblemDetailsResponseFactory $responseFactory) /** * Creates and returns a 404 response. */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // If we cannot provide a representation, act as a no-op. if (! $this->canActAsErrorHandler($request)) { @@ -54,7 +52,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Can the middleware act as an error handler? */ - private function canActAsErrorHandler(ServerRequestInterface $request) : bool + private function canActAsErrorHandler(ServerRequestInterface $request): bool { $accept = $request->getHeaderLine('Accept') ?: '*/*'; diff --git a/src/ProblemDetailsNotFoundHandlerFactory.php b/src/ProblemDetailsNotFoundHandlerFactory.php index 8a4f31f..804119e 100644 --- a/src/ProblemDetailsNotFoundHandlerFactory.php +++ b/src/ProblemDetailsNotFoundHandlerFactory.php @@ -14,7 +14,7 @@ class ProblemDetailsNotFoundHandlerFactory { - public function __invoke(ContainerInterface $container) : ProblemDetailsNotFoundHandler + public function __invoke(ContainerInterface $container): ProblemDetailsNotFoundHandler { return new ProblemDetailsNotFoundHandler($container->get(ProblemDetailsResponseFactory::class)); } diff --git a/src/ProblemDetailsResponseFactory.php b/src/ProblemDetailsResponseFactory.php index 0c91680..c8a164f 100644 --- a/src/ProblemDetailsResponseFactory.php +++ b/src/ProblemDetailsResponseFactory.php @@ -4,6 +4,8 @@ * @see https://github.com/mezzio/mezzio-problem-details for the canonical source repository * @copyright https://github.com/mezzio/mezzio-problem-details/blob/master/COPYRIGHT.md * @license https://github.com/mezzio/mezzio-problem-details/blob/master/LICENSE.md New BSD License + * + * phpcs:disable WebimpressCodingStandard.Namespaces.UnusedUseStatement.UnusedUse */ declare(strict_types=1); @@ -30,6 +32,7 @@ use function preg_replace; use function print_r; use function sprintf; +use function str_replace; use function strpos; use const JSON_PARTIAL_OUTPUT_ON_ERROR; @@ -79,36 +82,36 @@ class ProblemDetailsResponseFactory */ public const DEFAULT_TITLE_MAP = [ // 4×× Client Error - StatusCode::STATUS_BAD_REQUEST => 'Bad Request', - StatusCode::STATUS_UNAUTHORIZED => 'Unauthorized', - StatusCode::STATUS_PAYMENT_REQUIRED => 'Payment Required', - StatusCode::STATUS_FORBIDDEN => 'Forbidden', - StatusCode::STATUS_NOT_FOUND => 'Not Found', - StatusCode::STATUS_METHOD_NOT_ALLOWED => 'Method Not Allowed', - StatusCode::STATUS_NOT_ACCEPTABLE => 'Not Acceptable', - StatusCode::STATUS_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', - StatusCode::STATUS_REQUEST_TIMEOUT => 'Request Timeout', - StatusCode::STATUS_CONFLICT => 'Conflict', - StatusCode::STATUS_GONE => 'Gone', - StatusCode::STATUS_LENGTH_REQUIRED => 'Length Required', - StatusCode::STATUS_PRECONDITION_FAILED => 'Precondition Failed', - StatusCode::STATUS_PAYLOAD_TOO_LARGE => 'Payload Too Large', - StatusCode::STATUS_URI_TOO_LONG => 'Request-URI Too Long', - StatusCode::STATUS_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', - StatusCode::STATUS_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', - StatusCode::STATUS_EXPECTATION_FAILED => 'Expectation Failed', - StatusCode::STATUS_IM_A_TEAPOT => 'I\'m a teapot', - StatusCode::STATUS_MISDIRECTED_REQUEST => 'Misdirected Request', - StatusCode::STATUS_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', - StatusCode::STATUS_LOCKED => 'Locked', - StatusCode::STATUS_FAILED_DEPENDENCY => 'Failed Dependency', - StatusCode::STATUS_UPGRADE_REQUIRED => 'Upgrade Required', - StatusCode::STATUS_PRECONDITION_REQUIRED => 'Precondition Required', - StatusCode::STATUS_TOO_MANY_REQUESTS => 'Too Many Requests', - StatusCode::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', - 444 => 'Connection Closed Without Response', - StatusCode::STATUS_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', - 499 => 'Client Closed Request', + StatusCode::STATUS_BAD_REQUEST => 'Bad Request', + StatusCode::STATUS_UNAUTHORIZED => 'Unauthorized', + StatusCode::STATUS_PAYMENT_REQUIRED => 'Payment Required', + StatusCode::STATUS_FORBIDDEN => 'Forbidden', + StatusCode::STATUS_NOT_FOUND => 'Not Found', + StatusCode::STATUS_METHOD_NOT_ALLOWED => 'Method Not Allowed', + StatusCode::STATUS_NOT_ACCEPTABLE => 'Not Acceptable', + StatusCode::STATUS_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', + StatusCode::STATUS_REQUEST_TIMEOUT => 'Request Timeout', + StatusCode::STATUS_CONFLICT => 'Conflict', + StatusCode::STATUS_GONE => 'Gone', + StatusCode::STATUS_LENGTH_REQUIRED => 'Length Required', + StatusCode::STATUS_PRECONDITION_FAILED => 'Precondition Failed', + StatusCode::STATUS_PAYLOAD_TOO_LARGE => 'Payload Too Large', + StatusCode::STATUS_URI_TOO_LONG => 'Request-URI Too Long', + StatusCode::STATUS_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', + StatusCode::STATUS_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', + StatusCode::STATUS_EXPECTATION_FAILED => 'Expectation Failed', + StatusCode::STATUS_IM_A_TEAPOT => 'I\'m a teapot', + StatusCode::STATUS_MISDIRECTED_REQUEST => 'Misdirected Request', + StatusCode::STATUS_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', + StatusCode::STATUS_LOCKED => 'Locked', + StatusCode::STATUS_FAILED_DEPENDENCY => 'Failed Dependency', + StatusCode::STATUS_UPGRADE_REQUIRED => 'Upgrade Required', + StatusCode::STATUS_PRECONDITION_REQUIRED => 'Precondition Required', + StatusCode::STATUS_TOO_MANY_REQUESTS => 'Too Many Requests', + StatusCode::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', + 444 => 'Connection Closed Without Response', + StatusCode::STATUS_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', + 499 => 'Client Closed Request', // 5×× Server Error StatusCode::STATUS_INTERNAL_SERVER_ERROR => 'Internal Server Error', StatusCode::STATUS_NOT_IMPLEMENTED => 'Not Implemented', @@ -217,16 +220,16 @@ class ProblemDetailsResponseFactory public function __construct( callable $responseFactory, bool $isDebug = self::EXCLUDE_THROWABLE_DETAILS, - int $jsonFlags = null, + ?int $jsonFlags = null, bool $exceptionDetailsInResponse = false, string $defaultDetailMessage = self::DEFAULT_DETAIL_MESSAGE, array $defaultTypesMap = [] ) { // Ensures type safety of the composed factory - $this->responseFactory = function () use ($responseFactory) : ResponseInterface { + $this->responseFactory = function () use ($responseFactory): ResponseInterface { return $responseFactory(); }; - $this->isDebug = $isDebug; + $this->isDebug = $isDebug; if (! $jsonFlags) { $jsonFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE @@ -236,10 +239,10 @@ public function __construct( $jsonFlags = JSON_PRETTY_PRINT | $jsonFlags; } } - $this->jsonFlags = $jsonFlags; + $this->jsonFlags = $jsonFlags; $this->exceptionDetailsInResponse = $exceptionDetailsInResponse; - $this->defaultDetailMessage = $defaultDetailMessage; - $this->defaultTypesMap = $defaultTypesMap; + $this->defaultDetailMessage = $defaultDetailMessage; + $this->defaultTypesMap = $defaultTypesMap; } public function createResponse( @@ -249,7 +252,7 @@ public function createResponse( string $title = '', string $type = '', array $additional = [] - ) : ResponseInterface { + ): ResponseInterface { $status = $this->normalizeStatus($status); $title = $title ?: $this->createTitleFromStatus($status); $type = $type ?: $this->createTypeFromStatus($status); @@ -280,7 +283,7 @@ public function createResponse( public function createResponseFromThrowable( ServerRequestInterface $request, Throwable $e - ) : ResponseInterface { + ): ResponseInterface { if ($e instanceof Exception\ProblemDetailsExceptionInterface) { return $this->createResponse( $request, @@ -292,9 +295,10 @@ public function createResponseFromThrowable( ); } - $detail = $this->isDebug || $this->exceptionDetailsInResponse ? $e->getMessage() : $this->defaultDetailMessage; + $detail = $this->isDebug + || $this->exceptionDetailsInResponse ? $e->getMessage() : $this->defaultDetailMessage; $additionalDetails = $this->isDebug ? $this->createThrowableDetail($e) : []; - $code = $this->isDebug || $this->exceptionDetailsInResponse ? $this->getThrowableCode($e) : 500; + $code = $this->isDebug || $this->exceptionDetailsInResponse ? $this->getThrowableCode($e) : 500; return $this->createResponse( $request, @@ -306,14 +310,14 @@ public function createResponseFromThrowable( ); } - protected function getThrowableCode(Throwable $e) : int + protected function getThrowableCode(Throwable $e): int { $code = $e->getCode(); return is_int($code) ? $code : 0; } - protected function generateJsonResponse(array $payload) : ResponseInterface + protected function generateJsonResponse(array $payload): ResponseInterface { return $this->generateResponse( $payload['status'], @@ -330,15 +334,15 @@ private function cleanKeysForXml(array $input): array { $return = []; foreach ($input as $key => $value) { - $key = str_replace("\n", '_', $key); + $key = str_replace("\n", '_', (string) $key); $startCharacterPattern = '[A-Z]|_|[a-z]|[\xC0-\xD6]|[\xD8-\xF6]|[\xF8-\x{2FF}]|[\x{370}-\x{37D}]|[\x{37F}-\x{1FFF}]|' . '[\x{200C}-\x{200D}]|[\x{2070}-\x{218F}]|[\x{2C00}-\x{2FEF}]|[\x{3001}-\x{D7FF}]|[\x{F900}-\x{FDCF}]' . '|[\x{FDF0}-\x{FFFD}]'; - $characterPattern = $startCharacterPattern . '|\-|\.|[0-9]|\xB7|[\x{300}-\x{36F}]|[\x{203F}-\x{2040}]'; + $characterPattern = $startCharacterPattern . '|\-|\.|[0-9]|\xB7|[\x{300}-\x{36F}]|[\x{203F}-\x{2040}]'; - $key = preg_replace('/(?!'.$characterPattern.')./u', '_', $key); - $key = preg_replace('/^(?!'.$startCharacterPattern.')./u', '_', $key); + $key = preg_replace('/(?!' . $characterPattern . ')./u', '_', $key); + $key = preg_replace('/^(?!' . $startCharacterPattern . ')./u', '_', $key); if (is_array($value)) { $value = $this->cleanKeysForXml($value); @@ -348,7 +352,7 @@ private function cleanKeysForXml(array $input): array return $return; } - protected function generateXmlResponse(array $payload) : ResponseInterface + protected function generateXmlResponse(array $payload): ResponseInterface { // Ensure any objects are flattened to arrays first $content = json_decode(json_encode($payload), true); @@ -357,8 +361,8 @@ protected function generateXmlResponse(array $payload) : ResponseInterface $cleanedContent = $this->cleanKeysForXml($content); $converter = new ArrayToXml($cleanedContent, 'problem'); - $dom = $converter->toDom(); - $root = $dom->firstChild; + $dom = $converter->toDom(); + $root = $dom->firstChild; $root->setAttribute('xmlns', 'urn:ietf:rfc:7807'); return $this->generateResponse( @@ -368,7 +372,7 @@ protected function generateXmlResponse(array $payload) : ResponseInterface ); } - protected function generateResponse(int $status, string $contentType, string $payload) : ResponseInterface + protected function generateResponse(int $status, string $contentType, string $payload): ResponseInterface { $response = ($this->responseFactory)(); $response->getBody()->write($payload); @@ -378,7 +382,7 @@ protected function generateResponse(int $status, string $contentType, string $pa ->withHeader('Content-Type', $contentType); } - private function getResponseGenerator(ServerRequestInterface $request) : callable + private function getResponseGenerator(ServerRequestInterface $request): callable { $accept = $request->getHeaderLine('Accept') ?: '*/*'; $mediaType = (new Negotiator())->getBest($accept, self::NEGOTIATION_PRIORITIES); @@ -388,7 +392,7 @@ private function getResponseGenerator(ServerRequestInterface $request) : callabl : Closure::fromCallable([$this, 'generateJsonResponse']); } - private function normalizeStatus(int $status) : int + private function normalizeStatus(int $status): int { if ($status < 400 || $status > 599) { return 500; @@ -397,17 +401,17 @@ private function normalizeStatus(int $status) : int return $status; } - private function createTitleFromStatus(int $status) : string + private function createTitleFromStatus(int $status): string { return self::DEFAULT_TITLE_MAP[$status] ?? 'Unknown Error'; } - private function createTypeFromStatus(int $status) : string + private function createTypeFromStatus(int $status): string { return $this->defaultTypesMap[$status] ?? sprintf('https://httpstatus.es/%s', $status); } - private function createThrowableDetail(Throwable $e) : array + private function createThrowableDetail(Throwable $e): array { $detail = [ 'class' => get_class($e), diff --git a/src/ProblemDetailsResponseFactoryFactory.php b/src/ProblemDetailsResponseFactoryFactory.php index 265f86e..e7d1d58 100644 --- a/src/ProblemDetailsResponseFactoryFactory.php +++ b/src/ProblemDetailsResponseFactoryFactory.php @@ -15,14 +15,14 @@ class ProblemDetailsResponseFactoryFactory { - public function __invoke(ContainerInterface $container) : ProblemDetailsResponseFactory + public function __invoke(ContainerInterface $container): ProblemDetailsResponseFactory { - $config = $container->has('config') ? $container->get('config') : []; + $config = $container->has('config') ? $container->get('config') : []; $includeThrowableDetail = $config['debug'] ?? ProblemDetailsResponseFactory::EXCLUDE_THROWABLE_DETAILS; $problemDetailsConfig = $config['problem-details'] ?? []; - $jsonFlags = $problemDetailsConfig['json_flags'] ?? null; - $defaultTypesMap = $problemDetailsConfig['default_types_map'] ?? []; + $jsonFlags = $problemDetailsConfig['json_flags'] ?? null; + $defaultTypesMap = $problemDetailsConfig['default_types_map'] ?? []; return new ProblemDetailsResponseFactory( $container->get(ResponseInterface::class), diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index dbf4cdc..cdb6524 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -19,10 +19,10 @@ class ConfigProviderTest extends TestCase { - public function testReturnsExpectedDependencies() : void + public function testReturnsExpectedDependencies(): void { $provider = new ConfigProvider(); - $config = $provider(); + $config = $provider(); $this->assertArrayHasKey('dependencies', $config); diff --git a/test/Exception/ProblemDetailsExceptionInterfaceTest.php b/test/Exception/ProblemDetailsExceptionInterfaceTest.php index 039ca18..c44c803 100644 --- a/test/Exception/ProblemDetailsExceptionInterfaceTest.php +++ b/test/Exception/ProblemDetailsExceptionInterfaceTest.php @@ -18,17 +18,22 @@ use function json_decode; use function json_encode; -class ProblemDetailsExceptionTest extends TestCase +class ProblemDetailsExceptionInterfaceTest extends TestCase { + /** @var int */ protected $status = 403; + /** @var string */ protected $detail = 'You are not authorized to do that'; + /** @var string */ protected $title = 'Unauthorized'; + /** @var string */ protected $type = 'https://httpstatus.es/403'; + /** @var string[] */ protected $additional = [ 'foo' => 'bar', ]; - protected function setUp() : void + protected function setUp(): void { $this->exception = new class ( $this->status, @@ -41,16 +46,16 @@ protected function setUp() : void public function __construct(int $status, string $detail, string $title, string $type, array $additional) { - $this->status = $status; - $this->detail = $detail; - $this->title = $title; - $this->type = $type; + $this->status = $status; + $this->detail = $detail; + $this->title = $title; + $this->type = $type; $this->additional = $additional; } }; } - public function testCanPullDetailsIndividually() : void + public function testCanPullDetailsIndividually(): void { $this->assertEquals($this->status, $this->exception->getStatus()); $this->assertEquals($this->detail, $this->exception->getDetail()); @@ -59,7 +64,7 @@ public function testCanPullDetailsIndividually() : void $this->assertEquals($this->additional, $this->exception->getAdditionalData()); } - public function testCanCastDetailsToArray() : void + public function testCanCastDetailsToArray(): void { $this->assertEquals([ 'status' => $this->status, @@ -70,7 +75,7 @@ public function testCanCastDetailsToArray() : void ], $this->exception->toArray()); } - public function testIsJsonSerializable() : void + public function testIsJsonSerializable(): void { $problem = json_decode(json_encode($this->exception), true); diff --git a/test/ProblemDetailsAssertionsTrait.php b/test/ProblemDetailsAssertionsTrait.php index eead62a..9f0b5d4 100644 --- a/test/ProblemDetailsAssertionsTrait.php +++ b/test/ProblemDetailsAssertionsTrait.php @@ -11,8 +11,7 @@ namespace MezzioTest\ProblemDetails; use PHPUnit\Framework\Assert; -use Prophecy\Argument; -use Prophecy\Prophecy\ObjectProphecy; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\StreamInterface; use Throwable; @@ -26,7 +25,7 @@ trait ProblemDetailsAssertionsTrait { - public function assertProblemDetails(array $expected, array $details) : void + public function assertProblemDetails(array $expected, array $details): void { foreach ($expected as $key => $value) { $this->assertArrayHasKey( @@ -44,7 +43,7 @@ public function assertProblemDetails(array $expected, array $details) : void } } - public function assertExceptionDetails(Throwable $e, array $details) : void + public function assertExceptionDetails(Throwable $e, array $details): void { $this->assertArrayHasKey('class', $details); $this->assertSame(get_class($e), $details['class']); @@ -65,13 +64,13 @@ public function assertExceptionDetails(Throwable $e, array $details) : void } /** - * @param StreamInterface|ObjectProphecy $stream + * @param StreamInterface|MockObject $stream */ public function prepareResponsePayloadAssertions( string $contentType, - ObjectProphecy $stream, + MockObject $stream, callable $assertion - ) : void { + ): void { if ('application/problem+json' === $contentType) { $this->preparePayloadForJsonResponse($stream, $assertion); return; @@ -84,39 +83,41 @@ public function prepareResponsePayloadAssertions( } /** - * @param StreamInterface|ObjectProphecy $stream + * @param StreamInterface|MockObject $stream */ - public function preparePayloadForJsonResponse(ObjectProphecy $stream, callable $assertion) : void + public function preparePayloadForJsonResponse(MockObject $stream, callable $assertion): void { $stream - ->write(Argument::that(function ($body) use ($assertion) { - Assert::assertInternalType('string', $body); + ->expects($this->any()) + ->method('write') + ->with($this->callback(function ($body) use ($assertion) { + Assert::assertIsString($body); $data = json_decode($body, true); $assertion($data); - return $body; - })) - ->shouldBeCalled(); + return true; + })); } /** - * @param StreamInterface|ObjectProphecy $stream + * @param StreamInterface|MockObject $stream */ - public function preparePayloadForXmlResponse(ObjectProphecy $stream, callable $assertion) : void + public function preparePayloadForXmlResponse(MockObject $stream, callable $assertion): void { $stream - ->write(Argument::that(function ($body) use ($assertion) { - Assert::assertInternalType('string', $body); + ->expects($this->any()) + ->method('write') + ->with($this->callback(function ($body) use ($assertion) { + Assert::assertIsString($body); $data = $this->deserializeXmlPayload($body); $assertion($data); - return $body; - })) - ->shouldBeCalled(); + return true; + })); } - public function deserializeXmlPayload(string $xml) : array + public function deserializeXmlPayload(string $xml): array { - $xml = simplexml_load_string($xml); - $json = json_encode($xml); + $xml = simplexml_load_string($xml); + $json = json_encode($xml); $payload = json_decode($json, true); // Ensure ints and floats are properly represented diff --git a/test/ProblemDetailsMiddlewareFactoryTest.php b/test/ProblemDetailsMiddlewareFactoryTest.php index d14fe15..c06e2fd 100644 --- a/test/ProblemDetailsMiddlewareFactoryTest.php +++ b/test/ProblemDetailsMiddlewareFactoryTest.php @@ -13,39 +13,50 @@ use Mezzio\ProblemDetails\ProblemDetailsMiddleware; use Mezzio\ProblemDetails\ProblemDetailsMiddlewareFactory; use Mezzio\ProblemDetails\ProblemDetailsResponseFactory; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use ReflectionObject; use RuntimeException; class ProblemDetailsMiddlewareFactoryTest extends TestCase { - protected function setUp() : void + /** @var ContainerInterface|MockObject */ + private $container; + + protected function setUp(): void { - $this->container = $this->prophesize(ContainerInterface::class); - $this->factory = new ProblemDetailsMiddlewareFactory(); + $this->container = $this->createMock(ContainerInterface::class); + $this->factory = new ProblemDetailsMiddlewareFactory(); } public function testRaisesExceptionWhenProblemDetailsResponseFactoryServiceIsNotAvailable() { $e = new RuntimeException(); - $this->container->get(ProblemDetailsResponseFactory::class)->willThrow($e); + $this->container + ->method('get') + ->with(ProblemDetailsResponseFactory::class) + ->willThrowException($e); $this->expectException(RuntimeException::class); - $this->factory->__invoke($this->container->reveal()); + $this->factory->__invoke($this->container); } - public function testCreatesMiddlewareUsingResponseFactoryService() : void + public function testCreatesMiddlewareUsingResponseFactoryService(): void { - $responseFactory = $this->prophesize(ProblemDetailsResponseFactory::class)->reveal(); - $this->container->get(ProblemDetailsResponseFactory::class)->willReturn($responseFactory); + $responseFactory = $this->createMock(ProblemDetailsResponseFactory::class); + + $this->container + ->method('get') + ->with(ProblemDetailsResponseFactory::class) + ->willReturn($responseFactory); + + $middleware = ($this->factory)($this->container); - $middleware = ($this->factory)($this->container->reveal()); + $r = (new ReflectionObject($middleware))->getProperty('responseFactory'); + $r->setAccessible(true); $this->assertInstanceOf(ProblemDetailsMiddleware::class, $middleware); - $this->assertAttributeSame( - $responseFactory, - 'responseFactory', - $middleware - ); + $this->assertSame($responseFactory, $r->getValue($middleware)); } } diff --git a/test/ProblemDetailsMiddlewareTest.php b/test/ProblemDetailsMiddlewareTest.php index 81fd57a..98c779d 100644 --- a/test/ProblemDetailsMiddlewareTest.php +++ b/test/ProblemDetailsMiddlewareTest.php @@ -14,7 +14,6 @@ use Mezzio\ProblemDetails\ProblemDetailsMiddleware; use Mezzio\ProblemDetails\ProblemDetailsResponseFactory; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -27,14 +26,14 @@ class ProblemDetailsMiddlewareTest extends TestCase { use ProblemDetailsAssertionsTrait; - protected function setUp() : void + protected function setUp(): void { - $this->request = $this->prophesize(ServerRequestInterface::class); - $this->responseFactory = $this->prophesize(ProblemDetailsResponseFactory::class); - $this->middleware = new ProblemDetailsMiddleware($this->responseFactory->reveal()); + $this->request = $this->createMock(ServerRequestInterface::class); + $this->responseFactory = $this->createMock(ProblemDetailsResponseFactory::class); + $this->middleware = new ProblemDetailsMiddleware($this->responseFactory); } - public function acceptHeaders() : array + public function acceptHeaders(): array { return [ 'empty' => [''], @@ -45,40 +44,45 @@ public function acceptHeaders() : array ]; } - public function testSuccessfulDelegationReturnsHandlerResponse() : void + public function testSuccessfulDelegationReturnsHandlerResponse(): void { - $response = $this->prophesize(ResponseInterface::class); - $handler = $this->prophesize(RequestHandlerInterface::class); + $response = $this->createMock(ResponseInterface::class); + $handler = $this->createMock(RequestHandlerInterface::class); $handler - ->handle(Argument::that([$this->request, 'reveal'])) - ->will([$response, 'reveal']); + ->method('handle') + ->with($this->request) + ->willReturn($response); + $result = $this->middleware->process($this->request, $handler); - $result = $this->middleware->process($this->request->reveal(), $handler->reveal()); - - $this->assertSame($response->reveal(), $result); + $this->assertSame($response, $result); } /** * @dataProvider acceptHeaders */ - public function testThrowableRaisedByHandlerResultsInProblemDetails(string $accept) : void + public function testThrowableRaisedByHandlerResultsInProblemDetails(string $accept): void { - $this->request->getHeaderLine('Accept')->willReturn($accept); + $this->request + ->method('getHeaderLine') + ->with('Accept') + ->willReturn($accept); $exception = new TestAsset\RuntimeException('Thrown!', 507); - $handler = $this->prophesize(RequestHandlerInterface::class); + $handler = $this->createMock(RequestHandlerInterface::class); $handler - ->handle(Argument::that([$this->request, 'reveal'])) - ->willThrow($exception); + ->method('handle') + ->with($this->request) + ->willThrowException($exception); - $expected = $this->prophesize(ResponseInterface::class)->reveal(); + $expected = $this->createMock(ResponseInterface::class); $this->responseFactory - ->createResponseFromThrowable($this->request->reveal(), $exception) + ->method('createResponseFromThrowable') + ->with($this->request, $exception) ->willReturn($expected); - $result = $this->middleware->process($this->request->reveal(), $handler->reveal()); + $result = $this->middleware->process($this->request, $handler); $this->assertSame($expected, $result); } @@ -86,20 +90,25 @@ public function testThrowableRaisedByHandlerResultsInProblemDetails(string $acce /** * @dataProvider acceptHeaders */ - public function testMiddlewareRegistersErrorHandlerToConvertErrorsToProblemDetails(string $accept) : void + public function testMiddlewareRegistersErrorHandlerToConvertErrorsToProblemDetails(string $accept): void { - $this->request->getHeaderLine('Accept')->willReturn($accept); + $this->request + ->method('getHeaderLine') + ->with('Accept') + ->willReturn($accept); - $handler = $this->prophesize(RequestHandlerInterface::class); + $handler = $this->createMock(RequestHandlerInterface::class); $handler - ->handle(Argument::that([$this->request, 'reveal'])) - ->will(function () { + ->method('handle') + ->with($this->request) + ->willReturnCallback(function () { trigger_error('Triggered error!', E_USER_ERROR); }); - $expected = $this->prophesize(ResponseInterface::class)->reveal(); + $expected = $this->createMock(ResponseInterface::class); $this->responseFactory - ->createResponseFromThrowable($this->request->reveal(), Argument::that(function ($e) { + ->method('createResponseFromThrowable') + ->with($this->request, $this->callback(function ($e) { $this->assertInstanceOf(ErrorException::class, $e); $this->assertEquals(E_USER_ERROR, $e->getSeverity()); $this->assertEquals('Triggered error!', $e->getMessage()); @@ -107,55 +116,65 @@ public function testMiddlewareRegistersErrorHandlerToConvertErrorsToProblemDetai })) ->willReturn($expected); - $result = $this->middleware->process($this->request->reveal(), $handler->reveal()); + $result = $this->middleware->process($this->request, $handler); $this->assertSame($expected, $result); } - public function testRethrowsCaughtExceptionIfUnableToNegotiateAcceptHeader() : void + public function testRethrowsCaughtExceptionIfUnableToNegotiateAcceptHeader(): void { - $this->request->getHeaderLine('Accept')->willReturn('text/html'); + $this->request + ->method('getHeaderLine') + ->with('Accept') + ->willReturn('text/html'); + $exception = new TestAsset\RuntimeException('Thrown!', 507); - $handler = $this->prophesize(RequestHandlerInterface::class); + $handler = $this->createMock(RequestHandlerInterface::class); $handler - ->handle(Argument::that([$this->request, 'reveal'])) - ->willThrow($exception); + ->method('handle') + ->with($this->request) + ->willThrowException($exception); $this->expectException(TestAsset\RuntimeException::class); $this->expectExceptionMessage('Thrown!'); $this->expectExceptionCode(507); - $this->middleware->process($this->request->reveal(), $handler->reveal()); + $this->middleware->process($this->request, $handler); } /** * @dataProvider acceptHeaders */ - public function testErrorHandlingTriggersListeners(string $accept) : void + public function testErrorHandlingTriggersListeners(string $accept): void { - $this->request->getHeaderLine('Accept')->willReturn($accept); + $this->request + ->method('getHeaderLine') + ->with('Accept') + ->willReturn($accept); $exception = new TestAsset\RuntimeException('Thrown!', 507); - $handler = $this->prophesize(RequestHandlerInterface::class); + $handler = $this->createMock(RequestHandlerInterface::class); $handler - ->handle(Argument::that([$this->request, 'reveal'])) - ->willThrow($exception); + ->method('handle') + ->with($this->request) + ->willThrowException($exception); - $expected = $this->prophesize(ResponseInterface::class)->reveal(); + $expected = $this->createMock(ResponseInterface::class); $this->responseFactory - ->createResponseFromThrowable($this->request->reveal(), $exception) + ->method('createResponseFromThrowable') + ->with($this->request, $exception) ->willReturn($expected); - $listener = function ($error, $request, $response) use ($exception, $expected) { + $listener = function ($error, $request, $response) use ($exception, $expected) { $this->assertSame($exception, $error, 'Listener did not receive same exception as was raised'); - $this->assertSame($this->request->reveal(), $request, 'Listener did not receive same request'); + $this->assertSame($this->request, $request, 'Listener did not receive same request'); $this->assertSame($expected, $response, 'Listener did not receive same response'); }; $listener2 = clone $listener; $this->middleware->attachListener($listener); $this->middleware->attachListener($listener2); - $result = $this->middleware->process($this->request->reveal(), $handler->reveal()); + $result = $this->middleware->process($this->request, $handler); $this->assertSame($expected, $result); } diff --git a/test/ProblemDetailsNotFoundHandlerFactoryTest.php b/test/ProblemDetailsNotFoundHandlerFactoryTest.php index ec1c41c..473d4fc 100644 --- a/test/ProblemDetailsNotFoundHandlerFactoryTest.php +++ b/test/ProblemDetailsNotFoundHandlerFactoryTest.php @@ -15,37 +15,43 @@ use Mezzio\ProblemDetails\ProblemDetailsResponseFactory; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use ReflectionObject; use RuntimeException; class ProblemDetailsNotFoundHandlerFactoryTest extends TestCase { - protected function setUp() : void + protected function setUp(): void { - $this->container = $this->prophesize(ContainerInterface::class); - $this->factory = new ProblemDetailsNotFoundHandlerFactory(); + $this->container = $this->createMock(ContainerInterface::class); + $this->factory = new ProblemDetailsNotFoundHandlerFactory(); } public function testRaisesExceptionWhenProblemDetailsResponseFactoryServiceIsNotAvailable() { $e = new RuntimeException(); - $this->container->get(ProblemDetailsResponseFactory::class)->willThrow($e); + $this->container + ->method('get') + ->with(ProblemDetailsResponseFactory::class) + ->willThrowException($e); $this->expectException(RuntimeException::class); - $this->factory->__invoke($this->container->reveal()); + $this->factory->__invoke($this->container); } - public function testCreatesNotFoundHandlerUsingResponseFactoryService() : void + public function testCreatesNotFoundHandlerUsingResponseFactoryService(): void { - $responseFactory = $this->prophesize(ProblemDetailsResponseFactory::class)->reveal(); - $this->container->get(ProblemDetailsResponseFactory::class)->willReturn($responseFactory); + $responseFactory = $this->createMock(ProblemDetailsResponseFactory::class); + $this->container + ->method('get') + ->with(ProblemDetailsResponseFactory::class) + ->willReturn($responseFactory); - $notFoundHandler = ($this->factory)($this->container->reveal()); + $notFoundHandler = ($this->factory)($this->container); + + $r = (new ReflectionObject($notFoundHandler))->getProperty('responseFactory'); + $r->setAccessible(true); $this->assertInstanceOf(ProblemDetailsNotFoundHandler::class, $notFoundHandler); - $this->assertAttributeSame( - $responseFactory, - 'responseFactory', - $notFoundHandler - ); + $this->assertSame($responseFactory, $r->getValue($notFoundHandler)); } } diff --git a/test/ProblemDetailsNotFoundHandlerTest.php b/test/ProblemDetailsNotFoundHandlerTest.php index 354c68b..aed32f6 100644 --- a/test/ProblemDetailsNotFoundHandlerTest.php +++ b/test/ProblemDetailsNotFoundHandlerTest.php @@ -13,7 +13,6 @@ use Mezzio\ProblemDetails\ProblemDetailsNotFoundHandler; use Mezzio\ProblemDetails\ProblemDetailsResponseFactory; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -22,12 +21,12 @@ class ProblemDetailsNotFoundHandlerTest extends TestCase { use ProblemDetailsAssertionsTrait; - public function setUp() + protected function setUp(): void { - $this->responseFactory = $this->prophesize(ProblemDetailsResponseFactory::class); + $this->responseFactory = $this->createMock(ProblemDetailsResponseFactory::class); } - public function acceptHeaders() : array + public function acceptHeaders(): array { return [ 'application/json' => ['application/json', 'application/problem+json'], @@ -38,46 +37,48 @@ public function acceptHeaders() : array /** * @dataProvider acceptHeaders */ - public function testResponseFactoryPassedInConstructorGeneratesTheReturnedResponse(string $acceptHeader) : void + public function testResponseFactoryPassedInConstructorGeneratesTheReturnedResponse(string $acceptHeader): void { - $request = $this->prophesize(ServerRequestInterface::class); - $request->getMethod()->willReturn('POST'); - $request->getHeaderLine('Accept')->willReturn($acceptHeader); - $request->getUri()->willReturn('https://example.com/foo'); + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('POST'); + $request->method('getHeaderLine')->with('Accept')->willReturn($acceptHeader); + $request->method('getUri')->willReturn('https://example.com/foo'); - $response = $this->prophesize(ResponseInterface::class); + $response = $this->createMock(ResponseInterface::class); - $this->responseFactory->createResponse( - Argument::that([$request, 'reveal']), - 404, - 'Cannot POST https://example.com/foo!' - )->will([$response, 'reveal']); + $this->responseFactory + ->method('createResponse') + ->with( + $request, + 404, + 'Cannot POST https://example.com/foo!' + )->willReturn($response); - $notFoundHandler = new ProblemDetailsNotFoundHandler($this->responseFactory->reveal()); + $notFoundHandler = new ProblemDetailsNotFoundHandler($this->responseFactory); $this->assertSame( - $response->reveal(), - $notFoundHandler->process($request->reveal(), $this->prophesize(RequestHandlerInterface::class)->reveal()) + $response, + $notFoundHandler->process($request, $this->createMock(RequestHandlerInterface::class)) ); } - public function testHandlerIsCalledIfAcceptHeaderIsUnacceptable() : void + public function testHandlerIsCalledIfAcceptHeaderIsUnacceptable(): void { - $request = $this->prophesize(ServerRequestInterface::class); - $request->getMethod()->willReturn('POST'); - $request->getHeaderLine('Accept')->willReturn('text/html'); - $request->getUri()->willReturn('https://example.com/foo'); + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('POST'); + $request->method('getHeaderLine')->with('Accept')->willReturn('text/html'); + $request->method('getUri')->willReturn('https://example.com/foo'); - $response = $this->prophesize(ResponseInterface::class); + $response = $this->createMock(ResponseInterface::class); - $handler = $this->prophesize(RequestHandlerInterface::class); - $handler->handle($request->reveal())->will([$response, 'reveal']); + $handler = $this->createMock(RequestHandlerInterface::class); + $handler->method('handle')->with($request)->willReturn($response); - $notFoundHandler = new ProblemDetailsNotFoundHandler($this->responseFactory->reveal()); + $notFoundHandler = new ProblemDetailsNotFoundHandler($this->responseFactory); $this->assertSame( - $response->reveal(), - $notFoundHandler->process($request->reveal(), $handler->reveal()) + $response, + $notFoundHandler->process($request, $handler) ); } } diff --git a/test/ProblemDetailsResponseFactoryFactoryTest.php b/test/ProblemDetailsResponseFactoryFactoryTest.php index 8a83f4b..acd613e 100644 --- a/test/ProblemDetailsResponseFactoryFactoryTest.php +++ b/test/ProblemDetailsResponseFactoryFactoryTest.php @@ -17,11 +17,13 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; +use ReflectionObject; use ReflectionProperty; use RuntimeException; use stdClass; use TypeError; +use const JSON_PARTIAL_OUTPUT_ON_ERROR; use const JSON_PRESERVE_ZERO_FRACTION; use const JSON_PRETTY_PRINT; use const JSON_UNESCAPED_SLASHES; @@ -29,9 +31,9 @@ class ProblemDetailsResponseFactoryFactoryTest extends TestCase { - protected function setUp() : void + protected function setUp(): void { - $this->container = $this->prophesize(ContainerInterface::class); + $this->container = $this->createMock(ContainerInterface::class); } public function assertResponseFactoryReturns(ResponseInterface $expected, ProblemDetailsResponseFactory $factory) @@ -46,119 +48,168 @@ public function assertResponseFactoryReturns(ResponseInterface $expected, Proble public function testLackOfResponseServiceResultsInException() { $factory = new ProblemDetailsResponseFactoryFactory(); - $e = new RuntimeException(); + $e = new RuntimeException(); - $this->container->has('config')->willReturn(false); - $this->container->get('config')->shouldNotBeCalled(); - $this->container->get(ResponseInterface::class)->willThrow($e); + $this->container->method('has')->with('config')->willReturn(false); + $this->container->method('get')->with(ResponseInterface::class)->willThrowException($e); $this->expectException(RuntimeException::class); - $factory($this->container->reveal()); + $factory($this->container); } public function testNonCallableResponseServiceResultsInException() { $factory = new ProblemDetailsResponseFactoryFactory(); - $this->container->has('config')->willReturn(false); - $this->container->get('config')->shouldNotBeCalled(); - $this->container->get(ResponseInterface::class)->willReturn(new stdClass); + $this->container->method('has')->with('config')->willReturn(false); + $this->container->method('get')->with(ResponseInterface::class)->willReturn(new stdClass()); $this->expectException(TypeError::class); - $factory($this->container->reveal()); + $factory($this->container); } - public function testLackOfConfigServiceResultsInFactoryUsingDefaults() : void + public function testLackOfConfigServiceResultsInFactoryUsingDefaults(): void { - $this->container->has('config')->willReturn(false); + $this->container->method('has')->with('config')->willReturn(false); - $response = $this->prophesize(ResponseInterface::class)->reveal(); - $this->container->get(ResponseInterface::class)->willReturn(function () use ($response) { - return $response; - }); + $response = $this->createMock(ResponseInterface::class); + $this->container + ->method('get') + ->with(ResponseInterface::class) + ->willReturn(function () use ($response) { + return $response; + }); $factoryFactory = new ProblemDetailsResponseFactoryFactory(); - $factory = $factoryFactory($this->container->reveal()); + $factory = $factoryFactory($this->container); + + $isDebug = (new ReflectionObject($factory))->getProperty('isDebug'); + $isDebug->setAccessible(true); + + $jsonFlags = (new ReflectionObject($factory))->getProperty('jsonFlags'); + $jsonFlags->setAccessible(true); + + $responseFactory = (new ReflectionObject($factory))->getProperty('responseFactory'); + $responseFactory->setAccessible(true); $this->assertInstanceOf(ProblemDetailsResponseFactory::class, $factory); - $this->assertAttributeSame(ProblemDetailsResponseFactory::EXCLUDE_THROWABLE_DETAILS, 'isDebug', $factory); - $this->assertAttributeSame( + $this->assertSame(ProblemDetailsResponseFactory::EXCLUDE_THROWABLE_DETAILS, $isDebug->getValue($factory)); + $this->assertSame( JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_PARTIAL_OUTPUT_ON_ERROR, - 'jsonFlags', - $factory + $jsonFlags->getValue($factory) ); - $this->assertAttributeInstanceOf(Closure::class, 'responseFactory', $factory); + $this->assertInstanceOf(Closure::class, $responseFactory->getValue($factory)); $this->assertResponseFactoryReturns($response, $factory); } - public function testUsesPrettyPrintFlagOnEnabledDebugMode() : void + public function testUsesPrettyPrintFlagOnEnabledDebugMode(): void { - $this->container->has('config')->willReturn(true); - $this->container->get('config')->willReturn([ - 'debug' => true, - ]); - $this->container->get(ResponseInterface::class)->willReturn(function () { - }); + $this->container->method('has')->with('config')->willReturn(true); + + $this->container + ->method('get') + ->willReturnMap([ + ['config', ['debug' => true]], + [ + ResponseInterface::class, + function () { + }, + ], + ]); $factoryFactory = new ProblemDetailsResponseFactoryFactory(); - $factory = $factoryFactory($this->container->reveal()); + $factory = $factoryFactory($this->container); - $this->assertSame(JSON_PRETTY_PRINT, Assert::readAttribute($factory, 'jsonFlags') & JSON_PRETTY_PRINT); + $jsonFlags = (new ReflectionObject($factory))->getProperty('jsonFlags'); + $jsonFlags->setAccessible(true); + + $this->assertSame(JSON_PRETTY_PRINT, $jsonFlags->getValue($factory) & JSON_PRETTY_PRINT); } - public function testUsesDebugSettingFromConfigWhenPresent() : void + public function testUsesDebugSettingFromConfigWhenPresent(): void { - $this->container->has('config')->willReturn(true); - $this->container->get('config')->willReturn(['debug' => true]); - - $this->container->get(ResponseInterface::class)->willReturn(function () { - }); + $this->container->method('has')->with('config')->willReturn(true); + + $this->container + ->method('get') + ->willReturnMap([ + ['config', ['debug' => true]], + [ + ResponseInterface::class, + function () { + }, + ], + ]); $factoryFactory = new ProblemDetailsResponseFactoryFactory(); - $factory = $factoryFactory($this->container->reveal()); + $factory = $factoryFactory($this->container); + + $isDebug = (new ReflectionObject($factory))->getProperty('isDebug'); + $isDebug->setAccessible(true); + + $exceptionDetailsInResponse = (new ReflectionObject($factory))->getProperty('exceptionDetailsInResponse'); + $exceptionDetailsInResponse->setAccessible(true); $this->assertInstanceOf(ProblemDetailsResponseFactory::class, $factory); - $this->assertAttributeSame(ProblemDetailsResponseFactory::INCLUDE_THROWABLE_DETAILS, 'isDebug', $factory); - $this->assertAttributeSame(true, 'exceptionDetailsInResponse', $factory); + $this->assertSame(ProblemDetailsResponseFactory::INCLUDE_THROWABLE_DETAILS, $isDebug->getValue($factory)); + $this->assertSame(true, $exceptionDetailsInResponse->getValue($factory)); } - public function testUsesJsonFlagsSettingFromConfigWhenPresent() : void + public function testUsesJsonFlagsSettingFromConfigWhenPresent(): void { - $this->container->has('config')->willReturn(true); - $this->container->get('config')->willReturn(['problem-details' => ['json_flags' => JSON_PRETTY_PRINT]]); - - $this->container->get(ResponseInterface::class)->willReturn(function () { - }); + $this->container->method('has')->with('config')->willReturn(true); + + $this->container + ->method('get') + ->willReturnMap([ + ['config', ['problem-details' => ['json_flags' => JSON_PRETTY_PRINT]]], + [ + ResponseInterface::class, + function () { + }, + ], + ]); $factoryFactory = new ProblemDetailsResponseFactoryFactory(); - $factory = $factoryFactory($this->container->reveal()); + $factory = $factoryFactory($this->container); + + $jsonFlags = (new ReflectionObject($factory))->getProperty('jsonFlags'); + $jsonFlags->setAccessible(true); $this->assertInstanceOf(ProblemDetailsResponseFactory::class, $factory); - $this->assertAttributeSame(JSON_PRETTY_PRINT, 'jsonFlags', $factory); + $this->assertSame(JSON_PRETTY_PRINT, $jsonFlags->getValue($factory)); } - public function testUsesDefaultTypesSettingFromConfigWhenPresent() : void + public function testUsesDefaultTypesSettingFromConfigWhenPresent(): void { $expectedDefaultTypes = [ 404 => 'https://example.com/problem-details/error/not-found', ]; - $this->container->has('config')->willReturn(true); - $this->container->get('config')->willReturn( - ['problem-details' => ['default_types_map' => $expectedDefaultTypes]] - ); + $this->container->method('has')->with('config')->willReturn(true); - $this->container->get(ResponseInterface::class)->willReturn(function () { - }); + $this->container + ->method('get') + ->willReturnMap([ + ['config', ['problem-details' => ['default_types_map' => $expectedDefaultTypes]]], + [ + ResponseInterface::class, + function () { + }, + ], + ]); $factoryFactory = new ProblemDetailsResponseFactoryFactory(); - $factory = $factoryFactory($this->container->reveal()); + $factory = $factoryFactory($this->container); + + $defaultTypesMap = (new ReflectionObject($factory))->getProperty('defaultTypesMap'); + $defaultTypesMap->setAccessible(true); $this->assertInstanceOf(ProblemDetailsResponseFactory::class, $factory); - $this->assertAttributeSame($expectedDefaultTypes, 'defaultTypesMap', $factory); + $this->assertSame($expectedDefaultTypes, $defaultTypesMap->getValue($factory)); } } diff --git a/test/ProblemDetailsResponseFactoryTest.php b/test/ProblemDetailsResponseFactoryTest.php index 62ba6a3..3f8654e 100644 --- a/test/ProblemDetailsResponseFactoryTest.php +++ b/test/ProblemDetailsResponseFactoryTest.php @@ -11,18 +11,19 @@ namespace MezzioTest\ProblemDetails; use Exception; +use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Mezzio\ProblemDetails\ProblemDetailsResponseFactory; use PHPUnit\Framework\Assert; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use RuntimeException; use function array_keys; +use function chr; use function fclose; use function fopen; use function json_decode; @@ -32,10 +33,10 @@ class ProblemDetailsResponseFactoryTest extends TestCase { use ProblemDetailsAssertionsTrait; - /** @var ServerRequestInterface|ObjectProphecy */ + /** @var ServerRequestInterface|MockObject */ private $request; - /** @var ResponseInterface|ObjectProphecy */ + /** @var ResponseInterface|MockObject */ private $response; /** @var ProblemDetailsResponseFactory */ @@ -43,16 +44,16 @@ class ProblemDetailsResponseFactoryTest extends TestCase private const UTF_8_INVALID_2_OCTET_SEQUENCE = "\xc3\x28"; - protected function setUp() : void + protected function setUp(): void { - $this->request = $this->prophesize(ServerRequestInterface::class); - $this->response = $this->prophesize(ResponseInterface::class); - $this->factory = new ProblemDetailsResponseFactory(function () { - return $this->response->reveal(); + $this->request = $this->createMock(ServerRequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->factory = new ProblemDetailsResponseFactory(function () { + return $this->response; }); } - public function acceptHeaders() : array + public function acceptHeaders(): array { return [ 'empty' => ['', 'application/problem+json'], @@ -66,47 +67,47 @@ public function acceptHeaders() : array /** * @dataProvider acceptHeaders */ - public function testCreateResponseCreatesExpectedType(string $header, string $expectedType) : void + public function testCreateResponseCreatesExpectedType(string $header, string $expectedType): void { - $this->request->getHeaderLine('Accept')->willReturn($header); + $this->request->method('getHeaderLine')->with('Accept')->willReturn($header); - $stream = $this->prophesize(StreamInterface::class); - $stream->write(Argument::type('string'))->shouldBeCalled(); + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->atLeastOnce())->method('write')->with($this->isType('string')); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', $expectedType)->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response->method('withHeader')->with('Content-Type', $expectedType)->willReturn($this->response); $response = $this->factory->createResponse( - $this->request->reveal(), + $this->request, 500, 'Unknown error occurred' ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } /** * @dataProvider acceptHeaders */ - public function testCreateResponseFromThrowableCreatesExpectedType(string $header, string $expectedType) : void + public function testCreateResponseFromThrowableCreatesExpectedType(string $header, string $expectedType): void { - $this->request->getHeaderLine('Accept')->willReturn($header); + $this->request->method('getHeaderLine')->with('Accept')->willReturn($header); - $stream = $this->prophesize(StreamInterface::class); - $stream->write(Argument::type('string'))->shouldBeCalled(); + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->atLeastOnce())->method('write')->with($this->isType('string')); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', $expectedType)->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response->method('withHeader')->with('Content-Type', $expectedType)->willReturn($this->response); $exception = new RuntimeException(); - $response = $this->factory->createResponseFromThrowable( - $this->request->reveal(), + $response = $this->factory->createResponseFromThrowable( + $this->request, $exception ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } /** @@ -115,40 +116,40 @@ public function testCreateResponseFromThrowableCreatesExpectedType(string $heade public function testCreateResponseFromThrowableCreatesExpectedTypeWithExtraInformation( string $header, string $expectedType - ) : void { - $this->request->getHeaderLine('Accept')->willReturn($header); + ): void { + $this->request->method('getHeaderLine')->with('Accept')->willReturn($header); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->prepareResponsePayloadAssertions($expectedType, $stream, function (array $payload) { Assert::assertArrayHasKey('exception', $payload); }); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', $expectedType)->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response->method('withHeader')->with('Content-Type', $expectedType)->willReturn($this->response); $factory = new ProblemDetailsResponseFactory( function () { - return $this->response->reveal(); + return $this->response; }, ProblemDetailsResponseFactory::INCLUDE_THROWABLE_DETAILS ); $exception = new RuntimeException(); - $response = $factory->createResponseFromThrowable( - $this->request->reveal(), + $response = $factory->createResponseFromThrowable( + $this->request, $exception ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } /** * @dataProvider acceptHeaders */ - public function testCreateResponseRemovesInvalidCharactersFromXmlKeys(string $header, string $expectedType) : void + public function testCreateResponseRemovesInvalidCharactersFromXmlKeys(string $header, string $expectedType): void { - $this->request->getHeaderLine('Accept')->willReturn($header); + $this->request->method('getHeaderLine')->with('Accept')->willReturn($header); $additional = [ 'foo' => [ @@ -172,7 +173,7 @@ public function testCreateResponseRemovesInvalidCharactersFromXmlKeys(string $he $expectedKeyNames = array_keys($additional['foo']); } - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->prepareResponsePayloadAssertions( $expectedType, $stream, @@ -181,12 +182,12 @@ function (array $payload) use ($expectedKeyNames) { } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', $expectedType)->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response->method('withHeader')->with('Content-Type', $expectedType)->willReturn($this->response); $response = $this->factory->createResponse( - $this->request->reveal(), + $this->request, 500, 'Unknown error occurred', 'Title', @@ -194,21 +195,35 @@ function (array $payload) use ($expectedKeyNames) { $additional ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } - public function testCreateResponseFromThrowableWillPullDetailsFromProblemDetailsExceptionInterface() : void + private function createProblemDetailsExceptionWithAdditional(array $additional): ProblemDetailsExceptionInterface { - $e = $this->prophesize(ProblemDetailsExceptionInterface::class); - $e->getStatus()->willReturn(400); - $e->getDetail()->willReturn('Exception details'); - $e->getTitle()->willReturn('Invalid client request'); - $e->getType()->willReturn('https://example.com/api/doc/invalid-client-request'); - $e->getAdditionalData()->willReturn(['foo' => 'bar']); - - $this->request->getHeaderLine('Accept')->willReturn('application/json'); + return new class ( + 400, + 'Exception details', + 'Invalid client request', + 'https://example.com/api/doc/invalid-client-request', + $additional + ) extends Exception implements ProblemDetailsExceptionInterface { + use CommonProblemDetailsExceptionTrait; + + public function __construct(int $status, string $detail, string $title, string $type, array $additional) + { + $this->status = $status; + $this->detail = $detail; + $this->title = $title; + $this->type = $type; + $this->additional = $additional; + } + }; + } - $stream = $this->prophesize(StreamInterface::class); + public function testCreateResponseFromThrowableWillPullDetailsFromProblemDetailsExceptionInterface(): void + { + $e = $this->createProblemDetailsExceptionWithAdditional(['foo' => 'bar']); + $stream = $this->createMock(StreamInterface::class); $this->preparePayloadForJsonResponse( $stream, function (array $payload) { @@ -220,44 +235,48 @@ function (array $payload) { } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(400)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(400)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); $factory = new ProblemDetailsResponseFactory(function () { - return $this->response->reveal(); + return $this->response; }); $response = $factory->createResponseFromThrowable( - $this->request->reveal(), - $e->reveal() + $this->request, + $e ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } /** * @dataProvider acceptHeaders */ - public function testCreateResponseRemovesResourcesFromInputData(string $header, string $expectedType) : void + public function testCreateResponseRemovesResourcesFromInputData(string $header, string $expectedType): void { - $this->request->getHeaderLine('Accept')->willReturn($header); + $this->request->method('getHeaderLine')->with('Accept')->willReturn($header); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $stream - ->write(Argument::that(function ($body) { + ->expects($this->atLeastOnce()) + ->method('write') + ->with($this->callback(function ($body) { Assert::assertNotEmpty($body); - return $body; - })) - ->shouldBeCalled(); + return true; + })); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', $expectedType)->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response->method('withHeader')->with('Content-Type', $expectedType)->willReturn($this->response); - $fh = fopen(__FILE__, 'r'); + $fh = fopen(__FILE__, 'r'); $response = $this->factory->createResponse( - $this->request->reveal(), + $this->request, 500, 'Unknown error occurred', 'Title', @@ -265,41 +284,42 @@ public function testCreateResponseRemovesResourcesFromInputData(string $header, [ 'args' => [ 'resource' => $fh, - ] + ], ] ); fclose($fh); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } - public function testFactoryGeneratesXmlResponseIfNegotiationFails() : void + public function testFactoryGeneratesXmlResponseIfNegotiationFails(): void { - $this->request->getHeaderLine('Accept')->willReturn('text/plain'); + $this->request->method('getHeaderLine')->with('Accept')->willReturn('text/plain'); - $stream = $this->prophesize(StreamInterface::class); - $stream - ->write(Argument::type('string')) - ->shouldBeCalled(); + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->atLeastOnce())->method('write')->with($this->isType('string')); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+xml')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+xml') + ->willReturn($this->response); $response = $this->factory->createResponse( - $this->request->reveal(), + $this->request, 500, 'Unknown error occurred' ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } - public function testFactoryRendersPreviousExceptionsInDebugMode() : void + public function testFactoryRendersPreviousExceptionsInDebugMode(): void { - $this->request->getHeaderLine('Accept')->willReturn('application/json'); + $this->request->method('getHeaderLine')->with('Accept')->willReturn('application/json'); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->preparePayloadForJsonResponse( $stream, function (array $payload) { @@ -307,62 +327,69 @@ function (array $payload) { Assert::assertEquals(101011, $payload['exception']['code']); Assert::assertEquals('second', $payload['exception']['message']); Assert::assertArrayHasKey('stack', $payload['exception']); - Assert::assertInternalType('array', $payload['exception']['stack']); + Assert::assertIsArray($payload['exception']['stack']); Assert::assertEquals(101010, $payload['exception']['stack'][0]['code']); Assert::assertEquals('first', $payload['exception']['stack'][0]['message']); } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); - $first = new RuntimeException('first', 101010); + $first = new RuntimeException('first', 101010); $second = new RuntimeException('second', 101011, $first); $factory = new ProblemDetailsResponseFactory( function () { - return $this->response->reveal(); + return $this->response; }, ProblemDetailsResponseFactory::INCLUDE_THROWABLE_DETAILS ); $response = $factory->createResponseFromThrowable( - $this->request->reveal(), + $this->request, $second ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } public function testFragileDataInExceptionMessageShouldBeHiddenInResponseBodyInNoDebugMode() { $fragileMessage = 'Your SQL or password here'; - $exception = new Exception($fragileMessage); + $exception = new Exception($fragileMessage); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $stream - ->write(Argument::that(function ($body) use ($fragileMessage) { - Assert::assertNotContains($fragileMessage, $body); - Assert::assertContains(ProblemDetailsResponseFactory::DEFAULT_DETAIL_MESSAGE, $body); - return $body; - })) - ->shouldBeCalled(); - - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); - - $response = $this->factory->createResponseFromThrowable($this->request->reveal(), $exception); - - $this->assertSame($this->response->reveal(), $response); + ->expects($this->atLeastOnce()) + ->method('write') + ->with($this->callback(function ($body) use ($fragileMessage) { + Assert::assertStringNotContainsString($fragileMessage, $body); + Assert::assertStringContainsString(ProblemDetailsResponseFactory::DEFAULT_DETAIL_MESSAGE, $body); + return true; + })); + + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); + + $response = $this->factory->createResponseFromThrowable($this->request, $exception); + + $this->assertSame($this->response, $response); } public function testExceptionCodeShouldBeIgnoredAnd500ServedInResponseBodyInNonDebugMode() { $exception = new Exception('', 400); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->preparePayloadForJsonResponse( $stream, function (array $payload) { @@ -370,21 +397,24 @@ function (array $payload) { } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); - $response = $this->factory->createResponseFromThrowable($this->request->reveal(), $exception); + $response = $this->factory->createResponseFromThrowable($this->request, $exception); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } public function testFragileDataInExceptionMessageShouldBeVisibleInResponseBodyInNonDebugModeWhenAllowToShowByFlag() { $fragileMessage = 'Your SQL or password here'; - $exception = new Exception($fragileMessage); + $exception = new Exception($fragileMessage); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->preparePayloadForJsonResponse( $stream, function (array $payload) use ($fragileMessage) { @@ -392,29 +422,32 @@ function (array $payload) use ($fragileMessage) { } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); $factory = new ProblemDetailsResponseFactory( function () { - return $this->response->reveal(); + return $this->response; }, false, null, true ); - $response = $factory->createResponseFromThrowable($this->request->reveal(), $exception); + $response = $factory->createResponseFromThrowable($this->request, $exception); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } public function testCustomDetailMessageShouldBeVisible() { $detailMessage = 'Custom detail message'; - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->preparePayloadForJsonResponse( $stream, function (array $payload) use ($detailMessage) { @@ -422,13 +455,16 @@ function (array $payload) use ($detailMessage) { } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(500)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(500)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); $factory = new ProblemDetailsResponseFactory( function () { - return $this->response->reveal(); + return $this->response; }, false, null, @@ -436,25 +472,20 @@ function () { $detailMessage ); - $response = $factory->createResponseFromThrowable($this->request->reveal(), new Exception()); + $response = $factory->createResponseFromThrowable($this->request, new Exception()); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } public function testRenderWithMalformedUtf8Sequences(): void { - $e = $this->prophesize(ProblemDetailsExceptionInterface::class); - $e->getStatus()->willReturn(400); - $e->getDetail()->willReturn('Exception details'); - $e->getTitle()->willReturn('Invalid client request'); - $e->getType()->willReturn('https://example.com/api/doc/invalid-client-request'); - $e->getAdditionalData()->willReturn([ + $e = $this->createProblemDetailsExceptionWithAdditional([ 'malformed-utf8' => self::UTF_8_INVALID_2_OCTET_SEQUENCE, ]); - $this->request->getHeaderLine('Accept')->willReturn('application/json'); + $this->request->method('getHeaderLine')->with('Accept')->willReturn('application/json'); - $stream = $this->prophesize(StreamInterface::class); + $stream = $this->createMock(StreamInterface::class); $this->preparePayloadForJsonResponse( $stream, function (array $payload) { @@ -462,23 +493,26 @@ function (array $payload) { } ); - $this->response->getBody()->will([$stream, 'reveal']); - $this->response->withStatus(400)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $this->response->method('getBody')->willReturn($stream); + $this->response->method('withStatus')->with(400)->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); $factory = new ProblemDetailsResponseFactory(function () { - return $this->response->reveal(); + return $this->response; }); $response = $factory->createResponseFromThrowable( - $this->request->reveal(), - $e->reveal() + $this->request, + $e ); - $this->assertSame($this->response->reveal(), $response); + $this->assertSame($this->response, $response); } - public function provideMappedStatuses() : array + public function provideMappedStatuses(): array { $defaultTypesMap = [ 404 => 'https://example.com/problem-details/error/not-found', @@ -496,25 +530,34 @@ public function provideMappedStatuses() : array /** * @dataProvider provideMappedStatuses */ - public function testTypeIsInferredFromDefaultTypesMap(array $map, int $status, string $expectedType) : void + public function testTypeIsInferredFromDefaultTypesMap(array $map, int $status, string $expectedType): void { - $this->request->getHeaderLine('Accept')->willReturn('application/json'); - - $stream = $this->prophesize(StreamInterface::class); - $writeStream = $stream->write(Argument::that(function (string $body) use ($expectedType) { - $payload = json_decode($body, true); - Assert::assertEquals($expectedType, $payload['type']); + $this->request->method('getHeaderLine')->with('Accept')->willReturn('application/json'); - return $body; - })); - - $this->response->getBody()->will([$stream, 'reveal']); - $withStatus = $this->response->withStatus($status)->will([$this->response, 'reveal']); - $this->response->withHeader('Content-Type', 'application/problem+json')->will([$this->response, 'reveal']); + $stream = $this->createMock(StreamInterface::class); + $stream + ->expects($this->atLeastOnce()) + ->method('write') + ->with($this->callback(function (string $body) use ($expectedType) { + $payload = json_decode($body, true); + Assert::assertEquals($expectedType, $payload['type']); + return true; + })); + + $this->response->method('getBody')->willReturn($stream); + $this->response + ->expects($this->atLeastOnce()) + ->method('withStatus') + ->with($status) + ->willReturn($this->response); + $this->response + ->method('withHeader') + ->with('Content-Type', 'application/problem+json') + ->willReturn($this->response); $factory = new ProblemDetailsResponseFactory( function () { - return $this->response->reveal(); + return $this->response; }, false, null, @@ -523,9 +566,6 @@ function () { $map ); - $factory->createResponse($this->request->reveal(), $status, 'detail'); - - $writeStream->shouldHaveBeenCalled(); - $withStatus->shouldHaveBeenCalled(); + $factory->createResponse($this->request, $status, 'detail'); } } diff --git a/test/TestAsset/RuntimeException.php b/test/TestAsset/RuntimeException.php index 33f284b..973b66b 100644 --- a/test/TestAsset/RuntimeException.php +++ b/test/TestAsset/RuntimeException.php @@ -16,11 +16,9 @@ class RuntimeException extends BaseRuntimeException { /** - * @param string $message * @param mixed $code Mimic PHP internal exceptions, and allow any code. - * @param Throwable $previous */ - public function __construct(string $message, $code = 0, Throwable $previous = null) + public function __construct(string $message, $code = 0, ?Throwable $previous = null) { parent::__construct($message, 0, $previous); $this->code = $code;