diff --git a/composer.json b/composer.json index 39ba424c595..a7e64a0d7e3 100644 --- a/composer.json +++ b/composer.json @@ -86,7 +86,7 @@ "symfony/web-profiler-bundle": "^6.1", "symfony/yaml": "^6.1", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", - "webonyx/graphql-php": "^14.0" + "webonyx/graphql-php": "^14.0 || ^15.0" }, "conflict": { "doctrine/common": "<3.2.2", @@ -95,7 +95,7 @@ "doctrine/mongodb-odm": "<2.4", "doctrine/persistence": "<1.3", "symfony/service-contracts": "<3", - "symfony/var-exporter" : "<6.1.1", + "symfony/var-exporter" : "<6.1.1", "phpunit/phpunit": "<9.5", "phpspec/prophecy": "<1.15", "elasticsearch/elasticsearch": ">=8.0" diff --git a/features/graphql/authorization.feature b/features/graphql/authorization.feature index dd5ad290a62..f1e918b5242 100644 --- a/features/graphql/authorization.feature +++ b/features/graphql/authorization.feature @@ -19,7 +19,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.securedDummy" should be null @@ -42,7 +41,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.securedDummies" should be null @@ -87,7 +85,6 @@ Feature: Authorization checking And the header "Content-Type" should be equal to "application/json" And the JSON node "data.securedDummies" should be null And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.securedDummies" should be null @@ -107,7 +104,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Only admins can create a secured dummy." And the JSON node "data.createSecuredDummy" should be null @@ -206,7 +202,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.relatedSecuredDummy" should be null @@ -228,7 +223,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.relatedSecuredDummies" should be null @@ -427,7 +421,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.securedDummy" should be null @@ -560,7 +553,6 @@ Feature: Authorization checking And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 403 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "Access Denied." And the JSON node "data.updateSecuredDummy" should be null diff --git a/features/graphql/input_output.feature b/features/graphql/input_output.feature index 1a5bcb166b5..aac22be3f3c 100644 --- a/features/graphql/input_output.feature +++ b/features/graphql/input_output.feature @@ -143,15 +143,12 @@ Feature: GraphQL DTO input and output Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON should be equal to: + And the JSON should be a superset of: """ { "errors": [ { "message": "Cannot query field \"id\" on type \"DummyDtoNoOutput\".", - "extensions": { - "category": "graphql" - }, "locations": [ { "line": 4, @@ -175,37 +172,8 @@ Feature: GraphQL DTO input and output Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON should be equal to: - """ - { - "errors": [ - { - "message": "Field \"lorem\" is not defined by type createDummyDtoNoInputInput.", - "extensions": { - "category": "graphql" - }, - "locations": [ - { - "line": 2, - "column": 33 - } - ] - }, - { - "message": "Field \"ipsum\" is not defined by type createDummyDtoNoInputInput.", - "extensions": { - "category": "graphql" - }, - "locations": [ - { - "line": 2, - "column": 53 - } - ] - } - ] - } - """ + And the JSON node "errors[0].message" should match '/^Field "lorem" is not defined by type "?createDummyDtoNoInputInput"?\.$/' + And the JSON node "errors[1].message" should match '/^Field "ipsum" is not defined by type "?createDummyDtoNoInputInput"?\.$/' Scenario: Use messenger with GraphQL and an input where the handler gives a synchronous result When I send the following GraphQL request: diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature index 03be840718e..599288f1c4c 100644 --- a/features/graphql/introspection.feature +++ b/features/graphql/introspection.feature @@ -7,7 +7,6 @@ Feature: GraphQL introspection support And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "errors[0].extensions.status" should be equal to 400 - And the JSON node "errors[0].extensions.category" should be equal to user And the JSON node "errors[0].message" should be equal to "GraphQL query is not valid." Scenario: Introspect the GraphQL schema @@ -563,7 +562,7 @@ Feature: GraphQL introspection support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionCursorConnection" is not present in the types container' + And the GraphQL debug message should be equal to 'Type with id "VoDummyInspectionCursorConnection" is not present in the types container' And the JSON node "data.typeNotAvailable" should be null And the JSON node "data.typeOwner.fields[1].type.name" should be equal to "VoDummyInspectionCursorConnection" diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index b0532bf4d80..c8e290edbfa 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -673,7 +673,7 @@ Feature: GraphQL mutation support } } """ - And the response should be in JSON + Then the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "data.createWritableId.writableId.id" should be equal to "/writable_ids/c6b722fe-0331-48c4-a214-f81f9f1ca082" And the JSON node "data.createWritableId.writableId._id" should be equal to "c6b722fe-0331-48c4-a214-f81f9f1ca082" diff --git a/features/graphql/query.feature b/features/graphql/query.feature index ce4c7c2b0e5..a8a6a755831 100644 --- a/features/graphql/query.feature +++ b/features/graphql/query.feature @@ -278,7 +278,7 @@ Feature: GraphQL query support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON node "errors[0].debugMessage" should be equal to 'No route matches "/foo/1".' + And the GraphQL debug message should be equal to 'No route matches "/foo/1".' And the JSON should be valid according to this schema: """ { @@ -289,35 +289,38 @@ Feature: GraphQL query support "items": { "type": "object", "properties": { - "debugMessage": {"type": "string"}, "message": {"type": "string"}, - "extensions": {"type": "object"}, + "extensions": { + "type": "object", + "properties": { + "debugMessage": {"type": "string"}, + "file": {"type": "string"}, + "line": {"type": "integer"}, + "trace": { + "type": "array", + "items": { + "type": "object", + "properties": { + "file": {"type": "string"}, + "line": {"type": "integer"}, + "call": {"type": ["string", "null"]}, + "function": {"type": ["string", "null"]} + }, + "additionalProperties": false + }, + "minItems": 1 + } + } + }, "locations": {"type": "array"}, - "path": {"type": "array"}, - "trace": { - "type": "array", - "items": { - "type": "object", - "properties": { - "file": {"type": "string"}, - "line": {"type": "integer"}, - "call": {"type": ["string", "null"]}, - "function": {"type": ["string", "null"]} - }, - "additionalProperties": false - }, - "minItems": 1 - } + "path": {"type": "array"} }, "required": [ - "debugMessage", "message", "extensions", "locations", - "path", - "trace" - ], - "additionalProperties": false + "path" + ] }, "minItems": 1, "maxItems": 1 diff --git a/features/graphql/schema.feature b/features/graphql/schema.feature index 765905fce98..5ef4892aa84 100644 --- a/features/graphql/schema.feature +++ b/features/graphql/schema.feature @@ -15,7 +15,9 @@ Feature: GraphQL schema-related features ###The dummy name### name: String! } - + """ + And the command output should contain: + """ ###Cursor connection for DummyFriend.### type DummyFriendCursorConnection { edges: [DummyFriendEdge] @@ -37,41 +39,62 @@ Feature: GraphQL schema-related features hasPreviousPage: Boolean! } """ + And the command output should contain: + """ + ###Updates a DummyFriend.### + updateDummyFriend(input: updateDummyFriendInput!): updateDummyFriendPayload - Scenario: Export the GraphQL schema in SDL with comment descriptions - When I run the command "api:graphql:export" with options: - | --comment-descriptions | true | - Then the command output should contain: + ###Deletes a DummyFriend.### + deleteDummyFriend(input: deleteDummyFriendInput!): deleteDummyFriendPayload + + ###Creates a DummyFriend.### + createDummyFriend(input: createDummyFriendInput!): createDummyFriendPayload """ - # Dummy Friend. - type DummyFriend implements Node { + And the command output should contain: + """ + ###Updates a DummyFriend.### + input updateDummyFriendInput { id: ID! - # The id - _id: Int! - - # The dummy name - name: String! + ###The dummy name### + name: String + clientMutationId: String } - - # Cursor connection for DummyFriend. - type DummyFriendCursorConnection { - edges: [DummyFriendEdge] - pageInfo: DummyFriendPageInfo! - totalCount: Int! + """ + And the command output should contain: + """ + ###Updates a DummyFriend.### + type updateDummyFriendPayload { + dummyFriend: DummyFriend + clientMutationId: String + } + """ + And the command output should contain: + """ + ###Deletes a DummyFriend.### + input deleteDummyFriendInput { + id: ID! + clientMutationId: String } - # Edge of DummyFriend. - type DummyFriendEdge { - node: DummyFriend - cursor: String! + ###Deletes a DummyFriend.### + type deleteDummyFriendPayload { + dummyFriend: DummyFriend + clientMutationId: String + } + """ + And the command output should contain: + """ + ###Creates a DummyFriend.### + input createDummyFriendInput { + ###The dummy name### + name: String! + clientMutationId: String } - # Information about the current page. - type DummyFriendPageInfo { - endCursor: String - startCursor: String - hasNextPage: Boolean! - hasPreviousPage: Boolean! + ###Creates a DummyFriend.### + type createDummyFriendPayload { + dummyFriend: DummyFriend + clientMutationId: String } """ diff --git a/features/graphql/type.feature b/features/graphql/type.feature index 5e7d641fc9e..03a072785d5 100644 --- a/features/graphql/type.feature +++ b/features/graphql/type.feature @@ -76,4 +76,5 @@ Feature: GraphQL type support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON node "errors[0].message" should be equal to 'Variable "$itemDate" got invalid value "bad date"; Expected type DateTime; DateTime cannot represent non date value: "bad date"' + And the JSON node "errors[0].message" should contain 'Variable "$itemDate" got invalid value "bad date";' + And the JSON node "errors[0].message" should contain 'DateTime cannot represent non date value: "bad date"' diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f0af5143126..e03130e7110 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -60,10 +60,6 @@ parameters: - message: '#Call to an undefined method Negotiation\\AcceptHeader::getType\(\)\.#' path: src/Symfony/EventListener/AddFormatListener.php - - '#Parameter \#1 \$vars of class GraphQL\\Language\\AST\\(IntValue|ObjectField|ObjectValue|BooleanValue|ListValue|StringValue)Node constructor expects array, array given\.#' - - - message: '#Parameter \#1 \$objectValue of method GraphQL\\Type\\Definition\\InterfaceType::resolveType\(\) expects object, array()? given.#' - path: tests/GraphQl/Type/TypeBuilderTest.php # https://github.com/phpstan/phpstan-symfony/issues/76 - message: '#Service "test" is not registered in the container\.#' @@ -78,6 +74,8 @@ parameters: - message: "#Call to function method_exists\\(\\) with ApiPlatform\\\\JsonApi\\\\Serializer\\\\ItemNormalizer and 'setCircularReferenc…' will always evaluate to false\\.#" path: tests/JsonApi/Serializer/ItemNormalizerTest.php + - '#Method GraphQL\\Type\\Definition\\WrappingType::getWrappedType\(\) invoked with 1 parameter, 0 required\.#' + - '#Access to an undefined property GraphQL\\Type\\Definition\\NamedType&GraphQL\\Type\\Definition\\Type::\$name\.#' # See https://github.com/phpstan/phpstan-symfony/issues/27 - message: '#^Service "[^"]+" is private.$#' diff --git a/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php index 6f77c124353..f64160d47d3 100644 --- a/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php @@ -35,7 +35,10 @@ public function normalize(mixed $object, string $format = null, array $context = $error = FormattedError::createFromException($object); $error['message'] = $httpException->getMessage(); $error['extensions']['status'] = $statusCode = $httpException->getStatusCode(); - $error['extensions']['category'] = $statusCode < 500 ? 'user' : Error::CATEGORY_INTERNAL; + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_INTERNAL')) { + $error['extensions']['category'] = $statusCode < 500 ? 'user' : Error::CATEGORY_INTERNAL; + } return $error; } diff --git a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php index 596739e3a6a..1fb6526ed47 100644 --- a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php @@ -53,7 +53,10 @@ public function normalize(mixed $object, string $format = null, array $context = } } $error['extensions']['status'] = $statusCode; - $error['extensions']['category'] = 'user'; + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_INTERNAL')) { + $error['extensions']['category'] = 'user'; + } $error['extensions']['violations'] = []; /** @var ConstraintViolation $violation */ diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 17d37d2b35a..90840d73aa7 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -300,7 +300,14 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); - $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; + $graphqlWrappedType = $graphqlType; + if ($graphqlType instanceof WrappingType) { + if (method_exists($graphqlType, 'getInnermostType')) { + $graphqlWrappedType = $graphqlType->getInnermostType(); + } else { + $graphqlWrappedType = $graphqlType->getWrappedType(true); + } + } $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); if ($isStandardGraphqlType) { $resourceClass = ''; diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index c1e2d1ce942..56c49607dfe 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -18,9 +18,9 @@ use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Schema; /** @@ -79,34 +79,39 @@ public function getSchema(): Schema } } + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => $queryFields, + ]); + $this->typesContainer->set('Query', $queryType); + $schema = [ - 'query' => new ObjectType([ - 'name' => 'Query', - 'fields' => $queryFields, - ]), - 'typeLoader' => function ($name): Type { - $type = $this->typesContainer->get($name); - - if ($type instanceof WrappingType) { - return $type->getWrappedType(true); - } + 'query' => $queryType, + 'typeLoader' => function (string $typeName): Type&NamedType { + $type = $this->typesContainer->get($typeName); - return $type; + return Type::getNamedType($type); }, ]; if ($mutationFields) { - $schema['mutation'] = new ObjectType([ + $mutationType = new ObjectType([ 'name' => 'Mutation', 'fields' => $mutationFields, ]); + $this->typesContainer->set('Mutation', $mutationType); + + $schema['mutation'] = $mutationType; } if ($subscriptionFields) { - $schema['subscription'] = new ObjectType([ + $subscriptionType = new ObjectType([ 'name' => 'Subscription', 'fields' => $subscriptionFields, ]); + $this->typesContainer->set('Subscription', $subscriptionType); + + $schema['subscription'] = $subscriptionType; } return new Schema($schema); diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 516b5efe18d..b01af9d2d12 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -213,7 +213,9 @@ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, st */ public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType { - $shortName = $resourceType->name; + $namedType = GraphQLType::getNamedType($resourceType); + // graphql-php 15: name() exists + $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name; $paginationType = $this->pagination->getGraphQlPaginationType($operation); $connectionTypeKey = sprintf('%s%sConnection', $shortName, ucfirst($paginationType)); @@ -282,7 +284,9 @@ public function isCollection(Type $type): bool private function getCursorBasedPaginationFields(GraphQLType $resourceType): array { - $shortName = $resourceType->name; + $namedType = GraphQLType::getNamedType($resourceType); + // graphql-php 15: name() exists + $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name; $edgeObjectTypeConfiguration = [ 'name' => "{$shortName}Edge", @@ -317,7 +321,9 @@ private function getCursorBasedPaginationFields(GraphQLType $resourceType): arra private function getPageBasedPaginationFields(GraphQLType $resourceType): array { - $shortName = $resourceType->name; + $namedType = GraphQLType::getNamedType($resourceType); + // graphql-php 15: name() exists + $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name; $paginationInfoObjectTypeConfiguration = [ 'name' => "{$shortName}PaginationInfo", diff --git a/src/Symfony/Bundle/Command/GraphQlExportCommand.php b/src/Symfony/Bundle/Command/GraphQlExportCommand.php index a49650f4e5a..6bca2332007 100644 --- a/src/Symfony/Bundle/Command/GraphQlExportCommand.php +++ b/src/Symfony/Bundle/Command/GraphQlExportCommand.php @@ -40,7 +40,8 @@ protected function configure(): void { $this ->setDescription('Export the GraphQL schema in Schema Definition Language (SDL)') - ->addOption('comment-descriptions', null, InputOption::VALUE_NONE, 'Use preceding comments as the description') + ->addOption('comment-descriptions', null, InputOption::VALUE_NONE, 'Use preceding comments as the description (deprecated: graphql-php < 15)') + ->addOption('sort-types', null, InputOption::VALUE_NONE, 'Order types alphabetically') ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Write output to file'); } @@ -53,9 +54,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $options = []; + // Removed in graphql-php 15 if ($input->getOption('comment-descriptions')) { $options['commentDescriptions'] = true; } + if ($input->getOption('sort-types')) { + $options['sortTypes'] = true; + } $schemaExport = SchemaPrinter::doPrint($this->schemaBuilder->getSchema(), $options); diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 329b20dfd8d..0d5596d0777 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -10,6 +10,7 @@ + diff --git a/tests/Behat/CommandContext.php b/tests/Behat/CommandContext.php index 7ecf68803e1..666f387410a 100644 --- a/tests/Behat/CommandContext.php +++ b/tests/Behat/CommandContext.php @@ -16,6 +16,7 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; +use GraphQL\Error\Error; use PHPUnit\Framework\Assert; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Command\Command; @@ -70,7 +71,12 @@ public function theCommandOutputShouldBe(PyStringNode $expectedOutput): void */ public function theCommandOutputShouldContain(PyStringNode $expectedOutput): void { - $expectedOutput = str_replace('###', '"""', $expectedOutput->getRaw()); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_GRAPHQL')) { + $expectedOutput = str_replace('###', '"""', $expectedOutput->getRaw()); + } else { + $expectedOutput = str_replace('###', '"', $expectedOutput->getRaw()); + } Assert::assertStringContainsString($expectedOutput, $this->commandTester->getDisplay()); } diff --git a/tests/Behat/GraphqlContext.php b/tests/Behat/GraphqlContext.php index d874179806a..4d02b8c9345 100644 --- a/tests/Behat/GraphqlContext.php +++ b/tests/Behat/GraphqlContext.php @@ -20,6 +20,7 @@ use Behat\Gherkin\Node\TableNode; use Behatch\Context\RestContext; use Behatch\HttpCall\Request; +use GraphQL\Error\Error; use GraphQL\Type\Introspection; use PHPUnit\Framework\ExpectationFailedException; @@ -31,6 +32,7 @@ final class GraphqlContext implements Context { private ?RestContext $restContext = null; + private ?JsonContext $jsonContext = null; private array $graphqlRequest; @@ -47,15 +49,14 @@ public function __construct(private readonly Request $request) */ public function gatherContexts(BeforeScenarioScope $scope): void { - /** - * @var InitializedContextEnvironment $environment - */ + /** @var InitializedContextEnvironment $environment */ $environment = $scope->getEnvironment(); - /** - * @var RestContext $restContext - */ + /** @var RestContext $restContext */ $restContext = $environment->getContext(RestContext::class); $this->restContext = $restContext; + /** @var JsonContext $jsonContext */ + $jsonContext = $environment->getContext(JsonContext::class); + $this->jsonContext = $jsonContext; } /** @@ -156,6 +157,20 @@ public function theGraphQLFieldIsDeprecatedForTheReason(string $fieldName, strin throw new ExpectationFailedException(sprintf('The field "%s" is not deprecated.', $fieldName)); } + /** + * @Then the GraphQL debug message should be equal to :expectedDebugMessage + */ + public function theGraphQLDebugMessageShouldBeEqualTo(string $expectedDebugMessage): void + { + $jsonNode = 'errors[0].extensions.debugMessage'; + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_INTERNAL')) { + $jsonNode = 'errors[0].debugMessage'; + } + + $this->jsonContext->theJsonNodeShouldBeEqualTo($jsonNode, $expectedDebugMessage); + } + private function sendGraphqlRequest(): void { $this->restContext->iSendARequestTo('GET', '/graphql?'.http_build_query($this->graphqlRequest)); diff --git a/tests/Fixtures/TestBundle/Document/UuidIdentifierDummy.php b/tests/Fixtures/TestBundle/Document/UuidIdentifierDummy.php index e5cabc151f8..d586b4dd4af 100644 --- a/tests/Fixtures/TestBundle/Document/UuidIdentifierDummy.php +++ b/tests/Fixtures/TestBundle/Document/UuidIdentifierDummy.php @@ -26,7 +26,7 @@ class UuidIdentifierDummy /** * @var string The custom identifier */ - #[ODM\Id(strategy: 'UUID')] + #[ODM\Id(strategy: 'none', type: 'string')] private ?string $uuid = null; /** * @var string The dummy name diff --git a/tests/Fixtures/TestBundle/Document/WritableId.php b/tests/Fixtures/TestBundle/Document/WritableId.php index 096b7c1e43f..9f691332c89 100644 --- a/tests/Fixtures/TestBundle/Document/WritableId.php +++ b/tests/Fixtures/TestBundle/Document/WritableId.php @@ -25,7 +25,7 @@ class WritableId { #[Assert\Uuid] - #[ODM\Id(strategy: 'UUID', type: 'string')] + #[ODM\Id(strategy: 'none', type: 'string')] public $id; #[ODM\Field] public $name; diff --git a/tests/GraphQl/Action/EntrypointActionTest.php b/tests/GraphQl/Action/EntrypointActionTest.php index 2d74dc88b8f..8aadf8498c6 100644 --- a/tests/GraphQl/Action/EntrypointActionTest.php +++ b/tests/GraphQl/Action/EntrypointActionTest.php @@ -22,6 +22,7 @@ use ApiPlatform\GraphQl\Serializer\Exception\HttpExceptionNormalizer; use ApiPlatform\GraphQl\Type\SchemaBuilderInterface; use GraphQL\Error\DebugFlag; +use GraphQL\Error\Error; use GraphQL\Executor\ExecutionResult; use GraphQL\Type\Schema; use PHPUnit\Framework\TestCase; @@ -152,49 +153,49 @@ public function multipartRequestProvider(): array '{"file": ["variables.file"]}', ['file' => $file], ['file' => $file], - new Response('{"errors":[{"message":"GraphQL multipart request does not respect the specification.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL multipart request does not respect the specification.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL multipart request does not respect the specification.","extensions":{"status":400}}]}'), ], 'upload without providing map' => [ '{"query": "graphqlQuery", "variables": {"file": null}, "operationName": "graphqlOperationName"}', null, ['file' => $file], ['file' => null], - new Response('{"errors":[{"message":"GraphQL multipart request does not respect the specification.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL multipart request does not respect the specification.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL multipart request does not respect the specification.","extensions":{"status":400}}]}'), ], 'upload with invalid json' => [ '{invalid}', '{"file": ["file"]}', ['file' => $file], ['file' => null], - new Response('{"errors":[{"message":"GraphQL data is not valid JSON.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL data is not valid JSON.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL data is not valid JSON.","extensions":{"status":400}}]}'), ], 'upload with invalid map JSON' => [ '{"query": "graphqlQuery", "variables": {"file": null}, "operationName": "graphqlOperationName"}', '{invalid}', ['file' => $file], ['file' => null], - new Response('{"errors":[{"message":"GraphQL multipart request map is not valid JSON.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL multipart request map is not valid JSON.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL multipart request map is not valid JSON.","extensions":{"status":400}}]}'), ], 'upload with no file' => [ '{"query": "graphqlQuery", "variables": {"file": null}, "operationName": "graphqlOperationName"}', '{"file": ["file"]}', [], ['file' => null], - new Response('{"errors":[{"message":"GraphQL multipart request file has not been sent correctly.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL multipart request file has not been sent correctly.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL multipart request file has not been sent correctly.","extensions":{"status":400}}]}'), ], 'upload with wrong map' => [ '{"query": "graphqlQuery", "variables": {"file": null}, "operationName": "graphqlOperationName"}', '{"file": ["file"]}', ['file' => $file], ['file' => null], - new Response('{"errors":[{"message":"GraphQL multipart request path in map is invalid.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL multipart request path in map is invalid.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL multipart request path in map is invalid.","extensions":{"status":400}}]}'), ], 'upload when variable path does not exist' => [ '{"query": "graphqlQuery", "variables": {"file": null}, "operationName": "graphqlOperationName"}', '{"file": ["variables.wrong"]}', ['file' => $file], ['file' => null], - new Response('{"errors":[{"message":"GraphQL multipart request path in map does not match the variables.","extensions":{"category":"user","status":400}}]}'), + new Response(\defined(Error::class.'::CATEGORY_GRAPHQL') ? '{"errors":[{"message":"GraphQL multipart request path in map does not match the variables.","extensions":{"category":"user","status":400}}]}' : '{"errors":[{"message":"GraphQL multipart request path in map does not match the variables.","extensions":{"status":400}}]}'), ], ]; } @@ -207,7 +208,12 @@ public function testBadContentTypePostAction(): void $mockedEntrypoint = $this->getEntrypointAction(); $this->assertSame(200, $mockedEntrypoint($request)->getStatusCode()); - $this->assertSame('{"errors":[{"message":"GraphQL query is not valid.","extensions":{"category":"user","status":400}}]}', $mockedEntrypoint($request)->getContent()); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_GRAPHQL')) { + $this->assertSame('{"errors":[{"message":"GraphQL query is not valid.","extensions":{"category":"user","status":400}}]}', $mockedEntrypoint($request)->getContent()); + } else { + $this->assertSame('{"errors":[{"message":"GraphQL query is not valid.","extensions":{"status":400}}]}', $mockedEntrypoint($request)->getContent()); + } } public function testBadMethodAction(): void @@ -217,7 +223,12 @@ public function testBadMethodAction(): void $mockedEntrypoint = $this->getEntrypointAction(); $this->assertSame(200, $mockedEntrypoint($request)->getStatusCode()); - $this->assertSame('{"errors":[{"message":"GraphQL query is not valid.","extensions":{"category":"user","status":400}}]}', $mockedEntrypoint($request)->getContent()); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_GRAPHQL')) { + $this->assertSame('{"errors":[{"message":"GraphQL query is not valid.","extensions":{"category":"user","status":400}}]}', $mockedEntrypoint($request)->getContent()); + } else { + $this->assertSame('{"errors":[{"message":"GraphQL query is not valid.","extensions":{"status":400}}]}', $mockedEntrypoint($request)->getContent()); + } } public function testBadVariablesAction(): void @@ -227,7 +238,12 @@ public function testBadVariablesAction(): void $mockedEntrypoint = $this->getEntrypointAction(); $this->assertSame(200, $mockedEntrypoint($request)->getStatusCode()); - $this->assertSame('{"errors":[{"message":"GraphQL variables are not valid JSON.","extensions":{"category":"user","status":400}}]}', $mockedEntrypoint($request)->getContent()); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_GRAPHQL')) { + $this->assertSame('{"errors":[{"message":"GraphQL variables are not valid JSON.","extensions":{"category":"user","status":400}}]}', $mockedEntrypoint($request)->getContent()); + } else { + $this->assertSame('{"errors":[{"message":"GraphQL variables are not valid JSON.","extensions":{"status":400}}]}', $mockedEntrypoint($request)->getContent()); + } } private function getEntrypointAction(array $variables = ['graphqlVariable']): EntrypointAction diff --git a/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php b/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php index 90e873e8d50..d3b19af325d 100644 --- a/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php +++ b/tests/GraphQl/Resolver/Factory/ItemResolverFactoryTest.php @@ -72,6 +72,7 @@ public function testResolve(?string $resourceClass, string $determinedResourceCl $source = ['source']; $args = ['args']; $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); @@ -130,6 +131,7 @@ public function testResolveBadReadStageItem(): void $source = ['source']; $args = ['args']; $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; $readStageItem = []; @@ -150,6 +152,7 @@ public function testResolveNoResourceNoItem(): void $source = ['source']; $args = ['args']; $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; $readStageItem = null; @@ -170,6 +173,7 @@ public function testResolveBadItem(): void $source = ['source']; $args = ['args']; $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; $readStageItem = new \stdClass(); @@ -190,6 +194,7 @@ public function testResolveCustom(): void $source = ['source']; $args = ['args']; $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; $readStageItem = new \stdClass(); @@ -226,6 +231,7 @@ public function testResolveCustomBadItem(): void $source = ['source']; $args = ['args']; $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; $readStageItem = new \stdClass(); diff --git a/tests/GraphQl/Resolver/ResourceFieldResolverTest.php b/tests/GraphQl/Resolver/ResourceFieldResolverTest.php index a04a42ebb6e..cb95aa23b93 100644 --- a/tests/GraphQl/Resolver/ResourceFieldResolverTest.php +++ b/tests/GraphQl/Resolver/ResourceFieldResolverTest.php @@ -18,6 +18,7 @@ use ApiPlatform\GraphQl\Resolver\ResourceFieldResolver; use ApiPlatform\GraphQl\Serializer\ItemNormalizer; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; @@ -34,7 +35,13 @@ public function testId(): void $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, null, ['uri_variables' => ['id' => 1]])->willReturn('/dummies/1')->shouldBeCalled(); - $resolveInfo = new ResolveInfo(FieldDefinition::create(['name' => 'id', 'type' => new ObjectType(['name' => ''])]), [], new ObjectType(['name' => '']), [], new Schema([]), [], null, null, []); + // graphql-php < 15 + if (method_exists(FieldDefinition::class, 'create')) { + $fieldDefinition = FieldDefinition::create(['name' => 'id', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } else { + $fieldDefinition = new FieldDefinition(['name' => 'id', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } + $resolveInfo = new ResolveInfo($fieldDefinition, new \ArrayObject(), new ObjectType(['name' => '', 'fields' => []]), [], new Schema([]), [], null, new OperationDefinitionNode([]), []); $resolver = new ResourceFieldResolver($iriConverterProphecy->reveal()); $this->assertSame('/dummies/1', $resolver([ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => Dummy::class, ItemNormalizer::ITEM_IDENTIFIERS_KEY => ['id' => 1]], [], [], $resolveInfo)); @@ -44,7 +51,13 @@ public function testOriginalId(): void { $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $resolveInfo = new ResolveInfo(FieldDefinition::create(['name' => '_id', 'type' => new ObjectType(['name' => ''])]), [], new ObjectType(['name' => '']), [], new Schema([]), [], null, null, []); + // graphql-php < 15 + if (method_exists(FieldDefinition::class, 'create')) { + $fieldDefinition = FieldDefinition::create(['name' => '_id', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } else { + $fieldDefinition = new FieldDefinition(['name' => '_id', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } + $resolveInfo = new ResolveInfo($fieldDefinition, new \ArrayObject(), new ObjectType(['name' => '', 'fields' => []]), [], new Schema([]), [], null, new OperationDefinitionNode([]), []); $resolver = new ResourceFieldResolver($iriConverterProphecy->reveal()); $this->assertSame(1, $resolver(['id' => 1], [], [], $resolveInfo)); @@ -54,7 +67,13 @@ public function testDirectAccess(): void { $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $resolveInfo = new ResolveInfo(FieldDefinition::create(['name' => 'foo', 'type' => new ObjectType(['name' => ''])]), [], new ObjectType(['name' => '']), [], new Schema([]), [], null, null, []); + // graphql-php < 15 + if (method_exists(FieldDefinition::class, 'create')) { + $fieldDefinition = FieldDefinition::create(['name' => 'foo', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } else { + $fieldDefinition = new FieldDefinition(['name' => 'foo', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } + $resolveInfo = new ResolveInfo($fieldDefinition, new \ArrayObject(), new ObjectType(['name' => '', 'fields' => []]), [], new Schema([]), [], null, new OperationDefinitionNode([]), []); $resolver = new ResourceFieldResolver($iriConverterProphecy->reveal()); $this->assertSame('bar', $resolver(['foo' => 'bar'], [], [], $resolveInfo)); @@ -66,7 +85,13 @@ public function testNonResource(): void $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummies/1')->shouldNotBeCalled(); - $resolveInfo = new ResolveInfo(FieldDefinition::create(['name' => 'id', 'type' => new ObjectType(['name' => ''])]), [], new ObjectType(['name' => '']), [], new Schema([]), [], null, null, []); + // graphql-php < 15 + if (method_exists(FieldDefinition::class, 'create')) { + $fieldDefinition = FieldDefinition::create(['name' => 'id', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } else { + $fieldDefinition = new FieldDefinition(['name' => 'id', 'type' => new ObjectType(['name' => '', 'fields' => []])]); + } + $resolveInfo = new ResolveInfo($fieldDefinition, new \ArrayObject(), new ObjectType(['name' => '', 'fields' => []]), [], new Schema([]), [], null, new OperationDefinitionNode([]), []); $resolver = new ResourceFieldResolver($iriConverterProphecy->reveal()); $this->assertNull($resolver([], [], [], $resolveInfo)); diff --git a/tests/GraphQl/Serializer/Exception/ErrorNormalizerTest.php b/tests/GraphQl/Serializer/Exception/ErrorNormalizerTest.php index 12db8ebe027..defb3c39df6 100644 --- a/tests/GraphQl/Serializer/Exception/ErrorNormalizerTest.php +++ b/tests/GraphQl/Serializer/Exception/ErrorNormalizerTest.php @@ -39,7 +39,10 @@ public function testNormalize(): void $normalizedError = $this->errorNormalizer->normalize($error); $this->assertSame($errorMessage, $normalizedError['message']); - $this->assertSame(Error::CATEGORY_GRAPHQL, $normalizedError['extensions']['category']); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_GRAPHQL')) { + $this->assertSame(Error::CATEGORY_GRAPHQL, $normalizedError['extensions']['category']); + } } public function testSupportsNormalization(): void diff --git a/tests/GraphQl/Serializer/Exception/HttpExceptionNormalizerTest.php b/tests/GraphQl/Serializer/Exception/HttpExceptionNormalizerTest.php index 2405294b761..d029623ecb5 100644 --- a/tests/GraphQl/Serializer/Exception/HttpExceptionNormalizerTest.php +++ b/tests/GraphQl/Serializer/Exception/HttpExceptionNormalizerTest.php @@ -45,7 +45,10 @@ public function testNormalize(HttpException $exception, string $expectedExceptio $normalizedError = $this->httpExceptionNormalizer->normalize($error); $this->assertSame($expectedExceptionMessage, $normalizedError['message']); $this->assertSame($expectedStatus, $normalizedError['extensions']['status']); - $this->assertSame($expectedCategory, $normalizedError['extensions']['category']); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_INTERNAL')) { + $this->assertSame($expectedCategory, $normalizedError['extensions']['category']); + } } public function exceptionProvider(): array @@ -54,7 +57,7 @@ public function exceptionProvider(): array return [ 'client error' => [new BadRequestHttpException($exceptionMessage), $exceptionMessage, 400, 'user'], - 'server error' => [new ServiceUnavailableHttpException(null, $exceptionMessage), $exceptionMessage, 503, Error::CATEGORY_INTERNAL], + 'server error' => [new ServiceUnavailableHttpException(null, $exceptionMessage), $exceptionMessage, 503, 'internal'], ]; } diff --git a/tests/GraphQl/Serializer/Exception/RuntimeExceptionNormalizerTest.php b/tests/GraphQl/Serializer/Exception/RuntimeExceptionNormalizerTest.php index 0135b37eb88..c4d92a8ffc2 100644 --- a/tests/GraphQl/Serializer/Exception/RuntimeExceptionNormalizerTest.php +++ b/tests/GraphQl/Serializer/Exception/RuntimeExceptionNormalizerTest.php @@ -40,7 +40,10 @@ public function testNormalize(): void $normalizedError = $this->runtimeExceptionNormalizer->normalize($error); $this->assertSame($exceptionMessage, $normalizedError['message']); - $this->assertSame(Error::CATEGORY_INTERNAL, $normalizedError['extensions']['category']); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_INTERNAL')) { + $this->assertSame(Error::CATEGORY_INTERNAL, $normalizedError['extensions']['category']); + } } public function testSupportsNormalization(): void diff --git a/tests/GraphQl/Serializer/Exception/ValidationExceptionNormalizerTest.php b/tests/GraphQl/Serializer/Exception/ValidationExceptionNormalizerTest.php index cbeb181b009..15ac2fdbc84 100644 --- a/tests/GraphQl/Serializer/Exception/ValidationExceptionNormalizerTest.php +++ b/tests/GraphQl/Serializer/Exception/ValidationExceptionNormalizerTest.php @@ -47,7 +47,10 @@ public function testNormalize(): void $normalizedError = $this->validationExceptionNormalizer->normalize($error); $this->assertSame($exceptionMessage, $normalizedError['message']); $this->assertSame(422, $normalizedError['extensions']['status']); - $this->assertSame('user', $normalizedError['extensions']['category']); + // graphql-php < 15 + if (\defined(Error::class.'::CATEGORY_INTERNAL')) { + $this->assertSame('user', $normalizedError['extensions']['category']); + } $this->assertArrayHasKey('violations', $normalizedError['extensions']); $this->assertEquals([ [ diff --git a/tests/GraphQl/Type/Definition/IterableTypeTest.php b/tests/GraphQl/Type/Definition/IterableTypeTest.php index bf7965b47e3..1ba2540590b 100644 --- a/tests/GraphQl/Type/Definition/IterableTypeTest.php +++ b/tests/GraphQl/Type/Definition/IterableTypeTest.php @@ -67,7 +67,7 @@ public function testParseLiteral(): void $iterableType = new IterableType(); $this->expectException(\Exception::class); - $iterableType->parseLiteral(new IntValueNode(['value' => 1])); + $iterableType->parseLiteral(new IntValueNode(['value' => '1'])); $listValueNode = new ListValueNode(['values' => []]); $this->assertEquals([], $iterableType->parseLiteral($listValueNode)); diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index 853b962223f..8b79d09f359 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -139,7 +139,7 @@ public function itemQueryFieldsProvider(): array { return [ 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nested item query' => ['resourceClass', (new Query())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], new ObjectType(['name' => 'item']), function (): void {}, []], + 'nested item query' => ['resourceClass', (new Query())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], new ObjectType(['name' => 'item', 'fields' => []]), function (): void {}, []], 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ @@ -153,7 +153,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item', 'fields' => []]), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -233,8 +233,8 @@ public function collectionQueryFieldsProvider(): array { return [ 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nested collection query' => ['resourceClass', (new QueryCollection())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], GraphQLType::listOf(new ObjectType(['name' => 'collection'])), function (): void {}, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'nested collection query' => ['resourceClass', (new QueryCollection())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), function (): void {}, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -263,7 +263,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -297,7 +297,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -311,7 +311,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -327,7 +327,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -370,7 +370,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -390,7 +390,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -433,9 +433,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription', 'fields' => []]), new ObjectType(['name' => 'input', 'fields' => []]), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -455,7 +455,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -496,14 +496,14 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); if ('propertyObject' === $propertyName) { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } if ('propertyNestedResource' === $propertyName) { $nestedResourceQueryOperation = new Query(); $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { }); } @@ -623,7 +623,7 @@ public function resourceObjectTypeFieldsProvider(): array 'type' => GraphQLType::nonNull(GraphQLType::id()), ], 'propertyNestedResource' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType', 'fields' => []])), 'description' => null, 'args' => [], 'resolve' => static function (): void { @@ -652,7 +652,7 @@ public function resourceObjectTypeFieldsProvider(): array 'deprecationReason' => null, ], 'propertyObject' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType', 'fields' => []])), 'description' => null, 'args' => [], 'resolve' => static function (): void { diff --git a/tests/GraphQl/Type/SchemaBuilderTest.php b/tests/GraphQl/Type/SchemaBuilderTest.php index 1850951d402..01a972c0248 100644 --- a/tests/GraphQl/Type/SchemaBuilderTest.php +++ b/tests/GraphQl/Type/SchemaBuilderTest.php @@ -28,6 +28,7 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Resource\ResourceNameCollection; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\StringType; use GraphQL\Type\Definition\Type as GraphQLType; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -66,13 +67,11 @@ protected function setUp(): void */ public function testGetSchema(string $resourceClass, ResourceMetadataCollection $resourceMetadata, ObjectType $expectedQueryType, ?ObjectType $expectedMutationType, ?ObjectType $expectedSubscriptionType): void { - $type = $this->prophesize(GraphQLType::class)->reveal(); - $type->name = 'MyType'; + $type = new StringType(['name' => 'MyType']); $this->typesFactoryProphecy->getTypes()->shouldBeCalled()->willReturn(['typeId' => $type]); $this->typesContainerProphecy->set('typeId', $type)->shouldBeCalled(); $this->typesContainerProphecy->get('MyType')->willReturn($type); - $typeFoo = $this->prophesize(GraphQLType::class)->reveal(); - $typeFoo->name = 'Foo'; + $typeFoo = new StringType(['name' => 'Foo']); $this->typesContainerProphecy->get('Foo')->willReturn(GraphQLType::listOf($typeFoo)); $this->fieldsBuilderProphecy->getNodeQueryFields()->shouldBeCalled()->willReturn(['node_fields']); $this->fieldsBuilderProphecy->getItemQueryFields($resourceClass, Argument::that(static fn (Operation $arg): bool => 'item_query' === $arg->getName()), [])->willReturn(['query' => ['query_fields']]); @@ -85,6 +84,14 @@ public function testGetSchema(string $resourceClass, ResourceMetadataCollection $this->resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([$resourceClass])); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn($resourceMetadata); + $this->typesContainerProphecy->set('Query', $expectedQueryType)->shouldBeCalled(); + if ($expectedMutationType) { + $this->typesContainerProphecy->set('Mutation', $expectedMutationType)->shouldBeCalled(); + } + if ($expectedSubscriptionType) { + $this->typesContainerProphecy->set('Subscription', $expectedSubscriptionType)->shouldBeCalled(); + } + $schema = $this->schemaBuilder->getSchema(); $this->assertEquals($expectedQueryType, $schema->getQueryType()); $this->assertEquals($expectedMutationType, $schema->getMutationType()); diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index fd1265d1042..743f63507da 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -184,7 +184,6 @@ public function testGetResourceObjectTypeInput(): void $this->assertInstanceOf(InputObjectType::class, $wrappedType); $this->assertSame('customShortNameInput', $wrappedType->name); $this->assertSame('description', $wrappedType->description); - $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); @@ -210,7 +209,6 @@ public function testGetResourceObjectTypeNestedInput(): void $this->assertInstanceOf(InputObjectType::class, $wrappedType); $this->assertSame('customShortNameNestedInput', $wrappedType->name); $this->assertSame('description', $wrappedType->description); - $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); @@ -236,7 +234,6 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void $this->assertInstanceOf(InputObjectType::class, $wrappedType); $this->assertSame('customShortNameInput', $wrappedType->name); $this->assertSame('description', $wrappedType->description); - $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php index 500fdd03921..891d331a0ee 100644 --- a/tests/GraphQl/Type/TypeConverterTest.php +++ b/tests/GraphQl/Type/TypeConverterTest.php @@ -86,7 +86,7 @@ public function convertTypeProvider(): array [new Type(Type::BUILTIN_TYPE_ARRAY), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_ITERABLE), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeInterface::class), false, 0, GraphQLType::string()], - [new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum'])], + [new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum', 'values' => []])], [new Type(Type::BUILTIN_TYPE_OBJECT), false, 0, null], [new Type(Type::BUILTIN_TYPE_CALLABLE), false, 0, null], [new Type(Type::BUILTIN_TYPE_NULL), false, 0, null], @@ -156,7 +156,7 @@ public function testConvertTypeInputResource(): void /** @var Operation $operation */ $operation = new Query(); $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['item_query' => $operation])]); - $expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType']); + $expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType', 'fields' => []]); $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata); $this->typeBuilderProphecy->isCollection($type)->willReturn(false); @@ -190,8 +190,8 @@ public function testConvertTypeCollectionResource(Type $type, ObjectType $expect public function convertTypeResourceProvider(): array { return [ - [new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType'])], - [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType'])], + [new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], + [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], ]; }