Skip to content

Commit b7fd4d2

Browse files
authored
Merge pull request #7 from pedrosalpr/feature/improvements-package
Feature/improvements package
2 parents 9bd42e7 + ee4ff27 commit b7fd4d2

File tree

12 files changed

+298
-42
lines changed

12 files changed

+298
-42
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
matrix:
1919
os: [ubuntu-latest, windows-latest]
2020
php: [8.3, 8.2, 8.1]
21-
laravel: [10.*]
21+
laravel: [10.*, 11.*, 12.*]
2222
stability: [prefer-lowest, prefer-stable]
2323
include:
2424
- laravel: 10.*

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
],
1919
"require": {
2020
"php": "^8.1",
21-
"illuminate/contracts": "^10.0||^11",
21+
"illuminate/contracts": "^10.0||^11||^12",
2222
"spatie/laravel-package-tools": "^1.14.0"
2323
},
2424
"require-dev": {

src/Commands/LaravelApiProblemCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protected function resolveStubPath(string $stub): string
2323
{
2424
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
2525
? $customPath
26-
: __DIR__ . $stub;
26+
: __DIR__.$stub;
2727
}
2828

2929
protected function getDefaultNamespace($rootNamespace): string

src/Commands/LaravelApiProblemExceptionCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ protected function resolveStubPath(string $stub): string
2424
{
2525
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
2626
? $customPath
27-
: __DIR__ . $stub;
27+
: __DIR__.$stub;
2828
}
2929

3030
protected function getDefaultNamespace($rootNamespace): string
@@ -34,7 +34,7 @@ protected function getDefaultNamespace($rootNamespace): string
3434

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

3939
// Do string replacement
4040
return str_replace('{{ class }}', $class, $stub);

src/Http/LaravelHttpApiProblem.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function __construct(
7272
if ($this->statusCode < 400 || $this->statusCode > 599) {
7373
$this->statusCode = 400;
7474
}
75-
if (!filter_var($this->type, FILTER_VALIDATE_URL) || empty($this->title)) {
75+
if (! filter_var($this->type, FILTER_VALIDATE_URL) || empty($this->title)) {
7676
$this->title = $this->getTitleForStatusCode($this->statusCode);
7777
$this->type = self::TYPE_ABOUT_BLANK;
7878
}
@@ -99,6 +99,11 @@ public function getStatusCode(): int
9999
return $this->statusCode;
100100
}
101101

102+
public function getInstance(): string
103+
{
104+
return $this->instance;
105+
}
106+
102107
public function getExtensions(): array
103108
{
104109
return $this->extensions;
@@ -113,7 +118,7 @@ public function toArray(): array
113118
'title' => $this->title,
114119
'detail' => $this->detail,
115120
'instance' => $this->instance,
116-
'timestamp' => $this->timestamp->toJSON()
121+
'timestamp' => $this->timestamp->toJSON(),
117122
],
118123
$this->extensions
119124
);

src/LaravelApiProblem.php

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Pedrosalpr\LaravelApiProblem;
66

7+
use Illuminate\Auth\Access\AuthorizationException;
78
use Illuminate\Http\JsonResponse;
89
use Illuminate\Http\Request;
910
use Illuminate\Http\Response;
@@ -26,20 +27,20 @@ public function __construct(
2627
if ($exception instanceof LaravelApiProblemException) {
2728
$this->apiProblemException = $exception;
2829
$this->apiProblemException();
30+
2931
return;
3032
}
3133
match (get_class($exception)) {
3234
ValidationException::class => $this->validation(),
3335
\UnhandledMatchError::class,\Exception::class => $this->default(),
3436
HttpException::class => $this->default(419),
37+
AuthorizationException::class => $this->default(403),
3538
default => $this->default()
3639
};
3740
}
3841

3942
/**
4043
* Render the exception as an HTTP response.
41-
*
42-
* @return JsonResponse
4344
*/
4445
public function render(): JsonResponse
4546
{
@@ -54,8 +55,6 @@ public function render(): JsonResponse
5455

5556
/**
5657
* Debug the class in array to view more details, such as: api problem and exception
57-
*
58-
* @return array
5958
*/
6059
public function toDebuggableArray(): array
6160
{
@@ -80,8 +79,6 @@ public function toDebuggableArray(): array
8079

8180
/**
8281
* Transform any exception into an http api problem with status code
83-
*
84-
* @param null|int $statusCode
8582
*/
8683
protected function default(?int $statusCode = null): void
8784
{
@@ -103,7 +100,7 @@ protected function validation(): void
103100
$extensions = [
104101
'errors' => ($this->exception instanceof ValidationException)
105102
? $this->exception->errors()
106-
: null
103+
: null,
107104
];
108105
$this->apiProblem = new LaravelHttpApiProblem(
109106
Response::HTTP_UNPROCESSABLE_ENTITY,
@@ -130,8 +127,6 @@ protected function apiProblemException(): void
130127

131128
/**
132129
* Get uri as instance
133-
*
134-
* @return string
135130
*/
136131
protected function getUriInstance(): string
137132
{
@@ -140,30 +135,25 @@ protected function getUriInstance(): string
140135

141136
/**
142137
* Get the context if it exists within the exception and return it as an extension
143-
*
144-
* @return array
145138
*/
146139
protected function getContextExceptionAsExtensions(): array
147140
{
148141
$extensions = [];
149-
if (!method_exists($this->exception, 'context')) {
142+
if (! method_exists($this->exception, 'context')) {
150143
return $extensions;
151144
}
152145
$context = $this->exception->context();
153146
if (is_array($context)) {
154147
$extensions = $context;
155-
} elseif (!empty($context)) {
148+
} elseif (! empty($context)) {
156149
$extensions = [$context];
157150
}
151+
158152
return $extensions;
159153
}
160154

161155
/**
162156
* Gets the status code from the exception code, or from the HttpException Interface, otherwise it returns an Internal Server Error
163-
*
164-
* @param null|int $code
165-
*
166-
* @return int
167157
*/
168158
protected function getStatusCode(?int $code): int
169159
{
@@ -175,17 +165,14 @@ protected function getStatusCode(?int $code): int
175165
? $this->exception->getStatusCode()
176166
: Response::HTTP_INTERNAL_SERVER_ERROR;
177167
}
168+
178169
return ($this->isStatusCodeInternalOrServerError($this->exception->getCode()))
179170
? $this->exception->getCode()
180171
: Response::HTTP_INTERNAL_SERVER_ERROR;
181172
}
182173

183174
/**
184175
* Checks if the status code is of the integer type and is in the range of Client and Server Errors
185-
*
186-
* @param null|int $statusCode
187-
*
188-
* @return bool
189176
*/
190177
protected function isStatusCodeInternalOrServerError(?int $statusCode): bool
191178
{
@@ -194,10 +181,6 @@ protected function isStatusCodeInternalOrServerError(?int $statusCode): bool
194181

195182
/**
196183
* Serialize the exception into an array
197-
*
198-
* @param \Throwable $throwable
199-
*
200-
* @return array
201184
*/
202185
private function serializeException(\Throwable $throwable): array
203186
{

src/LaravelApiProblemInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ public function getDetail(): string;
1919
public function getExtensions(): array;
2020

2121
public function getHeaderProblemJson(): string;
22+
23+
public function getInstance(): string;
2224
}

tests/ArchTest.php

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/ExampleTest.php

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/Feature/ApiProblemTest.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Auth\Access\AuthorizationException;
6+
use Illuminate\Auth\AuthenticationException;
7+
use Illuminate\Http\Response;
8+
use Illuminate\Support\Facades\Route;
9+
use Pedrosalpr\LaravelApiProblem\Tests\Handlers\TestExceptionHandler;
10+
use Symfony\Component\HttpKernel\Exception\HttpException;
11+
12+
use function Pest\Laravel\getJson;
13+
use function Pest\Laravel\postJson;
14+
15+
// Antes de cada teste, substitua o manipulador de exceções do Laravel
16+
// pelo seu manipulador de exceções de teste.
17+
beforeEach(function () {
18+
$this->app->singleton(
19+
\Illuminate\Contracts\Debug\ExceptionHandler::class,
20+
TestExceptionHandler::class // Use sua classe de handler de teste aqui
21+
);
22+
});
23+
24+
test('authentication exception returns 401 problem json', function () {
25+
// Definir uma rota que lança a exceção diretamente.
26+
Route::get('/api/protected', fn () => throw new AuthenticationException('Unauthenticated.'));
27+
28+
getJson('/api/protected')
29+
->assertStatus(Response::HTTP_UNAUTHORIZED)
30+
->assertHeader('Content-Type', 'application/problem+json')
31+
->assertJson([
32+
'status' => Response::HTTP_UNAUTHORIZED,
33+
'title' => 'Unauthorized',
34+
'detail' => 'Unauthenticated.',
35+
]);
36+
});
37+
38+
test('authorization exception returns 403 problem json', function () {
39+
// Definir uma rota que lança a exceção diretamente.
40+
Route::get('/api/forbidden', fn () => throw new AuthorizationException('This action is unauthorized.'));
41+
42+
getJson('/api/forbidden')
43+
->assertStatus(Response::HTTP_FORBIDDEN)
44+
->assertHeader('Content-Type', 'application/problem+json')
45+
->assertJson([
46+
'status' => Response::HTTP_FORBIDDEN,
47+
'title' => 'Forbidden',
48+
'detail' => 'This action is unauthorized.',
49+
]);
50+
});
51+
52+
test('validation exception returns 422 problem json', function () {
53+
Route::post('/api/validate', function () {
54+
request()->validate(['email' => 'required|email']);
55+
});
56+
57+
postJson('/api/validate', ['email' => 'invalid-email'])
58+
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
59+
->assertHeader('Content-Type', 'application/problem+json')
60+
->assertJson([
61+
'status' => Response::HTTP_UNPROCESSABLE_ENTITY,
62+
'title' => 'Unprocessable Entity',
63+
'detail' => 'The given data was invalid.',
64+
])
65+
->assertJsonStructure([
66+
'status',
67+
'title',
68+
'detail',
69+
'errors' => [
70+
'email',
71+
],
72+
]);
73+
});
74+
75+
test('not found exception returns 404 problem json', function () {
76+
// A rota não é definida, o Laravel lança a exceção automaticamente
77+
getJson('/api/non-existent-route')
78+
->assertStatus(404)
79+
->assertHeader('Content-Type', 'application/problem+json')
80+
->assertJson([
81+
'status' => 404,
82+
'title' => 'Not Found',
83+
'detail' => 'The route api/non-existent-route could not be found.',
84+
]);
85+
});
86+
87+
test('method not allowed exception returns 405 problem json', function () {
88+
// Definir uma rota que só aceita POST
89+
Route::post('/api/only-post', fn () => ['message' => 'success']);
90+
91+
// Tentar acessar com o método GET
92+
getJson('/api/only-post')
93+
->assertStatus(Response::HTTP_METHOD_NOT_ALLOWED)
94+
->assertHeader('Content-Type', 'application/problem+json')
95+
->assertJson([
96+
'status' => Response::HTTP_METHOD_NOT_ALLOWED,
97+
'title' => 'Method Not Allowed',
98+
'detail' => 'The GET method is not supported for route api/only-post. Supported methods: POST.',
99+
]);
100+
});
101+
102+
test('generic exception returns 500 problem json', function () {
103+
Route::get('/api/internal-error', function () {
104+
throw new Exception('An internal server error occurred.');
105+
});
106+
107+
getJson('/api/internal-error')
108+
->assertStatus(500)
109+
->assertHeader('Content-Type', 'application/problem+json')
110+
->assertJson([
111+
'status' => 500,
112+
'title' => 'Internal Server Error',
113+
'detail' => 'An internal server error occurred.',
114+
]);
115+
});
116+
117+
test('http exception returns correct problem json', function () {
118+
Route::get('/api/custom-error', function () {
119+
throw new HttpException(Response::HTTP_UNAUTHORIZED, 'You do not have permission.');
120+
});
121+
122+
getJson('/api/custom-error')
123+
->assertStatus(Response::HTTP_UNAUTHORIZED)
124+
->assertHeader('Content-Type', 'application/problem+json')
125+
->assertJson([
126+
'status' => Response::HTTP_UNAUTHORIZED,
127+
'title' => 'Unauthorized',
128+
'detail' => 'You do not have permission.',
129+
]);
130+
});

0 commit comments

Comments
 (0)