Skip to content

Commit

Permalink
Added unauthenticated response documentation (#392)
Browse files Browse the repository at this point in the history
* `Improvement` | Add 401 Unauthorized response to routes with `auth:sanctum` middleware or `Illuminate\Auth\AuthenticationException` exception

* added tests and proper auth error message

---------

Co-authored-by: Roman Lytvynenko <[email protected]>
  • Loading branch information
Andreas02-dev and romalytvynenko authored May 24, 2024
1 parent a13aeed commit ab22585
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/ScrambleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Dedoc\Scramble\Infer\Scope\Index;
use Dedoc\Scramble\Infer\Services\FileParser;
use Dedoc\Scramble\Support\ExceptionToResponseExtensions\AuthorizationExceptionToResponseExtension;
use Dedoc\Scramble\Support\ExceptionToResponseExtensions\AuthenticationExceptionToResponseExtension;
use Dedoc\Scramble\Support\ExceptionToResponseExtensions\HttpExceptionToResponseExtension;
use Dedoc\Scramble\Support\ExceptionToResponseExtensions\NotFoundExceptionToResponseExtension;
use Dedoc\Scramble\Support\ExceptionToResponseExtensions\ValidationExceptionToResponseExtension;
Expand Down Expand Up @@ -144,6 +145,7 @@ public function configurePackage(Package $package): void
array_merge($exceptionToResponseExtensions, [
ValidationExceptionToResponseExtension::class,
AuthorizationExceptionToResponseExtension::class,
AuthenticationExceptionToResponseExtension::class,
NotFoundExceptionToResponseExtension::class,
HttpExceptionToResponseExtension::class,
]),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Dedoc\Scramble\Support\ExceptionToResponseExtensions;

use Dedoc\Scramble\Extensions\ExceptionToResponseExtension;
use Dedoc\Scramble\Support\Generator\Reference;
use Dedoc\Scramble\Support\Generator\Response;
use Dedoc\Scramble\Support\Generator\Schema;
use Dedoc\Scramble\Support\Generator\Types as OpenApiTypes;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Str;

class AuthenticationExceptionToResponseExtension extends ExceptionToResponseExtension
{
public function shouldHandle(Type $type)
{
return $type instanceof ObjectType
&& $type->isInstanceOf(AuthenticationException::class);
}

public function toResponse(Type $type)
{
$responseBodyType = (new OpenApiTypes\ObjectType())
->addProperty(
'message',
(new OpenApiTypes\StringType())
->setDescription('Error overview.')
)
->setRequired(['message']);

return Response::make(401)
->description('Unauthenticated')
->setContent(
'application/json',
Schema::fromType($responseBodyType)
);
}

public function reference(ObjectType $type)
{
return new Reference('responses', Str::start($type->name, '\\'), $this->components);
}
}
24 changes: 24 additions & 0 deletions src/Support/OperationExtensions/ErrorResponsesExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Dedoc\Scramble\Support\Type\TemplateType;
use Dedoc\Scramble\Support\Type\Type;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
Expand All @@ -31,6 +32,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo)

$this->attachNotFoundException($operation, $methodType);
$this->attachAuthorizationException($routeInfo, $methodType);
$this->attachAuthenticationException($routeInfo, $methodType);
$this->attachCustomRequestExceptions($methodType);
}

Expand Down Expand Up @@ -68,6 +70,28 @@ private function attachAuthorizationException(RouteInfo $routeInfo, FunctionType
];
}

private function attachAuthenticationException(RouteInfo $routeInfo, FunctionType $methodType)
{
if (count($routeInfo->phpDoc()->getTagsByName('@unauthenticated'))) {
return;
}

$isAuthMiddleware = fn ($m) => is_string($m) && ($m === 'auth' || Str::startsWith($m, 'auth:'));

if (! collect($routeInfo->route->gatherMiddleware())->contains($isAuthMiddleware)) {
return;
}

if (collect($methodType->exceptions)->contains(fn (Type $e) => $e->isInstanceOf(AuthenticationException::class))) {
return;
}

$methodType->exceptions = [
...$methodType->exceptions,
new ObjectType(AuthenticationException::class),
];
}

private function attachCustomRequestExceptions(FunctionType $methodType)
{
if (! $formRequest = collect($methodType->arguments)->first(fn (Type $arg) => $arg->isInstanceOf(FormRequest::class))) {
Expand Down
30 changes: 24 additions & 6 deletions tests/ErrorsResponsesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,28 @@
->toHaveCount(1);
});

it('adds auth error response', function () {
RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_auth_error_response']);

Scramble::routes(fn (Route $r) => $r->uri === 'api/test');
$openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)();
it('adds authorization error response', function () {
$openApiDocument = generateForRoute(function () {
return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']);
});

assertMatchesSnapshot($openApiDocument);
});

it('adds authentication error response', function () {
$openApiDocument = generateForRoute(function () {
return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response'])
->middleware('auth');
});

expect($openApiDocument)
->toHaveKey('components.responses.AuthenticationException')
->and($openApiDocument['paths']['/test']['get']['responses'][401])
->toBe([
'$ref' => '#/components/responses/AuthenticationException',
]);
});

it('adds not found error response', function () {
$openApiDocument = generateForRoute(function () {
return RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response'])
Expand Down Expand Up @@ -96,11 +109,16 @@ public function doesnt_add_errors_with_custom_request_when_errors_producing_meth
{
}

public function adds_auth_error_response(Illuminate\Http\Request $request)
public function adds_authorization_error_response(Illuminate\Http\Request $request)
{
$this->authorize('read');
}

public function adds_authentication_error_response(Illuminate\Http\Request $request)
{

}

public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user)
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ info:
servers:
- { url: 'http://localhost/api' }
paths:
/test: { get: { operationId: errorsResponsesTest.addsAuthErrorResponse, tags: [ErrorsResponsesTest_], responses: { 200: { description: '', content: { application/json: { schema: { type: string } } } }, 403: { $ref: '#/components/responses/AuthorizationException' } } } }
/test: { get: { operationId: errorsResponsesTest.addsAuthorizationErrorResponse, tags: [ErrorsResponsesTest_], responses: { 200: { description: '', content: { application/json: { schema: { type: string } } } }, 403: { $ref: '#/components/responses/AuthorizationException' } } } }
components:
responses: { AuthorizationException: { description: 'Authorization error', content: { application/json: { schema: { type: object, properties: { message: { type: string, description: 'Error overview.' } }, required: [message] } } } } }

0 comments on commit ab22585

Please sign in to comment.