graphql-php/tests/Server/QueryExecutionTest.php

691 lines
21 KiB
PHP
Raw Normal View History

<?php
2018-09-01 23:00:00 +03:00
declare(strict_types=1);
namespace GraphQL\Tests\Server;
use GraphQL\Error\Debug;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Server\Helper;
use GraphQL\Server\OperationParams;
use GraphQL\Server\RequestError;
use GraphQL\Server\ServerConfig;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\CustomValidationRule;
use GraphQL\Validator\ValidationContext;
2018-09-26 12:07:23 +03:00
use stdClass;
2018-09-01 23:00:00 +03:00
use function count;
use function sprintf;
2018-07-29 18:43:10 +03:00
class QueryExecutionTest extends ServerTestCase
{
2018-09-01 23:00:00 +03:00
/** @var ServerConfig */
private $config;
public function setUp()
{
2018-09-01 23:00:00 +03:00
$schema = $this->buildSchema();
$this->config = ServerConfig::create()
->setSchema($schema);
}
public function testSimpleQueryExecution() : void
{
$query = '{f1}';
$expected = [
2018-09-01 23:00:00 +03:00
'data' => ['f1' => 'f1'],
];
$this->assertQueryResultEquals($expected, $query);
}
2018-09-01 23:00:00 +03:00
private function assertQueryResultEquals($expected, $query, $variables = null)
{
$result = $this->executeQuery($query, $variables);
2018-09-19 18:12:09 +03:00
self::assertArraySubset($expected, $result->toArray(true));
2018-09-01 23:00:00 +03:00
return $result;
}
private function executeQuery($query, $variables = null, $readonly = false)
{
$op = OperationParams::create(['query' => $query, 'variables' => $variables], $readonly);
$helper = new Helper();
$result = $helper->executeOperation($this->config, $op);
2018-09-19 18:12:09 +03:00
self::assertInstanceOf(ExecutionResult::class, $result);
2018-09-01 23:00:00 +03:00
return $result;
}
public function testReturnsSyntaxErrors() : void
{
$query = '{f1';
$result = $this->executeQuery($query);
2018-09-19 18:12:09 +03:00
self::assertNull($result->data);
self::assertCount(1, $result->errors);
self::assertContains(
'Syntax Error: Expected Name, found <EOF>',
$result->errors[0]->getMessage()
);
}
public function testDebugExceptions() : void
{
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
$this->config->setDebug($debug);
$query = '
{
fieldWithSafeException
f1
}
';
$expected = [
'data' => [
'fieldWithSafeException' => null,
2018-09-01 23:00:00 +03:00
'f1' => 'f1',
],
'errors' => [
[
'message' => 'This is the exception we want',
'path' => ['fieldWithSafeException'],
2018-09-03 22:43:02 +03:00
'trace' => [],
],
],
];
$result = $this->executeQuery($query)->toArray();
2018-09-19 18:12:09 +03:00
self::assertArraySubset($expected, $result);
}
2018-09-03 22:43:02 +03:00
public function testRethrowUnsafeExceptions() : void
{
$this->config->setDebug(Debug::RETHROW_UNSAFE_EXCEPTIONS);
2018-09-03 22:43:02 +03:00
$this->expectException(Unsafe::class);
$this->executeQuery('
{
fieldWithUnsafeException
}
')->toArray();
}
public function testPassesRootValueAndContext() : void
{
$rootValue = 'myRootValue';
2018-09-26 12:07:23 +03:00
$context = new stdClass();
$this->config
->setContext($context)
->setRootValue($rootValue);
$query = '
{
testContextAndRootValue
}
';
2018-09-19 18:12:09 +03:00
self::assertTrue(! isset($context->testedRootValue));
$this->executeQuery($query);
2018-09-19 18:12:09 +03:00
self::assertSame($rootValue, $context->testedRootValue);
}
public function testPassesVariables() : void
{
$variables = ['a' => 'a', 'b' => 'b'];
2018-09-01 23:00:00 +03:00
$query = '
query ($a: String!, $b: String!) {
a: fieldWithArg(arg: $a)
b: fieldWithArg(arg: $b)
}
';
2018-09-01 23:00:00 +03:00
$expected = [
'data' => [
'a' => 'a',
2018-09-01 23:00:00 +03:00
'b' => 'b',
],
];
$this->assertQueryResultEquals($expected, $query, $variables);
}
public function testPassesCustomValidationRules() : void
{
2018-09-01 23:00:00 +03:00
$query = '
{nonExistentField}
';
$expected = [
'errors' => [
2018-09-01 23:00:00 +03:00
['message' => 'Cannot query field "nonExistentField" on type "Query".'],
],
];
$this->assertQueryResultEquals($expected, $query);
$called = false;
$rules = [
2018-09-26 12:07:23 +03:00
new CustomValidationRule('SomeRule', static function () use (&$called) {
$called = true;
2018-09-01 23:00:00 +03:00
return [];
2018-09-01 23:00:00 +03:00
}),
];
$this->config->setValidationRules($rules);
$expected = [
2018-09-01 23:00:00 +03:00
'data' => [],
];
$this->assertQueryResultEquals($expected, $query);
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
}
public function testAllowsValidationRulesAsClosure() : void
{
$called = false;
$params = $doc = $operationType = null;
2018-09-26 12:07:23 +03:00
$this->config->setValidationRules(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
2018-09-01 23:00:00 +03:00
$called = true;
$params = $p;
$doc = $d;
$operationType = $o;
2018-09-01 23:00:00 +03:00
return [];
});
2018-09-19 18:12:09 +03:00
self::assertFalse($called);
$this->executeQuery('{f1}');
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
self::assertInstanceOf(OperationParams::class, $params);
self::assertInstanceOf(DocumentNode::class, $doc);
self::assertEquals('query', $operationType);
}
public function testAllowsDifferentValidationRulesDependingOnOperation() : void
{
2018-09-01 23:00:00 +03:00
$q1 = '{f1}';
$q2 = '{invalid}';
$called1 = false;
$called2 = false;
2018-09-26 12:07:23 +03:00
$this->config->setValidationRules(static function (OperationParams $params) use ($q1, &$called1, &$called2) {
if ($params->query === $q1) {
$called1 = true;
2018-09-01 23:00:00 +03:00
return DocumentValidator::allRules();
}
2018-09-01 23:00:00 +03:00
$called2 = true;
return [
2018-09-26 12:07:23 +03:00
new CustomValidationRule('MyRule', static function (ValidationContext $context) {
2018-09-01 23:00:00 +03:00
$context->reportError(new Error('This is the error we are looking for!'));
}),
];
});
$expected = ['data' => ['f1' => 'f1']];
$this->assertQueryResultEquals($expected, $q1);
2018-09-19 18:12:09 +03:00
self::assertTrue($called1);
self::assertFalse($called2);
2018-09-01 23:00:00 +03:00
$called1 = false;
$called2 = false;
$expected = ['errors' => [['message' => 'This is the error we are looking for!']]];
$this->assertQueryResultEquals($expected, $q2);
2018-09-19 18:12:09 +03:00
self::assertFalse($called1);
self::assertTrue($called2);
}
public function testAllowsSkippingValidation() : void
{
$this->config->setValidationRules([]);
2018-09-01 23:00:00 +03:00
$query = '{nonExistentField}';
$expected = ['data' => []];
$this->assertQueryResultEquals($expected, $query);
}
public function testPersistedQueriesAreDisabledByDefault() : void
{
$result = $this->executePersistedQuery('some-id');
$expected = [
'errors' => [
[
2018-09-01 23:00:00 +03:00
'message' => 'Persisted queries are not supported by this server',
'extensions' => ['category' => 'request'],
2018-09-01 23:00:00 +03:00
],
],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected, $result->toArray());
}
2018-09-01 23:00:00 +03:00
private function executePersistedQuery($queryId, $variables = null)
{
$op = OperationParams::create(['queryId' => $queryId, 'variables' => $variables]);
$helper = new Helper();
$result = $helper->executeOperation($this->config, $op);
2018-09-19 18:12:09 +03:00
self::assertInstanceOf(ExecutionResult::class, $result);
2018-09-01 23:00:00 +03:00
return $result;
}
public function testBatchedQueriesAreDisabledByDefault() : void
{
$batch = [
2018-09-01 23:00:00 +03:00
['query' => '{invalid}'],
2018-09-03 22:25:36 +03:00
['query' => '{f1,fieldWithSafeException}'],
];
$result = $this->executeBatchedQuery($batch);
$expected = [
[
'errors' => [
[
2018-09-01 23:00:00 +03:00
'message' => 'Batched queries are not supported by this server',
'extensions' => ['category' => 'request'],
2018-09-01 23:00:00 +03:00
],
],
],
[
'errors' => [
[
2018-09-01 23:00:00 +03:00
'message' => 'Batched queries are not supported by this server',
'extensions' => ['category' => 'request'],
2018-09-01 23:00:00 +03:00
],
],
],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected[0], $result[0]->toArray());
self::assertEquals($expected[1], $result[1]->toArray());
}
2018-09-01 23:00:00 +03:00
/**
* @param mixed[][] $qs
*/
private function executeBatchedQuery(array $qs)
{
$batch = [];
foreach ($qs as $params) {
$batch[] = OperationParams::create($params);
}
$helper = new Helper();
$result = $helper->executeBatch($this->config, $batch);
2018-09-19 18:12:09 +03:00
self::assertInternalType('array', $result);
self::assertCount(count($qs), $result);
2018-09-01 23:00:00 +03:00
foreach ($result as $index => $entry) {
2018-09-19 18:12:09 +03:00
self::assertInstanceOf(
2018-09-01 23:00:00 +03:00
ExecutionResult::class,
$entry,
sprintf('Result at %s is not an instance of %s', $index, ExecutionResult::class)
);
}
return $result;
}
public function testMutationsAreNotAllowedInReadonlyMode() : void
{
$mutation = 'mutation { a }';
$expected = [
'errors' => [
[
2018-09-01 23:00:00 +03:00
'message' => 'GET supports only query operation',
'extensions' => ['category' => 'request'],
2018-09-01 23:00:00 +03:00
],
],
];
$result = $this->executeQuery($mutation, null, true);
2018-09-19 18:12:09 +03:00
self::assertEquals($expected, $result->toArray());
}
public function testAllowsPersistentQueries() : void
{
$called = false;
2018-09-26 12:07:23 +03:00
$this->config->setPersistentQueryLoader(static function ($queryId, OperationParams $params) use (&$called) {
$called = true;
2018-09-19 18:12:09 +03:00
self::assertEquals('some-id', $queryId);
2018-09-01 23:00:00 +03:00
return '{f1}';
});
$result = $this->executePersistedQuery('some-id');
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
$expected = [
2018-09-01 23:00:00 +03:00
'data' => ['f1' => 'f1'],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected, $result->toArray());
// Make sure it allows returning document node:
$called = false;
2018-09-26 12:07:23 +03:00
$this->config->setPersistentQueryLoader(static function ($queryId, OperationParams $params) use (&$called) {
$called = true;
2018-09-19 18:12:09 +03:00
self::assertEquals('some-id', $queryId);
2018-09-01 23:00:00 +03:00
return Parser::parse('{f1}');
});
$result = $this->executePersistedQuery('some-id');
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
self::assertEquals($expected, $result->toArray());
}
public function testProhibitsInvalidPersistedQueryLoader() : void
{
2018-07-29 18:43:10 +03:00
$this->expectException(InvariantViolation::class);
$this->expectExceptionMessage(
'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode ' .
'but got: {"err":"err"}'
);
2018-09-26 12:07:23 +03:00
$this->config->setPersistentQueryLoader(static function () {
return ['err' => 'err'];
});
$this->executePersistedQuery('some-id');
}
public function testPersistedQueriesAreStillValidatedByDefault() : void
{
2018-09-26 12:07:23 +03:00
$this->config->setPersistentQueryLoader(static function () {
return '{invalid}';
});
2018-09-01 23:00:00 +03:00
$result = $this->executePersistedQuery('some-id');
$expected = [
'errors' => [
[
2018-09-01 23:00:00 +03:00
'message' => 'Cannot query field "invalid" on type "Query".',
'locations' => [['line' => 1, 'column' => 2]],
'extensions' => ['category' => 'graphql'],
2018-09-01 23:00:00 +03:00
],
],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected, $result->toArray());
}
public function testAllowSkippingValidationForPersistedQueries() : void
{
$this->config
2018-09-26 12:07:23 +03:00
->setPersistentQueryLoader(static function ($queryId) {
if ($queryId === 'some-id') {
return '{invalid}';
}
2018-09-01 23:00:00 +03:00
return '{invalid2}';
})
2018-09-26 12:07:23 +03:00
->setValidationRules(static function (OperationParams $params) {
if ($params->queryId === 'some-id') {
return [];
}
2018-09-01 23:00:00 +03:00
return DocumentValidator::allRules();
});
2018-09-01 23:00:00 +03:00
$result = $this->executePersistedQuery('some-id');
$expected = [
2018-09-01 23:00:00 +03:00
'data' => [],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected, $result->toArray());
2018-09-01 23:00:00 +03:00
$result = $this->executePersistedQuery('some-other-id');
$expected = [
'errors' => [
[
2018-09-01 23:00:00 +03:00
'message' => 'Cannot query field "invalid2" on type "Query".',
'locations' => [['line' => 1, 'column' => 2]],
'extensions' => ['category' => 'graphql'],
2018-09-01 23:00:00 +03:00
],
],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected, $result->toArray());
}
public function testProhibitsUnexpectedValidationRules() : void
{
2018-07-29 18:43:10 +03:00
$this->expectException(InvariantViolation::class);
$this->expectExceptionMessage('Expecting validation rules to be array or callable returning array, but got: instance of stdClass');
2018-09-26 12:07:23 +03:00
$this->config->setValidationRules(static function (OperationParams $params) {
return new stdClass();
});
$this->executeQuery('{f1}');
}
public function testExecutesBatchedQueries() : void
{
$this->config->setQueryBatching(true);
$batch = [
2018-09-01 23:00:00 +03:00
['query' => '{invalid}'],
2018-09-03 22:25:36 +03:00
['query' => '{f1,fieldWithSafeException}'],
[
2018-09-01 23:00:00 +03:00
'query' => '
query ($a: String!, $b: String!) {
a: fieldWithArg(arg: $a)
b: fieldWithArg(arg: $b)
}
',
'variables' => ['a' => 'a', 'b' => 'b'],
2018-09-01 23:00:00 +03:00
],
];
$result = $this->executeBatchedQuery($batch);
$expected = [
[
2018-09-01 23:00:00 +03:00
'errors' => [['message' => 'Cannot query field "invalid" on type "Query".']],
],
[
'data' => [
'f1' => 'f1',
2018-09-03 22:43:02 +03:00
'fieldWithSafeException' => null,
],
'errors' => [
2018-09-01 23:00:00 +03:00
['message' => 'This is the exception we want'],
],
],
[
'data' => [
'a' => 'a',
2018-09-01 23:00:00 +03:00
'b' => 'b',
],
],
];
2018-09-19 18:12:09 +03:00
self::assertArraySubset($expected[0], $result[0]->toArray());
self::assertArraySubset($expected[1], $result[1]->toArray());
self::assertArraySubset($expected[2], $result[2]->toArray());
}
public function testDeferredsAreSharedAmongAllBatchedQueries() : void
{
$batch = [
2018-09-01 23:00:00 +03:00
['query' => '{dfd(num: 1)}'],
['query' => '{dfd(num: 2)}'],
['query' => '{dfd(num: 3)}'],
];
$calls = [];
$this->config
->setQueryBatching(true)
->setRootValue('1')
->setContext([
2018-09-26 12:07:23 +03:00
'buffer' => static function ($num) use (&$calls) {
2018-09-01 23:00:00 +03:00
$calls[] = sprintf('buffer: %d', $num);
},
2018-09-26 12:07:23 +03:00
'load' => static function ($num) use (&$calls) {
2018-09-01 23:00:00 +03:00
$calls[] = sprintf('load: %d', $num);
return sprintf('loaded: %d', $num);
},
]);
$result = $this->executeBatchedQuery($batch);
$expectedCalls = [
'buffer: 1',
'buffer: 2',
'buffer: 3',
'load: 1',
'load: 2',
'load: 3',
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expectedCalls, $calls);
$expected = [
[
2018-09-01 23:00:00 +03:00
'data' => ['dfd' => 'loaded: 1'],
],
[
2018-09-01 23:00:00 +03:00
'data' => ['dfd' => 'loaded: 2'],
],
[
2018-09-01 23:00:00 +03:00
'data' => ['dfd' => 'loaded: 3'],
],
];
2018-09-19 18:12:09 +03:00
self::assertEquals($expected[0], $result[0]->toArray());
self::assertEquals($expected[1], $result[1]->toArray());
self::assertEquals($expected[2], $result[2]->toArray());
}
public function testValidatesParamsBeforeExecution() : void
{
2018-09-01 23:00:00 +03:00
$op = OperationParams::create(['queryBad' => '{f1}']);
$helper = new Helper();
$result = $helper->executeOperation($this->config, $op);
2018-09-19 18:12:09 +03:00
self::assertInstanceOf(ExecutionResult::class, $result);
2018-09-19 18:12:09 +03:00
self::assertEquals(null, $result->data);
self::assertCount(1, $result->errors);
2018-09-19 18:12:09 +03:00
self::assertEquals(
'GraphQL Request must include at least one of those two parameters: "query" or "queryId"',
$result->errors[0]->getMessage()
);
2018-09-19 18:12:09 +03:00
self::assertInstanceOf(
RequestError::class,
$result->errors[0]->getPrevious()
);
}
public function testAllowsContextAsClosure() : void
{
$called = false;
$params = $doc = $operationType = null;
2018-09-26 12:07:23 +03:00
$this->config->setContext(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
2018-09-01 23:00:00 +03:00
$called = true;
$params = $p;
$doc = $d;
$operationType = $o;
});
2018-09-19 18:12:09 +03:00
self::assertFalse($called);
$this->executeQuery('{f1}');
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
self::assertInstanceOf(OperationParams::class, $params);
self::assertInstanceOf(DocumentNode::class, $doc);
self::assertEquals('query', $operationType);
}
public function testAllowsRootValueAsClosure() : void
{
$called = false;
$params = $doc = $operationType = null;
2018-09-26 12:07:23 +03:00
$this->config->setRootValue(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
2018-09-01 23:00:00 +03:00
$called = true;
$params = $p;
$doc = $d;
$operationType = $o;
});
2018-09-19 18:12:09 +03:00
self::assertFalse($called);
$this->executeQuery('{f1}');
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
self::assertInstanceOf(OperationParams::class, $params);
self::assertInstanceOf(DocumentNode::class, $doc);
self::assertEquals('query', $operationType);
}
public function testAppliesErrorFormatter() : void
{
$called = false;
2018-09-01 23:00:00 +03:00
$error = null;
2018-09-26 12:07:23 +03:00
$this->config->setErrorFormatter(static function ($e) use (&$called, &$error) {
$called = true;
2018-09-01 23:00:00 +03:00
$error = $e;
return ['test' => 'formatted'];
});
$result = $this->executeQuery('{fieldWithSafeException}');
2018-09-19 18:12:09 +03:00
self::assertFalse($called);
$formatted = $result->toArray();
2018-09-01 23:00:00 +03:00
$expected = [
'errors' => [
2018-09-01 23:00:00 +03:00
['test' => 'formatted'],
],
];
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
self::assertArraySubset($expected, $formatted);
self::assertInstanceOf(Error::class, $error);
// Assert debugging still works even with custom formatter
$formatted = $result->toArray(Debug::INCLUDE_TRACE);
2018-09-01 23:00:00 +03:00
$expected = [
'errors' => [
[
2018-09-01 23:00:00 +03:00
'test' => 'formatted',
'trace' => [],
],
],
];
2018-09-19 18:12:09 +03:00
self::assertArraySubset($expected, $formatted);
}
public function testAppliesErrorsHandler() : void
{
2018-09-01 23:00:00 +03:00
$called = false;
$errors = null;
$formatter = null;
2018-09-26 12:07:23 +03:00
$this->config->setErrorsHandler(static function ($e, $f) use (&$called, &$errors, &$formatter) {
2018-09-01 23:00:00 +03:00
$called = true;
$errors = $e;
$formatter = $f;
2018-09-01 23:00:00 +03:00
return [
2018-09-01 23:00:00 +03:00
['test' => 'handled'],
];
});
$result = $this->executeQuery('{fieldWithSafeException,test: fieldWithSafeException}');
2018-09-19 18:12:09 +03:00
self::assertFalse($called);
$formatted = $result->toArray();
2018-09-01 23:00:00 +03:00
$expected = [
'errors' => [
2018-09-01 23:00:00 +03:00
['test' => 'handled'],
],
];
2018-09-19 18:12:09 +03:00
self::assertTrue($called);
self::assertArraySubset($expected, $formatted);
self::assertInternalType('array', $errors);
self::assertCount(2, $errors);
self::assertInternalType('callable', $formatter);
self::assertArraySubset($expected, $formatted);
}
}