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
6 changes: 6 additions & 0 deletions src/Laravel/Exception/ErrorRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Laravel\ApiResource\Error;
use ApiPlatform\Laravel\Controller\ApiPlatformController;
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use ApiPlatform\Metadata\Exception\StatusAwareExceptionInterface;
Expand All @@ -32,6 +33,7 @@
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

class ErrorRenderer
Expand Down Expand Up @@ -195,6 +197,10 @@ private function getStatusCode(?HttpOperation $apiOperation, ?HttpOperation $err
return 400;
}

if ($exception instanceof SerializerExceptionInterface || $exception instanceof InvalidArgumentException) {
return 400;
}

// if ($exception instanceof ValidationException) {
// return 422;
// }
Expand Down
7 changes: 6 additions & 1 deletion src/Laravel/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ public function __construct(private readonly ProviderInterface $provider, privat
*/
public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object
{
$parameters = $this->router->match($iri);
try {
$parameters = $this->router->match($iri);
} catch (RoutingExceptionInterface $e) {
throw new InvalidArgumentException(\sprintf('No route matches "%s".', $iri), $e->getCode(), $e);
}

if (!isset($parameters['_api_resource_class'], $parameters['_api_operation_name'], $parameters['uri_variables'])) {
throw new InvalidArgumentException(\sprintf('No resource associated to "%s".', $iri));
}
Expand Down
13 changes: 12 additions & 1 deletion src/Laravel/Routing/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
use Illuminate\Http\Request as LaravelRequest;
use Illuminate\Routing\Router as BaseRouter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
Expand Down Expand Up @@ -86,7 +90,14 @@ public function getRouteCollection(): RouteCollection
public function match(string $pathInfo): array
{
$request = LaravelRequest::create($pathInfo, Request::METHOD_GET);
$route = $this->router->getRoutes()->match($request);

try {
$route = $this->router->getRoutes()->match($request);
} catch (NotFoundHttpException $e) {
throw new ResourceNotFoundException(\sprintf('No routes found for "%s".', $pathInfo), 0, $e);
} catch (MethodNotAllowedHttpException $e) {
throw new MethodNotAllowedException(explode(', ', $e->getHeaders()['Allow'] ?? ''), $e->getMessage(), 0, $e);
}

return $route->defaults + ['uri_variables' => array_diff_key($route->parameters, $route->defaults)];
}
Expand Down
72 changes: 72 additions & 0 deletions src/Laravel/Tests/JsonLdTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
namespace ApiPlatform\Laravel\Tests;

use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Orchestra\Testbench\Attributes\DefineEnvironment;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;
use Workbench\App\Models\Author;
Expand Down Expand Up @@ -103,6 +106,75 @@ public function testCreateBook(): void
$this->assertMatchesRegularExpression('~^/api/books/~', $response->json('@id'));
}

public function testCreateBookWithAnIriWithoutRoute(): void
{
$response = $this->postJson(
'/api/books',
[
'name' => 'Don Quichotte',
'author' => '/api/nope/1',
'isbn' => fake()->isbn13(),
],
[
'accept' => 'application/ld+json',
'content_type' => 'application/ld+json',
]
);

$response->assertStatus(400);
$this->assertStringContainsString('Invalid IRI "/api/nope/1"', $response->json('detail'));
}

public function testCreateBookWithAnInvalidType(): void
{
AuthorFactory::new()->create();
$response = $this->postJson(
'/api/books',
[
'name' => 123,
'author' => $this->getIriFromResource(Author::find(1)),
'isbn' => fake()->isbn13(),
],
[
'accept' => 'application/ld+json',
'content_type' => 'application/ld+json',
]
);

$response->assertStatus(400);
}

/**
* @param Application $app
*/
protected function usePreviouslyPublishedExceptionToStatus($app): void
{
$app['config']->set('api-platform.exception_to_status', [
AuthenticationException::class => 401,
AuthorizationException::class => 403,
]);
}

#[DefineEnvironment('usePreviouslyPublishedExceptionToStatus')]
public function testCreateBookWithAnInvalidTypeAndAPreviouslyPublishedConfig(): void
{
AuthorFactory::new()->create();
$response = $this->postJson(
'/api/books',
[
'name' => 123,
'author' => $this->getIriFromResource(Author::find(1)),
'isbn' => fake()->isbn13(),
],
[
'accept' => 'application/ld+json',
'content_type' => 'application/ld+json',
]
);

$response->assertStatus(400);
}

public function testUpdateBook(): void
{
BookFactory::new()->has(AuthorFactory::new())->count(10)->create();
Expand Down
43 changes: 43 additions & 0 deletions src/Laravel/Tests/Unit/Routing/IriConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Laravel\Routing\IriConverter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
Expand All @@ -28,6 +29,8 @@
use ApiPlatform\State\ProviderInterface;
use Illuminate\Database\Eloquent\Relations\Relation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouterInterface;
use Workbench\App\Models\Book;
use Workbench\App\Models\BookVariant;
Expand Down Expand Up @@ -220,4 +223,44 @@ private function createIriConverter(RouterInterface $router, ResourceMetadataCol
$resourceMetadataFactory,
);
}

public function testGetResourceFromIriWithoutRoute(): void
{
$router = $this->createStub(RouterInterface::class);
$router->method('match')->willThrowException(new ResourceNotFoundException());

$iriConverter = new IriConverter(
$this->createStub(ProviderInterface::class),
$this->createStub(OperationMetadataFactoryInterface::class),
$router,
$this->createStub(IdentifiersExtractorInterface::class),
$this->createStub(ResourceClassResolverInterface::class),
$this->createStub(ResourceMetadataCollectionFactoryInterface::class),
);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('No route matches "/api/nope/1".');

$iriConverter->getResourceFromIri('/api/nope/1');
}

public function testGetResourceFromIriWithoutGetRoute(): void
{
$router = $this->createStub(RouterInterface::class);
$router->method('match')->willThrowException(new MethodNotAllowedException(['POST']));

$iriConverter = new IriConverter(
$this->createStub(ProviderInterface::class),
$this->createStub(OperationMetadataFactoryInterface::class),
$router,
$this->createStub(IdentifiersExtractorInterface::class),
$this->createStub(ResourceClassResolverInterface::class),
$this->createStub(ResourceMetadataCollectionFactoryInterface::class),
);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('No route matches "/api/only_posts".');

$iriConverter->getResourceFromIri('/api/only_posts');
}
}
34 changes: 34 additions & 0 deletions src/Laravel/Tests/Unit/Routing/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
use Illuminate\Routing\RouteCollection as IlluminateRouteCollection;
use Illuminate\Routing\Router as BaseRouter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouteCollection;

class RouterTest extends TestCase
Expand All @@ -41,4 +45,34 @@ public function testGetRouteCollectionIsMemoizedAcrossCalls(): void
$this->assertSame($symfonyRoutes, $first);
$this->assertSame($first, $second);
}

public function testMatchThrowsResourceNotFoundException(): void
{
$illuminateRoutes = $this->createStub(IlluminateRouteCollection::class);
$illuminateRoutes->method('match')->willThrowException(new NotFoundHttpException());

$baseRouter = $this->createStub(BaseRouter::class);
$baseRouter->method('getRoutes')->willReturn($illuminateRoutes);

$this->expectException(ResourceNotFoundException::class);

(new Router($baseRouter))->match('/api/nope/1');
}

public function testMatchThrowsMethodNotAllowedException(): void
{
$illuminateRoutes = $this->createStub(IlluminateRouteCollection::class);
$illuminateRoutes->method('match')->willThrowException(new MethodNotAllowedHttpException(['POST', 'PUT']));

$baseRouter = $this->createStub(BaseRouter::class);
$baseRouter->method('getRoutes')->willReturn($illuminateRoutes);

try {
(new Router($baseRouter))->match('/api/books');
$this->fail('A MethodNotAllowedException should have been thrown.');
} catch (MethodNotAllowedException $e) {
$this->assertSame(['POST', 'PUT'], $e->getAllowedMethods());
$this->assertInstanceOf(MethodNotAllowedHttpException::class, $e->getPrevious());
}
}
}
4 changes: 4 additions & 0 deletions src/Laravel/config/api-platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@

declare(strict_types=1);

use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;

return [
Expand Down Expand Up @@ -116,6 +118,8 @@
'exception_to_status' => [
AuthenticationException::class => 401,
AuthorizationException::class => 403,
SerializerExceptionInterface::class => 400,
InvalidArgumentException::class => 400,
],

'redoc' => [
Expand Down
Loading