Skip to content

Commit

Permalink
tests
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Aug 18, 2023
1 parent ce60e04 commit 523fc1d
Show file tree
Hide file tree
Showing 70 changed files with 1,306 additions and 261 deletions.
4 changes: 2 additions & 2 deletions features/mongodb/filters.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Feature: Filters on collections
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.badFourthLevel.level=4"
Then the response status code should be 500
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -21,7 +21,7 @@ Feature: Filters on collections
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level=3"
Then the response status code should be 500
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand Down
2 changes: 1 addition & 1 deletion src/Action/ExceptionAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated
* @deprecated since API Platform 3 and Error resource is used {@see ApiPlatform\Symfony\EventListener\ErrorListener}
*/
final class ExceptionAction
{
Expand Down
4 changes: 1 addition & 3 deletions src/Doctrine/Common/State/PersistProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,14 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
return $data;
}

$request = $context['request'] ?? null;

// PUT: reset the existing object managed by Doctrine and merge data sent by the user in it
// This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported:
// https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555
if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false)) {
\assert(method_exists($manager, 'getReference'));
// TODO: the call to getReference is most likely to fail with complex identifiers
$newData = $data;
if ($previousData = $context['previous_data'] ?? $request?->attributes->get('previous_data')) {
if ($previousData = $context['previous_data']) {
$newData = 1 === \count($uriVariables) ? $manager->getReference($class, current($uriVariables)) : clone $previousData;
}

Expand Down
71 changes: 43 additions & 28 deletions src/Documentation/Action/DocumentationAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
use ApiPlatform\Documentation\DocumentationInterface;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Util\ContentNegotiationTrait;
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\OpenApi;
use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Util\ContentNegotiationTrait;
use Negotiation\Negotiator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* Generates the API documentation.
Expand All @@ -49,45 +50,59 @@ public function __construct(
}

/**
* @return DocumentationInterface|OpenApi
* @return DocumentationInterface|OpenApi|Response
*/
public function __invoke(Request $request = null)
{
$context = [];
if (null !== $request) {
$isGateway = $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY);
$context['api_gateway'] = $isGateway;
$context['base_url'] = $request->getBaseUrl();
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
$format = $this->getRequestFormat($request, ['json' => ['application/json'], 'jsonld' => ['application/ld+json'], 'html' => ['text/html']]);

if ('html' === $format || 'json' === $format && null !== $this->openApiFactory) {
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(class: OpenApi::class, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $isGateway]);
if ('html' === $format) {
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
}

$body = $this->provider->provide($operation, [], $context);

return $this->processor->process($body, $operation, [], $context);
}

return $this->openApiFactory->__invoke($context);
if (null === $request) {
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
}

$context = ['api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), 'base_url' => $request->getBaseUrl()];
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
$format = $this->getRequestFormat($request, ['json' => ['application/json'], 'jsonld' => ['application/ld+json'], 'html' => ['text/html']]);

if (null !== $this->openApiFactory && ('html' === $format || 'json' === $format)) {
return $this->getOpenApiDocumentation($context, $format, $request);
}

return $this->getHydraDocumentation($context, $request);
}

/**
* @param array<string,mixed> $context
*/
private function getOpenApiDocumentation(array $context, string $format, Request $request): OpenApi|Response
{
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(class: OpenApi::class, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null]);
if ('html' === $format) {
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
}

return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context);
}

return $this->openApiFactory->__invoke($context);
}

/**
* TODO: the logic behind the Hydra Documentation is done in a ApiPlatform\Hydra\Serializer\DocumentationNormalizer.
* We should transform this to a provider, it'd improve performances also by a bit.
*
* @param array<string,mixed> $context
*/
private function getHydraDocumentation(array $context, Request $request): DocumentationInterface|Response
{
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(
class: Documentation::class,
provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version),
normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $isGateway ?? false]
provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version)
);
$body = $this->provider->provide($operation, [], $context);

return $this->processor->process($body, $operation, [], $context);
return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context);
}

return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

/**
* Converts inner fields with a decorated name converter.
* Converts inner fields with a inner name converter.
*
* @experimental
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class InnerFieldsNameConverter implements AdvancedNameConverterInterface
{
public function __construct(private readonly NameConverterInterface $decorated = new CamelCaseToSnakeCaseNameConverter())
public function __construct(private readonly NameConverterInterface $inner = new CamelCaseToSnakeCaseNameConverter())
{
}

Expand All @@ -51,7 +51,7 @@ private function convertInnerFields(string $propertyName, bool $normalization, s
$convertedProperties = [];

foreach (explode('.', $propertyName) as $decomposedProperty) {
$convertedProperties[] = $this->decorated->{$normalization ? 'normalize' : 'denormalize'}($decomposedProperty, $class, $format, $context);
$convertedProperties[] = $this->inner->{$normalization ? 'normalize' : 'denormalize'}($decomposedProperty, $class, $format, $context);
}

return implode('.', $convertedProperties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ public function testConstruct(): void

public function testNormalize(): void
{
$decoratedProphecy = $this->prophesize(AdvancedNameConverterInterface::class);
$decoratedProphecy->normalize('fooBar', null, null, [])->willReturn('foo_bar')->shouldBeCalled();
$decoratedProphecy->normalize('bazQux', null, null, [])->willReturn('baz_qux')->shouldBeCalled();
$innerProphecy = $this->prophesize(AdvancedNameConverterInterface::class);
$innerProphecy->normalize('fooBar', null, null, [])->willReturn('foo_bar')->shouldBeCalled();
$innerProphecy->normalize('bazQux', null, null, [])->willReturn('baz_qux')->shouldBeCalled();

$innerFieldsNameConverter = new InnerFieldsNameConverter($decoratedProphecy->reveal());
$innerFieldsNameConverter = new InnerFieldsNameConverter($innerProphecy->reveal());

self::assertSame('foo_bar.baz_qux', $innerFieldsNameConverter->normalize('fooBar.bazQux'));
}

public function testDenormalize(): void
{
$decoratedProphecy = $this->prophesize(AdvancedNameConverterInterface::class);
$decoratedProphecy->denormalize('foo_bar', null, null, [])->willReturn('fooBar')->shouldBeCalled();
$decoratedProphecy->denormalize('baz_qux', null, null, [])->willReturn('bazQux')->shouldBeCalled();
$innerProphecy = $this->prophesize(AdvancedNameConverterInterface::class);
$innerProphecy->denormalize('foo_bar', null, null, [])->willReturn('fooBar')->shouldBeCalled();
$innerProphecy->denormalize('baz_qux', null, null, [])->willReturn('bazQux')->shouldBeCalled();

$innerFieldsNameConverter = new InnerFieldsNameConverter($decoratedProphecy->reveal());
$innerFieldsNameConverter = new InnerFieldsNameConverter($innerProphecy->reveal());

self::assertSame('fooBar.bazQux', $innerFieldsNameConverter->denormalize('foo_bar.baz_qux'));
}
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Action/EntrypointAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use ApiPlatform\GraphQl\Error\ErrorHandlerInterface;
use ApiPlatform\GraphQl\ExecutorInterface;
use ApiPlatform\GraphQl\Type\SchemaBuilderInterface;
use ApiPlatform\Util\ContentNegotiationTrait;
use ApiPlatform\Metadata\Util\ContentNegotiationTrait;
use GraphQL\Error\DebugFlag;
use GraphQL\Error\Error;
use GraphQL\Executor\ExecutionResult;
Expand Down
22 changes: 9 additions & 13 deletions src/GraphQl/State/Processor/NormalizeProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
*/
private function getData(mixed $itemOrCollection, GraphQlOperation $operation, array $uriVariables = [], array $context = []): ?array
{
$isCollection = $operation instanceof CollectionOperationInterface;
$isMutation = $operation instanceof Mutation;
$isSubscription = $operation instanceof Subscription;
$isDelete = $operation instanceof DeleteOperationInterface;
$shortName = $operation->getShortName();

if (!($operation->canSerialize() ?? true)) {
if ($isCollection) {
if ($operation instanceof CollectionOperationInterface) {
if ($this->pagination->isGraphQlEnabled($operation, $context)) {
return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ?
$this->getDefaultCursorBasedPaginatedData() :
Expand All @@ -73,11 +67,11 @@ private function getData(mixed $itemOrCollection, GraphQlOperation $operation, a
return [];
}

if ($isMutation) {
if ($operation instanceof Mutation) {
return $this->getDefaultMutationData($context);
}

if ($isSubscription) {
if ($operation instanceof Subscription) {
return $this->getDefaultSubscriptionData($context);
}

Expand All @@ -87,15 +81,15 @@ private function getData(mixed $itemOrCollection, GraphQlOperation $operation, a
$normalizationContext = $this->serializerContextBuilder->create($operation->getClass(), $operation, $context, normalization: true);

$data = null;
if (!$isCollection) {
if ($isMutation && $isDelete) {
if (!$operation instanceof CollectionOperationInterface) {
if ($operation instanceof Mutation && $operation instanceof DeleteOperationInterface) {
$data = ['id' => $this->getIdentifierFromOperation($operation, $context['args'] ?? [])];
} else {
$data = $this->normalizer->normalize($itemOrCollection, ItemNormalizer::FORMAT, $normalizationContext);
}
}

if ($isCollection && is_iterable($itemOrCollection)) {
if ($operation instanceof CollectionOperationInterface && is_iterable($itemOrCollection)) {
if (!$this->pagination->isGraphQlEnabled($operation, $context)) {
$data = [];
foreach ($itemOrCollection as $index => $object) {
Expand All @@ -112,8 +106,10 @@ private function getData(mixed $itemOrCollection, GraphQlOperation $operation, a
throw new \UnexpectedValueException('Expected serialized data to be a nullable array.');
}

$isMutation = $operation instanceof Mutation;
$isSubscription = $operation instanceof Subscription;
if ($isMutation || $isSubscription) {
$wrapFieldName = lcfirst($shortName);
$wrapFieldName = lcfirst($operation->getShortName());

return [$wrapFieldName => $data] + ($isMutation ? $this->getDefaultMutationData($context) : $this->getDefaultSubscriptionData($context));
}
Expand Down
4 changes: 2 additions & 2 deletions src/GraphQl/State/Processor/SubscriptionProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
*/
final class SubscriptionProcessor implements ProcessorInterface
{
public function __construct(private readonly ProcessorInterface $inner, private readonly SubscriptionManagerInterface $subscriptionManager, private readonly ?MercureSubscriptionIriGeneratorInterface $mercureSubscriptionIriGenerator)
public function __construct(private readonly ProcessorInterface $decorated, private readonly SubscriptionManagerInterface $subscriptionManager, private readonly ?MercureSubscriptionIriGeneratorInterface $mercureSubscriptionIriGenerator)
{
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
$data = $this->inner->process($data, $operation, $uriVariables, $context);
$data = $this->decorated->process($data, $operation, $uriVariables, $context);
if (!$operation instanceof GraphQlOperation || !($mercure = $operation->getMercure())) {
return $data;
}
Expand Down
6 changes: 3 additions & 3 deletions src/GraphQl/State/Provider/DenormalizeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
final class DenormalizeProvider implements ProviderInterface
{
/**
* @param ProviderInterface<object> $inner
* @param ProviderInterface<object> $decorated
*/
public function __construct(private readonly ProviderInterface $inner, private readonly DenormalizerInterface $denormalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder)
public function __construct(private readonly ProviderInterface $decorated, private readonly DenormalizerInterface $denormalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder)
{
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$data = $this->inner->provide($operation, $uriVariables, $context);
$data = $this->decorated->provide($operation, $uriVariables, $context);

if (!($operation->canDeserialize() ?? true) || (!$operation instanceof Mutation)) {
return $data;
Expand Down
12 changes: 4 additions & 8 deletions src/GraphQl/State/Provider/ReadProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@

namespace ApiPlatform\GraphQl\State\Provider;

use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Exception\ItemNotFoundException;
use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait;
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\GraphQl\Util\ArrayTrait;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Util\ArrayTrait;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
Expand All @@ -38,12 +38,8 @@ final class ReadProvider implements ProviderInterface
use ClassInfoTrait;
use IdentifierTrait;

public function __construct(
private readonly ProviderInterface $provider,
private readonly IriConverterInterface $iriConverter,
private readonly ?SerializerContextBuilderInterface $serializerContextBuilder,
private readonly string $nestingSeparator
) {
public function __construct(private readonly ProviderInterface $provider, private readonly IriConverterInterface $iriConverter, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder, private readonly string $nestingSeparator)
{
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
Expand Down
4 changes: 2 additions & 2 deletions src/GraphQl/State/Provider/ResolverProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ final class ResolverProvider implements ProviderInterface
{
use ClassInfoTrait;

public function __construct(private readonly ProviderInterface $inner, private readonly ContainerInterface $queryResolverLocator)
public function __construct(private readonly ProviderInterface $decorated, private readonly ContainerInterface $queryResolverLocator)
{
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$item = $this->inner->provide($operation, $uriVariables, $context);
$item = $this->decorated->provide($operation, $uriVariables, $context);

if (!$operation instanceof GraphQlOperation || null === ($queryResolverId = $operation->getResolver())) {
return $item;
Expand Down
Loading

0 comments on commit 523fc1d

Please sign in to comment.