Skip to content

Commit 7032169

Browse files
committed
test(laravel-api-problem): add feature and unit tests, and delete default tests package
1 parent f255a98 commit 7032169

File tree

5 files changed

+276
-10
lines changed

5 files changed

+276
-10
lines changed

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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use function Pest\Laravel\{getJson, postJson};
6+
use Illuminate\Auth\AuthenticationException;
7+
use Illuminate\Auth\Access\AuthorizationException;
8+
use Illuminate\Http\Response;
9+
use Illuminate\Support\Facades\Route;
10+
use Pedrosalpr\LaravelApiProblem\Tests\Handlers\TestExceptionHandler;
11+
use Symfony\Component\HttpKernel\Exception\HttpException;
12+
13+
// Antes de cada teste, substitua o manipulador de exceções do Laravel
14+
// pelo seu manipulador de exceções de teste.
15+
beforeEach(function () {
16+
$this->app->singleton(
17+
\Illuminate\Contracts\Debug\ExceptionHandler::class,
18+
TestExceptionHandler::class // Use sua classe de handler de teste aqui
19+
);
20+
});
21+
22+
test('authentication exception returns 401 problem json', function () {
23+
// Definir uma rota que lança a exceção diretamente.
24+
Route::get('/api/protected', fn () => throw new AuthenticationException('Unauthenticated.'));
25+
26+
getJson('/api/protected')
27+
->assertStatus(Response::HTTP_UNAUTHORIZED)
28+
->assertHeader('Content-Type', 'application/problem+json')
29+
->assertJson([
30+
'status' => Response::HTTP_UNAUTHORIZED,
31+
'title' => 'Unauthorized',
32+
'detail' => 'Unauthenticated.',
33+
]);
34+
});
35+
36+
test('authorization exception returns 403 problem json', function () {
37+
// Definir uma rota que lança a exceção diretamente.
38+
Route::get('/api/forbidden', fn () => throw new AuthorizationException('This action is unauthorized.'));
39+
40+
getJson('/api/forbidden')
41+
->assertStatus(Response::HTTP_FORBIDDEN)
42+
->assertHeader('Content-Type', 'application/problem+json')
43+
->assertJson([
44+
'status' => Response::HTTP_FORBIDDEN,
45+
'title' => 'Forbidden',
46+
'detail' => 'This action is unauthorized.',
47+
]);
48+
});
49+
50+
test('validation exception returns 422 problem json', function () {
51+
Route::post('/api/validate', function () {
52+
request()->validate(['email' => 'required|email']);
53+
});
54+
55+
postJson('/api/validate', ['email' => 'invalid-email'])
56+
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
57+
->assertHeader('Content-Type', 'application/problem+json')
58+
->assertJson([
59+
'status' => Response::HTTP_UNPROCESSABLE_ENTITY,
60+
'title' => 'Unprocessable Entity',
61+
'detail' => 'The given data was invalid.',
62+
])
63+
->assertJsonStructure([
64+
'status',
65+
'title',
66+
'detail',
67+
'errors' => [
68+
'email',
69+
],
70+
]);
71+
});
72+
73+
test('not found exception returns 404 problem json', function () {
74+
// A rota não é definida, o Laravel lança a exceção automaticamente
75+
getJson('/api/non-existent-route')
76+
->assertStatus(404)
77+
->assertHeader('Content-Type', 'application/problem+json')
78+
->assertJson([
79+
'status' => 404,
80+
'title' => 'Not Found',
81+
'detail' => 'The route api/non-existent-route could not be found.',
82+
]);
83+
});
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+
118+
119+
test('http exception returns correct problem json', function () {
120+
Route::get('/api/custom-error', function () {
121+
throw new HttpException(Response::HTTP_UNAUTHORIZED, 'You do not have permission.');
122+
});
123+
124+
getJson('/api/custom-error')
125+
->assertStatus(Response::HTTP_UNAUTHORIZED)
126+
->assertHeader('Content-Type', 'application/problem+json')
127+
->assertJson([
128+
'status' => Response::HTTP_UNAUTHORIZED,
129+
'title' => 'Unauthorized',
130+
'detail' => 'You do not have permission.',
131+
]);
132+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pedrosalpr\LaravelApiProblem\Tests\Handlers;
6+
7+
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
8+
use Illuminate\Http\Response;
9+
use Illuminate\Auth\AuthenticationException;
10+
use Illuminate\Auth\Access\AuthorizationException;
11+
use Illuminate\Validation\ValidationException;
12+
use Pedrosalpr\LaravelApiProblem\Http\LaravelHttpApiProblem;
13+
use Throwable;
14+
15+
class TestExceptionHandler extends ExceptionHandler
16+
{
17+
public function render($request, Throwable $e)
18+
{
19+
// Define um valor de instância padrão para todos os problemas
20+
$instance = $request->fullUrl();
21+
22+
if ($e instanceof AuthenticationException) {
23+
$problem = new LaravelHttpApiProblem(Response::HTTP_UNAUTHORIZED, $e->getMessage(), $instance);
24+
return response()->json($problem->toArray(), $problem->getStatusCode())
25+
->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]);
26+
}
27+
28+
if ($e instanceof AuthorizationException) {
29+
$problem = new LaravelHttpApiProblem(Response::HTTP_FORBIDDEN, $e->getMessage(), $instance);
30+
return response()->json($problem->toArray(), $problem->getStatusCode())
31+
->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]);
32+
}
33+
34+
// As exceções Http (como 404) precisam ser tratadas para que o cabeçalho seja `application/problem+json`
35+
if ($this->isHttpException($e)) {
36+
$statusCode = $e->getStatusCode();
37+
$title = Response::$statusTexts[$statusCode] ?? 'Unknown Error';
38+
$problem = new LaravelHttpApiProblem($statusCode, $e->getMessage(), $instance, title: $title);
39+
return response()->json($problem->toArray(), $problem->getStatusCode())
40+
->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]);
41+
}
42+
43+
if ($e instanceof ValidationException) {
44+
$problem = new LaravelHttpApiProblem(
45+
statusCode: Response::HTTP_UNPROCESSABLE_ENTITY,
46+
detail: 'The given data was invalid.',
47+
instance: $instance,
48+
extensions: ['errors' => $e->errors()]
49+
);
50+
return response()->json($problem->toArray(), $problem->getStatusCode())
51+
->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]);
52+
}
53+
54+
$problem = new LaravelHttpApiProblem(
55+
statusCode: Response::HTTP_INTERNAL_SERVER_ERROR,
56+
detail: 'An internal server error occurred.', // Mensagem genérica por segurança
57+
instance: $instance
58+
);
59+
60+
return response()->json($problem->toArray(), $problem->getStatusCode())
61+
->withHeaders(['Content-Type' => $problem->getHeaderProblemJson()]);
62+
}
63+
}

tests/Unit/ApiProblemTest.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Pedrosalpr\LaravelApiProblem\Http\LaravelHttpApiProblem;
6+
use Pedrosalpr\LaravelApiProblem\Tests\TestCase;
7+
use function Pest\Laravel\{getJson, postJson};
8+
9+
test('it can be instantiated with required arguments', function () {
10+
$problem = new LaravelHttpApiProblem(
11+
statusCode: 404,
12+
detail: 'The resource was not found.',
13+
instance: 'https://example.com/api/posts/1'
14+
);
15+
16+
expect($problem->getStatusCode())->toBe(404);
17+
expect($problem->getDetail())->toBe('The resource was not found.');
18+
expect($problem->getInstance())->toBe('https://example.com/api/posts/1');
19+
expect($problem->getTitle())->toBe('Not Found');
20+
expect($problem->getType())->toBe('about:blank');
21+
});
22+
23+
test('it sets title and type automatically based on status code', function () {
24+
$problem = new LaravelHttpApiProblem(
25+
statusCode: 401,
26+
detail: 'Authentication failed.',
27+
instance: 'https://example.com/api/protected'
28+
);
29+
30+
expect($problem->getTitle())->toBe('Unauthorized');
31+
expect($problem->getType())->toBe('about:blank');
32+
});
33+
34+
test('toArray method returns valid problem json structure', function () {
35+
$problem = new LaravelHttpApiProblem(
36+
statusCode: 422,
37+
detail: 'The request payload is invalid.',
38+
instance: 'https://example.com/api/users',
39+
extensions: ['errors' => ['name' => ['The name field is required.']]]
40+
);
41+
42+
$array = $problem->toArray();
43+
44+
expect($array)
45+
->toHaveKeys(['status', 'type', 'title', 'detail', 'instance', 'timestamp', 'errors'])
46+
->and($array['status'])->toBe(422)
47+
->and($array['title'])->toBe('Unprocessable Entity')
48+
->and($array['errors']['name'])->toBe(['The name field is required.']);
49+
});
50+
51+
test('it defaults status code to 400 if out of range', function (int $statusCode) {
52+
$problem = new LaravelHttpApiProblem(
53+
statusCode: $statusCode,
54+
detail: 'Invalid status code.',
55+
instance: 'https://example.com/api'
56+
);
57+
58+
expect($problem->getStatusCode())->toBe(400);
59+
})->with([
60+
100,
61+
600,
62+
]);
63+
64+
test('it includes extensions in the problem detail', function () {
65+
$extensions = [
66+
'trace_id' => '1a2b3c4d5e',
67+
'app_code' => 'AUTH-001'
68+
];
69+
70+
$problem = new LaravelHttpApiProblem(
71+
statusCode: 401,
72+
detail: 'Unauthorized access.',
73+
instance: 'https://example.com/api/protected',
74+
extensions: $extensions
75+
);
76+
77+
$array = $problem->toArray();
78+
79+
expect($array)->toHaveKeys(['trace_id', 'app_code']);
80+
expect($array['trace_id'])->toBe('1a2b3c4d5e');
81+
});

0 commit comments

Comments
 (0)