diff --git a/src/Symfony/Bundle/Resources/config/symfony/events.xml b/src/Symfony/Bundle/Resources/config/symfony/events.xml index 53395b92c9d..4e86b56780e 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/events.xml +++ b/src/Symfony/Bundle/Resources/config/symfony/events.xml @@ -42,6 +42,7 @@ + @@ -68,8 +69,6 @@ - null - null @@ -77,7 +76,6 @@ - null diff --git a/src/Symfony/EventListener/AddFormatListener.php b/src/Symfony/EventListener/AddFormatListener.php index 569f58bf07f..fe788318360 100644 --- a/src/Symfony/EventListener/AddFormatListener.php +++ b/src/Symfony/EventListener/AddFormatListener.php @@ -102,7 +102,8 @@ public function onKernelRequest(RequestEvent $event): void return; } - if (!($request->attributes->has('_api_resource_class') + if (!( + $request->attributes->has('_api_resource_class') || $request->attributes->getBoolean('_api_respond', false) || $request->attributes->getBoolean('_graphql', false) )) { diff --git a/src/Symfony/EventListener/DeserializeListener.php b/src/Symfony/EventListener/DeserializeListener.php index 49098cd06d5..bf547d32b44 100644 --- a/src/Symfony/EventListener/DeserializeListener.php +++ b/src/Symfony/EventListener/DeserializeListener.php @@ -50,10 +50,7 @@ final class DeserializeListener private SerializerInterface $serializer; private ?ProviderInterface $provider = null; - /** - * @param ProviderInterface|SerializerInterface $serializer - */ - public function __construct($serializer, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null) + public function __construct(ProviderInterface|SerializerInterface $serializer, private readonly null|SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null) { if ($serializer instanceof ProviderInterface) { $this->provider = $serializer; @@ -62,6 +59,12 @@ public function __construct($serializer, private readonly ?SerializerContextBuil $this->serializer = $serializer; } + if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) { + $resourceMetadataFactory = $serializerContextBuilder; + } else { + trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class); + } + $this->resourceMetadataCollectionFactory = $resourceMetadataFactory; if (null === $this->translator) { $this->translator = new class() implements TranslatorInterface, LocaleAwareInterface { diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php index f2f88c6caf4..3490e4d6f2b 100644 --- a/src/Symfony/EventListener/ErrorListener.php +++ b/src/Symfony/EventListener/ErrorListener.php @@ -102,45 +102,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re } $dup = parent::duplicateRequest($exception, $request); - if ($this->resourceMetadataCollectionFactory) { - if ($this->resourceClassResolver?->isResourceClass($exception::class)) { - $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); - - $operation = null; - foreach ($resourceCollection as $resource) { - foreach ($resource->getOperations() as $op) { - foreach ($op->getOutputFormats() as $key => $value) { - if ($key === $format) { - $operation = $op; - break 3; - } - } - } - } - - // No operation found for the requested format, we take the first available - if (!$operation) { - $operation = $resourceCollection->getOperation(); - } - if ($exception instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) { - $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception); - $operation = $operation->withStatus($statusCode); - } - } else { - // Create a generic, rfc7807 compatible error according to the wanted format - $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); - // status code may be overriden by the exceptionToStatus option - $statusCode = 500; - if ($operation instanceof HttpOperation) { - $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception); - $operation = $operation->withStatus($statusCode); - } - } - } else { - /** @var HttpOperation $operation */ - $operation = new ErrorOperation(name: '_api_errors_problem', class: Error::class, outputFormats: ['jsonld' => ['application/problem+json']], normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]); - $operation = $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception)); - } + $operation = $this->initializeExceptionOperation($request, $exception, $format, $apiOperation); if (null === $operation->getProvider()) { $operation = $operation->withProvider('api_platform.state.error_provider'); @@ -171,6 +133,9 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re return $dup; } + /** + * @return array> + */ private function getOperationExceptionToStatus(Request $request): array { $attributes = RequestAttributesExtractor::extractAttributes($request); @@ -244,4 +209,56 @@ private function getFormatOperation(?string $format): string default => '_api_errors_problem' }; } + + private function initializeExceptionOperation(?Request $request, \Throwable $exception, string $format, ?HttpOperation $apiOperation): HttpOperation + { + if (!$this->resourceMetadataCollectionFactory) { + /** @var HttpOperation $operation */ + $operation = new ErrorOperation( + name: '_api_errors_problem', + class: Error::class, + outputFormats: ['jsonld' => ['application/problem+json']], + normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true] + ); + + return $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception)); + } + + if ($this->resourceClassResolver?->isResourceClass($exception::class)) { + $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); + + $operation = null; + // TODO: move this to ResourceMetadataCollection? + foreach ($resourceCollection as $resource) { + foreach ($resource->getOperations() as $op) { + foreach ($op->getOutputFormats() as $key => $value) { + if ($key === $format) { + $operation = $op; + break 3; + } + } + } + } + + // No operation found for the requested format, we take the first available + $operation ??= $resourceCollection->getOperation(); + + if ($exception instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) { + return $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception)); + } + + return $operation; + } + + // Create a generic, rfc7807 compatible error according to the wanted format + $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); + // status code may be overriden by the exceptionToStatus option + $statusCode = 500; + if ($operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception); + $operation = $operation->withStatus($statusCode); + } + + return $operation; + } } diff --git a/src/Symfony/EventListener/SerializeListener.php b/src/Symfony/EventListener/SerializeListener.php index 6eec5b2fd5d..0a289983ea3 100644 --- a/src/Symfony/EventListener/SerializeListener.php +++ b/src/Symfony/EventListener/SerializeListener.php @@ -46,10 +46,11 @@ final class SerializeListener public const OPERATION_ATTRIBUTE_KEY = 'serialize'; private ?SerializerInterface $serializer = null; private ?ProcessorInterface $processor = null; + private ?SerializerContextBuilderInterface $serializerContextBuilder = null; public function __construct( - $serializer, - private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, + SerializerInterface|ProcessorInterface $serializer, + SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly array $errorFormats = [], // @phpstan-ignore-next-line we don't need this anymore @@ -62,6 +63,13 @@ public function __construct( trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProcessorInterface::class, self::class, SerializerInterface::class); } + if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) { + $resourceMetadataFactory = $serializerContextBuilder; + } else { + $this->serializerContextBuilder = $serializerContextBuilder; + trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class); + } + $this->resourceMetadataCollectionFactory = $resourceMetadataFactory; } diff --git a/src/Symfony/EventListener/ValidateListener.php b/src/Symfony/EventListener/ValidateListener.php index fb93d048b82..98adb6f6792 100644 --- a/src/Symfony/EventListener/ValidateListener.php +++ b/src/Symfony/EventListener/ValidateListener.php @@ -32,13 +32,10 @@ final class ValidateListener public const OPERATION_ATTRIBUTE_KEY = 'validate'; - private ?ValidatorInterface $validator = null; - private ?ProviderInterface $provider = null; + private ValidatorInterface $validator; + private ProviderInterface $provider; - /** - * @param ProviderInterface|ValidatorInterface $validator - */ - public function __construct($validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) + public function __construct(ProviderInterface|ValidatorInterface $validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { if ($validator instanceof ProviderInterface) { $this->provider = $validator; diff --git a/src/Symfony/EventListener/WriteListener.php b/src/Symfony/EventListener/WriteListener.php index 778d90b1303..6c7a42cb497 100644 --- a/src/Symfony/EventListener/WriteListener.php +++ b/src/Symfony/EventListener/WriteListener.php @@ -17,6 +17,9 @@ use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Exception\InvalidIdentifierException; +use ApiPlatform\Metadata\Error; +use ApiPlatform\Metadata\Exception\InvalidUriVariableException; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; @@ -46,22 +49,32 @@ final class WriteListener use OperationRequestInitiatorTrait; use UriVariablesResolverTrait; + private LegacyIriConverterInterface|IriConverterInterface|null $iriConverter; + /** * @param ProcessorInterface $processor */ public function __construct( private readonly ProcessorInterface $processor, - private readonly null|LegacyIriConverterInterface|IriConverterInterface $iriConverter = null, + LegacyIriConverterInterface|IriConverterInterface|ResourceMetadataCollectionFactoryInterface $iriConverter = null, private readonly null|ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, LegacyUriVariablesConverterInterface|UriVariablesConverterInterface $uriVariablesConverter = null, ) { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; $this->uriVariablesConverter = $uriVariablesConverter; if ($processor instanceof CallableProcessor) { trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', WriteProcessor::class, self::class, $processor::class); } + + if ($iriConverter instanceof ResourceMetadataCollectionFactoryInterface) { + $resourceMetadataCollectionFactory = $iriConverter; + } else { + $this->iriConverter = $iriConverter; + trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, IriConverterInterface::class); + } + + $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; } /** @@ -77,12 +90,20 @@ public function onKernelView(ViewEvent $event): void return; } - if ($operation && $this->processor instanceof WriteProcessor) { + if ($operation && !$this->processor instanceof CallableProcessor) { if (null === $operation->canWrite()) { $operation = $operation->withWrite(!$request->isMethodSafe()); } $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; + if (!$uriVariables && !$operation instanceof Error && $operation instanceof HttpOperation) { + try { + $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass()); + } catch (InvalidIdentifierException|InvalidUriVariableException $e) { + throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); + } + } + $request->attributes->set('original_data', $this->clone($controllerResult)); $data = $this->processor->process($controllerResult, $operation, $uriVariables, [ 'request' => $request, diff --git a/src/Symfony/Tests/EventListener/AddFormatListenerTest.php b/src/Symfony/Tests/EventListener/AddFormatListenerTest.php index 83948de314e..11a70b10527 100644 --- a/src/Symfony/Tests/EventListener/AddFormatListenerTest.php +++ b/src/Symfony/Tests/EventListener/AddFormatListenerTest.php @@ -40,8 +40,11 @@ public function testFetchOperation(): void $listener = new AddFormatListener($provider, $metadata); $listener->onKernelRequest( - new RequestEvent($this->createStub(HttpKernelInterface::class), - new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']), HttpKernelInterface::MAIN_REQUEST) + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']), + HttpKernelInterface::MAIN_REQUEST + ) ); } @@ -53,8 +56,11 @@ public function testCallProvider(): void $listener = new AddFormatListener($provider, $metadata); $listener->onKernelRequest( - new RequestEvent($this->createStub(HttpKernelInterface::class), - new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']), HttpKernelInterface::MAIN_REQUEST) + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']), + HttpKernelInterface::MAIN_REQUEST + ) ); } @@ -66,8 +72,11 @@ public function testNoCallProvider(...$attributes): void $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); $listener = new AddFormatListener($provider, $metadata); $listener->onKernelRequest( - new RequestEvent($this->createStub(HttpKernelInterface::class), - new Request([], [], $attributes), HttpKernelInterface::MAIN_REQUEST) + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + new Request([], [], $attributes), + HttpKernelInterface::MAIN_REQUEST + ) ); } diff --git a/src/Symfony/Tests/EventListener/DeserializeListenerTest.php b/src/Symfony/Tests/EventListener/DeserializeListenerTest.php new file mode 100644 index 00000000000..4a2a66ba538 --- /dev/null +++ b/src/Symfony/Tests/EventListener/DeserializeListenerTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\EventListener; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\EventListener\DeserializeListener; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class DeserializeListenerTest extends TestCase +{ + public function testFetchOperation(): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide'); + $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [ + new ApiResource(operations: [ + 'operation' => new Post(), + ]), + ])); + + $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $request->setMethod('POST'); + $listener = new DeserializeListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testCallProvider(): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Post(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $request->setMethod('POST'); + $listener = new DeserializeListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + #[DataProvider('provideNonApiAttributes')] + public function testNoCallProvider(...$attributes): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->never())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new DeserializeListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + new Request([], [], $attributes), + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testDeserializeFalse(): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->never())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Post(deserialize: false), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $request->setMethod('POST'); + $listener = new DeserializeListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testDeserializeNullWithGetMethod(): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->never())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new DeserializeListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public static function provideNonApiAttributes(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ['_api_receive' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } +} diff --git a/tests/Symfony/EventListener/ErrorListenerTest.php b/src/Symfony/Tests/EventListener/ErrorListenerTest.php similarity index 52% rename from tests/Symfony/EventListener/ErrorListenerTest.php rename to src/Symfony/Tests/EventListener/ErrorListenerTest.php index 58d806af2bd..3bc622c1bdf 100644 --- a/tests/Symfony/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Tests/EventListener/ErrorListenerTest.php @@ -11,52 +11,53 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Symfony\EventListener; +namespace ApiPlatform\Symfony\Tests\EventListener; -use ApiPlatform\Api\IdentifiersExtractorInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\State\ApiResource\Error; use ApiPlatform\Symfony\EventListener\ErrorListener; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelInterface; -final class ErrorListenerTest extends TestCase +class ErrorListenerTest extends TestCase { - use ProphecyTrait; - public function testDuplicateException(): void { $exception = new \Exception(); $operation = new Get(name: '_api_errors_problem', priority: 0, status: 400); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->create(Error::class) - ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass($exception::class)->willReturn(false); - $kernel = $this->prophesize(KernelInterface::class); - $kernel->handle(Argument::that(function ($request) { + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once())->method('create') + ->with(Error::class) + ->willReturn( + new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])]) + ); + + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolver->expects($this->once())->method('isResourceClass')->with($exception::class)->willReturn(false); + $kernel = $this->createStub(KernelInterface::class); + $kernel->method('handle')->willReturnCallback(function ($request) { $this->assertTrue($request->attributes->has('_api_original_route')); $this->assertTrue($request->attributes->has('_api_original_route_params')); $this->assertTrue($request->attributes->has('_api_requested_operation')); $this->assertTrue($request->attributes->has('_api_previous_operation')); $this->assertEquals('_api_errors_problem', $request->attributes->get('_api_operation_name')); - return true; - }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response()); + return new Response(); + }); + $request = Request::create('/'); $request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true])); - $exceptionEvent = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $exception); - $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonproblem' => ['application/problem+json']], [], null, $resourceClassResolver->reveal(), problemCompliantErrors: true); + $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception); + $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonproblem' => ['application/problem+json']], [], null, $resourceClassResolver, problemCompliantErrors: true); $errorListener->onKernelException($exceptionEvent); } @@ -64,25 +65,29 @@ public function testDuplicateExceptionWithHydra(): void { $exception = new \Exception(); $operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->create(Error::class) - ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass($exception::class)->willReturn(false); - $kernel = $this->prophesize(KernelInterface::class); - $kernel->handle(Argument::that(function ($request) { + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once())->method('create') + ->with(Error::class) + ->willReturn( + new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])]) + ); + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolver->expects($this->once())->method('isResourceClass')->with($exception::class)->willReturn(false); + + $kernel = $this->createStub(KernelInterface::class); + $kernel->method('handle')->willReturnCallback(function ($request) { $this->assertTrue($request->attributes->has('_api_original_route')); $this->assertTrue($request->attributes->has('_api_original_route_params')); $this->assertTrue($request->attributes->has('_api_requested_operation')); $this->assertTrue($request->attributes->has('_api_previous_operation')); $this->assertEquals('_api_errors_hydra', $request->attributes->get('_api_operation_name')); - return true; - }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response()); + return new Response(); + }); $request = Request::create('/'); $request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true])); - $exceptionEvent = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $exception); - $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], null, $resourceClassResolver->reveal()); + $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception); + $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], null, $resourceClassResolver); $errorListener->onKernelException($exceptionEvent); } @@ -90,13 +95,18 @@ public function testDuplicateExceptionWithErrorResource(): void { $exception = Error::createFromException(new \Exception(), 400); $operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400, outputFormats: ['jsonld' => ['application/ld+json']]); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->create(Error::class) - ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(Error::class)->willReturn(true); - $kernel = $this->prophesize(KernelInterface::class); - $kernel->handle(Argument::that(function ($request) { + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once())->method('create') + ->with(Error::class) + ->willReturn( + new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])]) + ); + + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolver->expects($this->once())->method('isResourceClass')->with(Error::class)->willReturn(true); + + $kernel = $this->createStub(KernelInterface::class); + $kernel->method('handle')->willReturnCallback(function ($request) { $this->assertTrue($request->attributes->has('_api_original_route')); $this->assertTrue($request->attributes->has('_api_original_route_params')); $this->assertTrue($request->attributes->has('_api_requested_operation')); @@ -117,33 +127,34 @@ public function testDuplicateExceptionWithErrorResource(): void ], ]); - return true; - }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response()); + return new Response(); + }); $request = Request::create('/'); $request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true])); - $exceptionEvent = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $exception); - $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); - $identifiersExtractor->getIdentifiersFromItem($exception, Argument::any())->willReturn(['id' => 1]); - $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal()); + $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception); + $identifiersExtractor = $this->createStub(IdentifiersExtractorInterface::class); + $identifiersExtractor->method('getIdentifiersFromItem')->willReturn(['id' => 1]); + $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], $identifiersExtractor, $resourceClassResolver); $errorListener->onKernelException($exceptionEvent); } public function testDisableErrorResourceHandling(): void { $exception = Error::createFromException(new \Exception(), 400); - $operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400, outputFormats: ['jsonld' => ['application/ld+json']]); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $kernel = $this->prophesize(KernelInterface::class); - $kernel->handle(Argument::that(function ($request) { + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->never())->method('create'); + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolver->expects($this->never())->method('isResourceClass'); + $kernel = $this->createStub(KernelInterface::class); + $kernel->method('handle')->willReturnCallback(function ($request) { $this->assertEquals($request->attributes->get('_api_operation'), null); - return true; - }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response()); - $exceptionEvent = new ExceptionEvent($kernel->reveal(), Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception); - $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); - $identifiersExtractor->getIdentifiersFromItem($exception, Argument::any())->willReturn(['id' => 1]); - $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal(), null, false); + return new Response(); + }); + + $exceptionEvent = new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception); + $identifiersExtractor = $this->createStub(IdentifiersExtractorInterface::class); + $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], $identifiersExtractor, $resourceClassResolver, null, false); $errorListener->onKernelException($exceptionEvent); } } diff --git a/src/Symfony/Tests/EventListener/ReadListenerTest.php b/src/Symfony/Tests/EventListener/ReadListenerTest.php new file mode 100644 index 00000000000..26c05ae1fbd --- /dev/null +++ b/src/Symfony/Tests/EventListener/ReadListenerTest.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\EventListener; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\UriVariablesConverterInterface; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\EventListener\ReadListener; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class ReadListenerTest extends TestCase +{ + public function testFetchOperation(): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide'); + $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [ + new ApiResource(operations: [ + 'operation' => new Get(), + ]), + ])); + + $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new ReadListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testCallProvider(): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new ReadListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + #[DataProvider('provideNonApiAttributes')] + public function testNoCallProvider(...$attributes): void + { + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->never())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new ReadListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + new Request([], [], $attributes), + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testReadFalse(): void + { + $operation = new Get(read: false); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide')->with($operation); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new ReadListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testReadWithUriVariables(): void + { + $operation = new Get(uriVariables: ['id' => new Link(identifiers: ['id'])], class: 'class'); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide')->with($operation->withRead(true), ['id' => 3]); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $uriVariablesConverter = $this->createMock(UriVariablesConverterInterface::class); + $uriVariablesConverter->expects($this->once())->method('convert')->with(['id' => '3'], 'class')->willReturn(['id' => 3]); + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', 'id' => '3']); + $listener = new ReadListener($provider, $metadata, null, $uriVariablesConverter); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public function testReadNullWithPostMethod(): void + { + $operation = new Post(read: false); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide')->with($operation); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Post(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $request->setMethod('POST'); + $listener = new ReadListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public static function provideNonApiAttributes(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ['_api_receive' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } +} diff --git a/src/Symfony/Tests/EventListener/RespondListenerTest.php b/src/Symfony/Tests/EventListener/RespondListenerTest.php new file mode 100644 index 00000000000..d897198dae3 --- /dev/null +++ b/src/Symfony/Tests/EventListener/RespondListenerTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\EventListener; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\Symfony\EventListener\RespondListener; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ViewEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class RespondListenerTest extends TestCase +{ + public function testFetchOperation(): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturn(new Response()); + $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [ + new ApiResource(operations: [ + 'operation' => new Get(), + ]), + ])); + + $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new RespondListener($metadata, $processor); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallProcessor(): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new RespondListener($metadata, $processor); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallProcessorContext(): void + { + $operation = new Get(class: 'class'); + $controllerResult = new \stdClass(); + $originalData = new \stdClass(); + $uriVariables = ['id' => 3]; + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables, 'original_data' => $originalData]); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process') + ->with($controllerResult, $operation, $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class', 'original_data' => $originalData])->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new RespondListener($metadata, $processor); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + #[DataProvider('provideNonApiAttributes')] + public function testNoCallProcessor(...$attributes): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->never())->method('process')->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], $attributes); + $listener = new RespondListener($metadata, $processor); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public static function provideNonApiAttributes(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ['_api_respond' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } +} diff --git a/src/Symfony/Tests/EventListener/SerializeListenerTest.php b/src/Symfony/Tests/EventListener/SerializeListenerTest.php new file mode 100644 index 00000000000..2ffdae3ebda --- /dev/null +++ b/src/Symfony/Tests/EventListener/SerializeListenerTest.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\EventListener; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\Symfony\EventListener\SerializeListener; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ViewEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class SerializeListenerTest extends TestCase +{ + public function testFetchOperation(): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturn(new Response()); + $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [ + new ApiResource(operations: [ + 'operation' => new Get(), + ]), + ])); + + $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new SerializeListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallProcessor(): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new SerializeListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallProcessorContext(): void + { + $operation = new Get(class: 'class'); + $controllerResult = new \stdClass(); + $uriVariables = ['id' => 3]; + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process') + ->with($controllerResult, $operation->withSerialize(true), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class'])->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new SerializeListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + #[DataProvider('provideNonApiAttributes')] + public function testNoCallProcessor(...$attributes): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->never())->method('process')->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], $attributes); + $listener = new SerializeListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public static function provideNonApiAttributes(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ['_api_respond' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } +} diff --git a/src/Symfony/Tests/EventListener/ValidateListenerTest.php b/src/Symfony/Tests/EventListener/ValidateListenerTest.php new file mode 100644 index 00000000000..f686b5e40cf --- /dev/null +++ b/src/Symfony/Tests/EventListener/ValidateListenerTest.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\EventListener; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\EventListener\ValidateListener; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ViewEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class ValidateListenerTest extends TestCase +{ + public function testFetchOperation(): void + { + $controllerResult = new \stdClass(); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide'); + $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [ + new ApiResource(operations: [ + 'operation' => new Post(), + ]), + ])); + + $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new ValidateListener($provider, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallprovider(): void + { + $controllerResult = new \stdClass(); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Post(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new ValidateListener($provider, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallproviderContext(): void + { + $operation = new Post(class: 'class'); + $controllerResult = new \stdClass(); + $uriVariables = ['id' => 3]; + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]); + $request->setMethod($operation->getMethod()); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide') + ->with($operation->withValidate(true), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class']); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new ValidateListener($provider, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testDeleteNoValidate(): void + { + $operation = new Delete(class: 'class'); + $controllerResult = new \stdClass(); + $uriVariables = ['id' => 3]; + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]); + $request->setMethod($operation->getMethod()); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide') + ->with($operation->withValidate(false), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class']); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new ValidateListener($provider, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testDeleteForceValidate(): void + { + $operation = new Delete(class: 'class', validate: true); + $controllerResult = new \stdClass(); + $uriVariables = ['id' => 3]; + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]); + $request->setMethod($operation->getMethod()); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide') + ->with($operation->withValidate(true), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class']); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new ValidateListener($provider, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + #[DataProvider('provideNonApiAttributes')] + public function testNoCallprovider(...$attributes): void + { + $controllerResult = new \stdClass(); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->never())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], $attributes); + $listener = new ValidateListener($provider, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public static function provideNonApiAttributes(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ['_api_respond' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } +} diff --git a/src/Symfony/Tests/EventListener/WriteListenerTest.php b/src/Symfony/Tests/EventListener/WriteListenerTest.php new file mode 100644 index 00000000000..98280b2f3d8 --- /dev/null +++ b/src/Symfony/Tests/EventListener/WriteListenerTest.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\EventListener; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\UriVariablesConverterInterface; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\Symfony\EventListener\WriteListener; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ViewEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class WriteListenerTest extends TestCase +{ + public function testFetchOperation(): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturn(new Response()); + $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [ + new ApiResource(operations: [ + 'operation' => new Get(), + ]), + ])); + + $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new WriteListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallProcessor(): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); + $listener = new WriteListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public function testCallProcessorContext(): void + { + $operation = new Get(class: 'class'); + $controllerResult = new \stdClass(); + $originalData = new \stdClass(); + $uriVariables = ['id' => 3]; + $returnValue = new \stdClass(); + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables, 'previous_data' => $originalData]); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process') + ->with($controllerResult, $operation, $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class', 'previous_data' => $originalData])->willReturn($returnValue); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $listener = new WriteListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + $this->assertEquals($returnValue, $request->attributes->get('original_data')); + } + + #[DataProvider('provideNonApiAttributes')] + public function testNoCallProcessor(...$attributes): void + { + $controllerResult = new \stdClass(); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->never())->method('process')->willReturn(new Response()); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $request = new Request([], [], $attributes); + $listener = new WriteListener($processor, $metadata); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } + + public static function provideNonApiAttributes(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ['_api_persist' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } + + public function testWriteWithUriVariables(): void + { + $controllerResult = new \stdClass(); + $operation = new Post(uriVariables: ['id' => new Link(identifiers: ['id'])], class: 'class'); + $provider = $this->createMock(ProcessorInterface::class); + $provider->expects($this->once())->method('process')->with($controllerResult, $operation->withWrite(true), ['id' => 3]); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $uriVariablesConverter = $this->createMock(UriVariablesConverterInterface::class); + $uriVariablesConverter->expects($this->once())->method('convert')->with(['id' => '3'], 'class')->willReturn(['id' => 3]); + $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', 'id' => '3']); + $request->setMethod($operation->getMethod()); + $listener = new WriteListener($provider, $metadata, uriVariablesConverter: $uriVariablesConverter); + $listener->onKernelView( + new ViewEvent( + $this->createStub(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST, + $controllerResult + ) + ); + } +} diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php index 839319dc928..cf3fdc8902c 100644 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ b/src/Symfony/Validator/Exception/ValidationException.php @@ -44,7 +44,8 @@ normalizationContext: ['groups' => ['json'], 'skip_null_values' => true, 'rfc_7807_compliant_errors' => true, - ]), + ] + ), new ErrorOperation( name: '_api_validation_errors_hydra', outputFormats: ['jsonld' => ['application/problem+json']],