ldc-arcadia-api/tests/Integration/Presentation/Api/V1/ApiExceptionRendererTest.php

145 lines
6 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Integration\Presentation\Api\V1;
use Application\Shared\Exceptions\NotFoundException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Validation\ValidationException;
use Presentation\Api\V1\Exceptions\ApiExceptionRenderer;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
covers(ApiExceptionRenderer::class);
describe('V1 API Exception Renderer Integration', function (): void {
it('renders NotFoundException as 404 Problem Details JSON', function (): void {
// GIVEN
$renderer = new ApiExceptionRenderer();
$request = Request::create('http://localhost/api/v1/employees/999', 'GET');
$exception = new NotFoundException('Employee with ID 999 not found.');
// WHEN
$response = $renderer->render($exception, $request);
// THEN
expect($response->getStatusCode())->toBe(404)
->and($response->headers->get('Content-Type'))->toBe('application/problem+json');
/** @var array<string, mixed> $data */
$data = json_decode((string) $response->getContent(), true);
expect($data['status'])->toBe(404)
->and($data['title'])->toBe('Resource Not Found')
->and($data['detail'])->toBe('Employee with ID 999 not found.')
->and($data['type'])->toBe('http://localhost/errors/404')
->and($data['instance'])->toBe('/api/v1/employees/999');
});
it('renders ValidationException as 422 Problem Details JSON with invalidParams', function (): void {
// GIVEN
$renderer = new ApiExceptionRenderer();
$request = Request::create('http://localhost/api/v1/employees', 'POST');
$validator = validator(['email' => 'invalid'], ['email' => 'email|required']);
try {
$validator->validate();
} catch (ValidationException $exception) {
// WHEN
$response = $renderer->render($exception, $request);
// THEN
expect($response->getStatusCode())->toBe(422)
->and($response->headers->get('Content-Type'))->toBe('application/problem+json');
/** @var array<string, mixed> $data */
$data = json_decode((string) $response->getContent(), true);
expect($data['status'])->toBe(422)
->and($data['title'])->toBe('Unprocessable Entity')
->and($data['invalidParams'])->toBeArray()
->and($data['invalidParams'][0]['name'])->toBe('email');
}
});
it('renders DomainException as 422 Problem Details JSON', function (): void {
// GIVEN
$renderer = new ApiExceptionRenderer();
$request = Request::create('http://localhost/api/v1/employees', 'POST');
$exception = new \DomainException('Invalid employee number format');
// WHEN
$response = $renderer->render($exception, $request);
// THEN
expect($response->getStatusCode())->toBe(422)
->and($response->headers->get('Content-Type'))->toBe('application/problem+json');
/** @var array<string, mixed> $data */
$data = json_decode((string) $response->getContent(), true);
expect($data['status'])->toBe(422)
->and($data['title'])->toBe('Unprocessable Entity')
->and($data['invalidParams'][0]['name'])->toBe('domain_invariant')
->and($data['invalidParams'][0]['reason'])->toBe('Invalid employee number format');
});
it('renders ThrottleRequestsException as 429 Problem Details JSON with rate headers', function (): void {
// GIVEN
$renderer = new ApiExceptionRenderer();
$request = Request::create('http://localhost/api/v1/employees', 'GET');
$exception = new ThrottleRequestsException('Too Many Attempts.', null, ['Retry-After' => '60']);
// WHEN
$response = $renderer->render($exception, $request);
// THEN
expect($response->getStatusCode())->toBe(429)
->and($response->headers->get('Content-Type'))->toBe('application/problem+json')
->and($response->headers->get('Retry-After'))->toBe('60');
/** @var array<string, mixed> $data */
$data = json_decode((string) $response->getContent(), true);
expect($data['status'])->toBe(429)
->and($data['title'])->toBe('Too Many Requests');
});
it('renders HttpExceptionInterface exception with status code and detail', function (): void {
// GIVEN
$renderer = new ApiExceptionRenderer();
$request = Request::create('http://localhost/api/v1/employees', 'DELETE');
$exception = new AccessDeniedHttpException('Access denied');
// WHEN
$response = $renderer->render($exception, $request);
// THEN
expect($response->getStatusCode())->toBe(403)
->and($response->headers->get('Content-Type'))->toBe('application/problem+json');
/** @var array<string, mixed> $data */
$data = json_decode((string) $response->getContent(), true);
expect($data['status'])->toBe(403)
->and($data['title'])->toBe('Forbidden')
->and($data['detail'])->toBe('Access denied');
});
it('renders generic Throwable as 500 Problem Details JSON with debug info based on config', function (): void {
// GIVEN
Config::set('app.debug', true);
$renderer = new ApiExceptionRenderer();
$request = Request::create('http://localhost/api/v1/employees', 'GET');
$exception = new \RuntimeException('Database connection failure');
// WHEN
$response = $renderer->render($exception, $request);
// THEN
expect($response->getStatusCode())->toBe(500);
/** @var array<string, mixed> $data */
$data = json_decode((string) $response->getContent(), true);
expect($data['status'])->toBe(500)
->and($data['title'])->toBe('Internal Server Error')
->and($data['detail'])->toBe('Database connection failure');
});
});