test(ApiExceptionDispatcher): add integration tests for exception handling
Added integration tests for ApiExceptionDispatcher to ensure proper handling of exceptions for various API request scenarios, including JSON acceptance and unversioned requests.
This commit is contained in:
parent
8e96abc88d
commit
52d0e6f0fd
2 changed files with 226 additions and 0 deletions
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration\Presentation\Api;
|
||||
|
||||
use Domain\Exceptions\NotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Presentation\Api\Exceptions\ApiExceptionDispatcher;
|
||||
use Presentation\Api\V1\Exceptions\ApiExceptionRenderer as V1ApiExceptionRenderer;
|
||||
|
||||
covers(ApiExceptionDispatcher::class);
|
||||
|
||||
describe('API Exception Dispatcher Integration', function (): void {
|
||||
it('returns null for non-API web requests that do not accept JSON', function (): void {
|
||||
// GIVEN
|
||||
$dispatcher = new ApiExceptionDispatcher(new V1ApiExceptionRenderer());
|
||||
$request = Request::create('/some-web-route', 'GET');
|
||||
$exception = new NotFoundException('Not found');
|
||||
|
||||
// WHEN
|
||||
$response = $dispatcher->render($exception, $request);
|
||||
|
||||
// THEN
|
||||
expect($response)->toBeNull();
|
||||
});
|
||||
|
||||
it('dispatches api/v1 requests to V1ApiExceptionRenderer', function (): void {
|
||||
// GIVEN
|
||||
$dispatcher = new ApiExceptionDispatcher(new V1ApiExceptionRenderer());
|
||||
$request = Request::create('http://localhost/api/v1/employees/999', 'GET');
|
||||
$exception = new NotFoundException('Employee not found');
|
||||
|
||||
// WHEN
|
||||
$response = $dispatcher->render($exception, $request);
|
||||
|
||||
// THEN
|
||||
expect($response)->not->toBeNull()
|
||||
->and($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 not found');
|
||||
});
|
||||
|
||||
it('falls back to V1ApiExceptionRenderer for unversioned api requests', function (): void {
|
||||
// GIVEN
|
||||
$dispatcher = new ApiExceptionDispatcher(new V1ApiExceptionRenderer());
|
||||
$request = Request::create('http://localhost/api/ping', 'GET');
|
||||
$exception = new \DomainException('Domain invariant error');
|
||||
|
||||
// WHEN
|
||||
$response = $dispatcher->render($exception, $request);
|
||||
|
||||
// THEN
|
||||
expect($response)->not->toBeNull()
|
||||
->and($response?->getStatusCode())->toBe(422);
|
||||
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = json_decode((string) $response?->getContent(), true);
|
||||
expect($data['status'])->toBe(422)
|
||||
->and($data['detail'])->toBe('Domain invariant error');
|
||||
});
|
||||
|
||||
it('dispatches to V1ApiExceptionRenderer when request accepts JSON even on non-api path', function (): void {
|
||||
// GIVEN
|
||||
$dispatcher = new ApiExceptionDispatcher(new V1ApiExceptionRenderer());
|
||||
$request = Request::create('http://localhost/custom-endpoint', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/json']);
|
||||
$exception = new NotFoundException('Custom endpoint not found');
|
||||
|
||||
// WHEN
|
||||
$response = $dispatcher->render($exception, $request);
|
||||
|
||||
// THEN
|
||||
expect($response)->not->toBeNull()
|
||||
->and($response?->getStatusCode())->toBe(404);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration\Presentation\Api\V1;
|
||||
|
||||
use Domain\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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue