render($exception, $request); // THEN expect($response->getStatusCode())->toBe(404) ->and($response->headers->get('Content-Type'))->toBe('application/problem+json'); /** @var array $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 $config = resolve(AppConfig::class); $renderer = new ApiExceptionRenderer($config); $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 $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 $config = resolve(AppConfig::class); $renderer = new ApiExceptionRenderer($config); $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 $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 $config = resolve(AppConfig::class); $renderer = new ApiExceptionRenderer($config); $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 $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 $config = resolve(AppConfig::class); $renderer = new ApiExceptionRenderer($config); $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 $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); $config = resolve(AppConfig::class); $renderer = new ApiExceptionRenderer($config); $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 $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'); }); });