Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.3, 8.2, 8.1]
laravel: [10.*]
laravel: [10.*, 11.*, 12.*]
stability: [prefer-lowest, prefer-stable]
include:
- laravel: 10.*
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
],
"require": {
"php": "^8.1",
"illuminate/contracts": "^10.0||^11",
"illuminate/contracts": "^10.0||^11||^12",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/LaravelApiProblemCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected function resolveStubPath(string $stub): string
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__ . $stub;
: __DIR__.$stub;
}

protected function getDefaultNamespace($rootNamespace): string
Expand Down
4 changes: 2 additions & 2 deletions src/Commands/LaravelApiProblemExceptionCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ protected function resolveStubPath(string $stub): string
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__ . $stub;
: __DIR__.$stub;
}

protected function getDefaultNamespace($rootNamespace): string
Expand All @@ -34,7 +34,7 @@ protected function getDefaultNamespace($rootNamespace): string

protected function replaceClass($stub, $name)
{
$class = str_replace($this->getNamespace($name) . '\\', '', $name);
$class = str_replace($this->getNamespace($name).'\\', '', $name);

// Do string replacement
return str_replace('{{ class }}', $class, $stub);
Expand Down
9 changes: 7 additions & 2 deletions src/Http/LaravelHttpApiProblem.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function __construct(
if ($this->statusCode < 400 || $this->statusCode > 599) {
$this->statusCode = 400;
}
if (!filter_var($this->type, FILTER_VALIDATE_URL) || empty($this->title)) {
if (! filter_var($this->type, FILTER_VALIDATE_URL) || empty($this->title)) {
$this->title = $this->getTitleForStatusCode($this->statusCode);
$this->type = self::TYPE_ABOUT_BLANK;
}
Expand All @@ -99,6 +99,11 @@ public function getStatusCode(): int
return $this->statusCode;
}

public function getInstance(): string
{
return $this->instance;
}

public function getExtensions(): array
{
return $this->extensions;
Expand All @@ -113,7 +118,7 @@ public function toArray(): array
'title' => $this->title,
'detail' => $this->detail,
'instance' => $this->instance,
'timestamp' => $this->timestamp->toJSON()
'timestamp' => $this->timestamp->toJSON(),
],
$this->extensions
);
Expand Down
33 changes: 8 additions & 25 deletions src/LaravelApiProblem.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Pedrosalpr\LaravelApiProblem;

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
Expand All @@ -26,20 +27,20 @@ public function __construct(
if ($exception instanceof LaravelApiProblemException) {
$this->apiProblemException = $exception;
$this->apiProblemException();

return;
}
match (get_class($exception)) {
ValidationException::class => $this->validation(),
\UnhandledMatchError::class,\Exception::class => $this->default(),
HttpException::class => $this->default(419),
AuthorizationException::class => $this->default(403),
default => $this->default()
};
}

/**
* Render the exception as an HTTP response.
*
* @return JsonResponse
*/
public function render(): JsonResponse
{
Expand All @@ -54,8 +55,6 @@ public function render(): JsonResponse

/**
* Debug the class in array to view more details, such as: api problem and exception
*
* @return array
*/
public function toDebuggableArray(): array
{
Expand All @@ -80,8 +79,6 @@ public function toDebuggableArray(): array

/**
* Transform any exception into an http api problem with status code
*
* @param null|int $statusCode
*/
protected function default(?int $statusCode = null): void
{
Expand All @@ -103,7 +100,7 @@ protected function validation(): void
$extensions = [
'errors' => ($this->exception instanceof ValidationException)
? $this->exception->errors()
: null
: null,
];
$this->apiProblem = new LaravelHttpApiProblem(
Response::HTTP_UNPROCESSABLE_ENTITY,
Expand All @@ -130,8 +127,6 @@ protected function apiProblemException(): void

/**
* Get uri as instance
*
* @return string
*/
protected function getUriInstance(): string
{
Expand All @@ -140,30 +135,25 @@ protected function getUriInstance(): string

/**
* Get the context if it exists within the exception and return it as an extension
*
* @return array
*/
protected function getContextExceptionAsExtensions(): array
{
$extensions = [];
if (!method_exists($this->exception, 'context')) {
if (! method_exists($this->exception, 'context')) {
return $extensions;
}
$context = $this->exception->context();
if (is_array($context)) {
$extensions = $context;
} elseif (!empty($context)) {
} elseif (! empty($context)) {
$extensions = [$context];
}

return $extensions;
}

/**
* Gets the status code from the exception code, or from the HttpException Interface, otherwise it returns an Internal Server Error
*
* @param null|int $code
*
* @return int
*/
protected function getStatusCode(?int $code): int
{
Expand All @@ -175,17 +165,14 @@ protected function getStatusCode(?int $code): int
? $this->exception->getStatusCode()
: Response::HTTP_INTERNAL_SERVER_ERROR;
}

return ($this->isStatusCodeInternalOrServerError($this->exception->getCode()))
? $this->exception->getCode()
: Response::HTTP_INTERNAL_SERVER_ERROR;
}

/**
* Checks if the status code is of the integer type and is in the range of Client and Server Errors
*
* @param null|int $statusCode
*
* @return bool
*/
protected function isStatusCodeInternalOrServerError(?int $statusCode): bool
{
Expand All @@ -194,10 +181,6 @@ protected function isStatusCodeInternalOrServerError(?int $statusCode): bool

/**
* Serialize the exception into an array
*
* @param \Throwable $throwable
*
* @return array
*/
private function serializeException(\Throwable $throwable): array
{
Expand Down
2 changes: 2 additions & 0 deletions src/LaravelApiProblemInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public function getDetail(): string;
public function getExtensions(): array;

public function getHeaderProblemJson(): string;

public function getInstance(): string;
}
5 changes: 0 additions & 5 deletions tests/ArchTest.php

This file was deleted.

5 changes: 0 additions & 5 deletions tests/ExampleTest.php

This file was deleted.

130 changes: 130 additions & 0 deletions tests/Feature/ApiProblemTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use Pedrosalpr\LaravelApiProblem\Tests\Handlers\TestExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;

use function Pest\Laravel\getJson;
use function Pest\Laravel\postJson;

// Antes de cada teste, substitua o manipulador de exceções do Laravel
// pelo seu manipulador de exceções de teste.
beforeEach(function () {
$this->app->singleton(
\Illuminate\Contracts\Debug\ExceptionHandler::class,
TestExceptionHandler::class // Use sua classe de handler de teste aqui
);
});

test('authentication exception returns 401 problem json', function () {
// Definir uma rota que lança a exceção diretamente.
Route::get('/api/protected', fn () => throw new AuthenticationException('Unauthenticated.'));

getJson('/api/protected')
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => Response::HTTP_UNAUTHORIZED,
'title' => 'Unauthorized',
'detail' => 'Unauthenticated.',
]);
});

test('authorization exception returns 403 problem json', function () {
// Definir uma rota que lança a exceção diretamente.
Route::get('/api/forbidden', fn () => throw new AuthorizationException('This action is unauthorized.'));

getJson('/api/forbidden')
->assertStatus(Response::HTTP_FORBIDDEN)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => Response::HTTP_FORBIDDEN,
'title' => 'Forbidden',
'detail' => 'This action is unauthorized.',
]);
});

test('validation exception returns 422 problem json', function () {
Route::post('/api/validate', function () {
request()->validate(['email' => 'required|email']);
});

postJson('/api/validate', ['email' => 'invalid-email'])
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => Response::HTTP_UNPROCESSABLE_ENTITY,
'title' => 'Unprocessable Entity',
'detail' => 'The given data was invalid.',
])
->assertJsonStructure([
'status',
'title',
'detail',
'errors' => [
'email',
],
]);
});

test('not found exception returns 404 problem json', function () {
// A rota não é definida, o Laravel lança a exceção automaticamente
getJson('/api/non-existent-route')
->assertStatus(404)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => 404,
'title' => 'Not Found',
'detail' => 'The route api/non-existent-route could not be found.',
]);
});

test('method not allowed exception returns 405 problem json', function () {
// Definir uma rota que só aceita POST
Route::post('/api/only-post', fn () => ['message' => 'success']);

// Tentar acessar com o método GET
getJson('/api/only-post')
->assertStatus(Response::HTTP_METHOD_NOT_ALLOWED)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => Response::HTTP_METHOD_NOT_ALLOWED,
'title' => 'Method Not Allowed',
'detail' => 'The GET method is not supported for route api/only-post. Supported methods: POST.',
]);
});

test('generic exception returns 500 problem json', function () {
Route::get('/api/internal-error', function () {
throw new Exception('An internal server error occurred.');
});

getJson('/api/internal-error')
->assertStatus(500)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => 500,
'title' => 'Internal Server Error',
'detail' => 'An internal server error occurred.',
]);
});

test('http exception returns correct problem json', function () {
Route::get('/api/custom-error', function () {
throw new HttpException(Response::HTTP_UNAUTHORIZED, 'You do not have permission.');
});

getJson('/api/custom-error')
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertHeader('Content-Type', 'application/problem+json')
->assertJson([
'status' => Response::HTTP_UNAUTHORIZED,
'title' => 'Unauthorized',
'detail' => 'You do not have permission.',
]);
});
Loading