mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-26 23:06:04 +03:00
60df83f47e
This is a fairly major refactoring of coerceValue which returns an Either so it can return a complete collection of errors. This allows originalError to be preserved for scalar coercion errors and ensures *all* errors are represented in the response. This had a minor change to the logic in execute / subscribe to allow for buildExecutionContext to abrupt complete with multiple errors. ref: graphql/graphql-js#1133
684 lines
20 KiB
PHP
684 lines
20 KiB
PHP
<?php
|
|
namespace GraphQL\Tests\Server;
|
|
|
|
use GraphQL\Deferred;
|
|
use GraphQL\Error\Debug;
|
|
use GraphQL\Error\Error;
|
|
use GraphQL\Error\InvariantViolation;
|
|
use GraphQL\Error\UserError;
|
|
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;
|
|
|
|
class QueryExecutionTest extends TestCase
|
|
{
|
|
/**
|
|
* @var ServerConfig
|
|
*/
|
|
private $config;
|
|
|
|
public function setUp()
|
|
{
|
|
$schema = $this->buildSchema();
|
|
$this->config = ServerConfig::create()
|
|
->setSchema($schema);
|
|
}
|
|
|
|
public function testSimpleQueryExecution()
|
|
{
|
|
$query = '{f1}';
|
|
|
|
$expected = [
|
|
'data' => [
|
|
'f1' => 'f1'
|
|
]
|
|
];
|
|
|
|
$this->assertQueryResultEquals($expected, $query);
|
|
}
|
|
|
|
public function testReturnsSyntaxErrors()
|
|
{
|
|
$query = '{f1';
|
|
|
|
$result = $this->executeQuery($query);
|
|
$this->assertSame(null, $result->data);
|
|
$this->assertCount(1, $result->errors);
|
|
$this->assertContains(
|
|
'Syntax Error: Expected Name, found <EOF>',
|
|
$result->errors[0]->getMessage()
|
|
);
|
|
}
|
|
|
|
public function testDebugExceptions()
|
|
{
|
|
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
|
|
$this->config->setDebug($debug);
|
|
|
|
$query = '
|
|
{
|
|
fieldWithException
|
|
f1
|
|
}
|
|
';
|
|
|
|
$expected = [
|
|
'data' => [
|
|
'fieldWithException' => null,
|
|
'f1' => 'f1'
|
|
],
|
|
'errors' => [
|
|
[
|
|
'message' => 'This is the exception we want',
|
|
'path' => ['fieldWithException'],
|
|
'trace' => []
|
|
]
|
|
]
|
|
];
|
|
|
|
$result = $this->executeQuery($query)->toArray();
|
|
$this->assertArraySubset($expected, $result);
|
|
}
|
|
|
|
public function testPassesRootValueAndContext()
|
|
{
|
|
$rootValue = 'myRootValue';
|
|
$context = new \stdClass();
|
|
|
|
$this->config
|
|
->setContext($context)
|
|
->setRootValue($rootValue);
|
|
|
|
$query = '
|
|
{
|
|
testContextAndRootValue
|
|
}
|
|
';
|
|
|
|
$this->assertTrue(!isset($context->testedRootValue));
|
|
$this->executeQuery($query);
|
|
$this->assertSame($rootValue, $context->testedRootValue);
|
|
}
|
|
|
|
public function testPassesVariables()
|
|
{
|
|
$variables = ['a' => 'a', 'b' => 'b'];
|
|
$query = '
|
|
query ($a: String!, $b: String!) {
|
|
a: fieldWithArg(arg: $a)
|
|
b: fieldWithArg(arg: $b)
|
|
}
|
|
';
|
|
$expected = [
|
|
'data' => [
|
|
'a' => 'a',
|
|
'b' => 'b'
|
|
]
|
|
];
|
|
$this->assertQueryResultEquals($expected, $query, $variables);
|
|
}
|
|
|
|
public function testPassesCustomValidationRules()
|
|
{
|
|
$query = '
|
|
{nonExistentField}
|
|
';
|
|
$expected = [
|
|
'errors' => [
|
|
['message' => 'Cannot query field "nonExistentField" on type "Query".']
|
|
]
|
|
];
|
|
|
|
$this->assertQueryResultEquals($expected, $query);
|
|
|
|
$called = false;
|
|
|
|
$rules = [
|
|
new CustomValidationRule('SomeRule', function() use (&$called) {
|
|
$called = true;
|
|
return [];
|
|
})
|
|
];
|
|
|
|
$this->config->setValidationRules($rules);
|
|
$expected = [
|
|
'data' => []
|
|
];
|
|
$this->assertQueryResultEquals($expected, $query);
|
|
$this->assertTrue($called);
|
|
}
|
|
|
|
public function testAllowsValidationRulesAsClosure()
|
|
{
|
|
$called = false;
|
|
$params = $doc = $operationType = null;
|
|
|
|
$this->config->setValidationRules(function($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
|
|
$called = true;
|
|
$params = $p;
|
|
$doc = $d;
|
|
$operationType = $o;
|
|
return [];
|
|
});
|
|
|
|
$this->assertFalse($called);
|
|
$this->executeQuery('{f1}');
|
|
$this->assertTrue($called);
|
|
$this->assertInstanceOf(OperationParams::class, $params);
|
|
$this->assertInstanceOf(DocumentNode::class, $doc);
|
|
$this->assertEquals('query', $operationType);
|
|
}
|
|
|
|
public function testAllowsDifferentValidationRulesDependingOnOperation()
|
|
{
|
|
$q1 = '{f1}';
|
|
$q2 = '{invalid}';
|
|
$called1 = false;
|
|
$called2 = false;
|
|
|
|
$this->config->setValidationRules(function(OperationParams $params) use ($q1, $q2, &$called1, &$called2) {
|
|
if ($params->query === $q1) {
|
|
$called1 = true;
|
|
return DocumentValidator::allRules();
|
|
} else {
|
|
$called2 = true;
|
|
return [
|
|
new CustomValidationRule('MyRule', function(ValidationContext $context) {
|
|
$context->reportError(new Error("This is the error we are looking for!"));
|
|
})
|
|
];
|
|
}
|
|
});
|
|
|
|
$expected = ['data' => ['f1' => 'f1']];
|
|
$this->assertQueryResultEquals($expected, $q1);
|
|
$this->assertTrue($called1);
|
|
$this->assertFalse($called2);
|
|
|
|
$called1 = false;
|
|
$called2 = false;
|
|
$expected = ['errors' => [['message' => 'This is the error we are looking for!']]];
|
|
$this->assertQueryResultEquals($expected, $q2);
|
|
$this->assertFalse($called1);
|
|
$this->assertTrue($called2);
|
|
}
|
|
|
|
public function testAllowsSkippingValidation()
|
|
{
|
|
$this->config->setValidationRules([]);
|
|
$query = '{nonExistentField}';
|
|
$expected = ['data' => []];
|
|
$this->assertQueryResultEquals($expected, $query);
|
|
}
|
|
|
|
public function testPersistedQueriesAreDisabledByDefault()
|
|
{
|
|
$result = $this->executePersistedQuery('some-id');
|
|
|
|
$expected = [
|
|
'errors' => [
|
|
[
|
|
'message' => 'Persisted queries are not supported by this server',
|
|
'category' => 'request'
|
|
]
|
|
]
|
|
];
|
|
$this->assertEquals($expected, $result->toArray());
|
|
}
|
|
|
|
public function testBatchedQueriesAreDisabledByDefault()
|
|
{
|
|
$batch = [
|
|
[
|
|
'query' => '{invalid}'
|
|
],
|
|
[
|
|
'query' => '{f1,fieldWithException}'
|
|
]
|
|
];
|
|
|
|
$result = $this->executeBatchedQuery($batch);
|
|
|
|
$expected = [
|
|
[
|
|
'errors' => [
|
|
[
|
|
'message' => 'Batched queries are not supported by this server',
|
|
'category' => 'request'
|
|
]
|
|
]
|
|
],
|
|
[
|
|
'errors' => [
|
|
[
|
|
'message' => 'Batched queries are not supported by this server',
|
|
'category' => 'request'
|
|
]
|
|
]
|
|
],
|
|
];
|
|
|
|
$this->assertEquals($expected[0], $result[0]->toArray());
|
|
$this->assertEquals($expected[1], $result[1]->toArray());
|
|
}
|
|
|
|
public function testMutationsAreNotAllowedInReadonlyMode()
|
|
{
|
|
$mutation = 'mutation { a }';
|
|
|
|
$expected = [
|
|
'errors' => [
|
|
[
|
|
'message' => 'GET supports only query operation',
|
|
'category' => 'request'
|
|
]
|
|
]
|
|
];
|
|
|
|
$result = $this->executeQuery($mutation, null, true);
|
|
$this->assertEquals($expected, $result->toArray());
|
|
}
|
|
|
|
public function testAllowsPersistentQueries()
|
|
{
|
|
$called = false;
|
|
$this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) {
|
|
$called = true;
|
|
$this->assertEquals('some-id', $queryId);
|
|
return '{f1}';
|
|
});
|
|
|
|
$result = $this->executePersistedQuery('some-id');
|
|
$this->assertTrue($called);
|
|
|
|
$expected = [
|
|
'data' => [
|
|
'f1' => 'f1'
|
|
]
|
|
];
|
|
$this->assertEquals($expected, $result->toArray());
|
|
|
|
// Make sure it allows returning document node:
|
|
$called = false;
|
|
$this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) {
|
|
$called = true;
|
|
$this->assertEquals('some-id', $queryId);
|
|
return Parser::parse('{f1}');
|
|
});
|
|
$result = $this->executePersistedQuery('some-id');
|
|
$this->assertTrue($called);
|
|
$this->assertEquals($expected, $result->toArray());
|
|
}
|
|
|
|
public function testProhibitsInvalidPersistedQueryLoader()
|
|
{
|
|
$this->setExpectedException(
|
|
InvariantViolation::class,
|
|
'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode '.
|
|
'but got: {"err":"err"}'
|
|
);
|
|
$this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) {
|
|
return ['err' => 'err'];
|
|
});
|
|
$this->executePersistedQuery('some-id');
|
|
}
|
|
|
|
public function testPersistedQueriesAreStillValidatedByDefault()
|
|
{
|
|
$this->config->setPersistentQueryLoader(function() {
|
|
return '{invalid}';
|
|
});
|
|
$result = $this->executePersistedQuery('some-id');
|
|
$expected = [
|
|
'errors' => [
|
|
[
|
|
'message' => 'Cannot query field "invalid" on type "Query".',
|
|
'locations' => [ ['line' => 1, 'column' => 2] ],
|
|
'category' => 'graphql'
|
|
]
|
|
]
|
|
];
|
|
$this->assertEquals($expected, $result->toArray());
|
|
|
|
}
|
|
|
|
public function testAllowSkippingValidationForPersistedQueries()
|
|
{
|
|
$this->config
|
|
->setPersistentQueryLoader(function($queryId) {
|
|
if ($queryId === 'some-id') {
|
|
return '{invalid}';
|
|
} else {
|
|
return '{invalid2}';
|
|
}
|
|
})
|
|
->setValidationRules(function(OperationParams $params) {
|
|
if ($params->queryId === 'some-id') {
|
|
return [];
|
|
} else {
|
|
return DocumentValidator::allRules();
|
|
}
|
|
});
|
|
|
|
$result = $this->executePersistedQuery('some-id');
|
|
$expected = [
|
|
'data' => []
|
|
];
|
|
$this->assertEquals($expected, $result->toArray());
|
|
|
|
$result = $this->executePersistedQuery('some-other-id');
|
|
$expected = [
|
|
'errors' => [
|
|
[
|
|
'message' => 'Cannot query field "invalid2" on type "Query".',
|
|
'locations' => [ ['line' => 1, 'column' => 2] ],
|
|
'category' => 'graphql'
|
|
]
|
|
]
|
|
];
|
|
$this->assertEquals($expected, $result->toArray());
|
|
}
|
|
|
|
public function testProhibitsUnexpectedValidationRules()
|
|
{
|
|
$this->setExpectedException(
|
|
InvariantViolation::class,
|
|
'Expecting validation rules to be array or callable returning array, but got: instance of stdClass'
|
|
);
|
|
$this->config->setValidationRules(function(OperationParams $params) {
|
|
return new \stdClass();
|
|
});
|
|
$this->executeQuery('{f1}');
|
|
}
|
|
|
|
public function testExecutesBatchedQueries()
|
|
{
|
|
$this->config->setQueryBatching(true);
|
|
|
|
$batch = [
|
|
[
|
|
'query' => '{invalid}'
|
|
],
|
|
[
|
|
'query' => '{f1,fieldWithException}'
|
|
],
|
|
[
|
|
'query' => '
|
|
query ($a: String!, $b: String!) {
|
|
a: fieldWithArg(arg: $a)
|
|
b: fieldWithArg(arg: $b)
|
|
}
|
|
',
|
|
'variables' => ['a' => 'a', 'b' => 'b'],
|
|
]
|
|
];
|
|
|
|
$result = $this->executeBatchedQuery($batch);
|
|
|
|
$expected = [
|
|
[
|
|
'errors' => [['message' => 'Cannot query field "invalid" on type "Query".']]
|
|
],
|
|
[
|
|
'data' => [
|
|
'f1' => 'f1',
|
|
'fieldWithException' => null
|
|
],
|
|
'errors' => [
|
|
['message' => 'This is the exception we want']
|
|
]
|
|
],
|
|
[
|
|
'data' => [
|
|
'a' => 'a',
|
|
'b' => 'b'
|
|
]
|
|
]
|
|
];
|
|
|
|
$this->assertArraySubset($expected[0], $result[0]->toArray());
|
|
$this->assertArraySubset($expected[1], $result[1]->toArray());
|
|
$this->assertArraySubset($expected[2], $result[2]->toArray());
|
|
}
|
|
|
|
public function testDeferredsAreSharedAmongAllBatchedQueries()
|
|
{
|
|
$batch = [
|
|
[
|
|
'query' => '{dfd(num: 1)}'
|
|
],
|
|
[
|
|
'query' => '{dfd(num: 2)}'
|
|
],
|
|
[
|
|
'query' => '{dfd(num: 3)}',
|
|
]
|
|
];
|
|
|
|
$calls = [];
|
|
|
|
$this->config
|
|
->setQueryBatching(true)
|
|
->setRootValue('1')
|
|
->setContext([
|
|
'buffer' => function($num) use (&$calls) {
|
|
$calls[] = "buffer: $num";
|
|
},
|
|
'load' => function($num) use (&$calls) {
|
|
$calls[] = "load: $num";
|
|
return "loaded: $num";
|
|
}
|
|
]);
|
|
|
|
$result = $this->executeBatchedQuery($batch);
|
|
|
|
$expectedCalls = [
|
|
'buffer: 1',
|
|
'buffer: 2',
|
|
'buffer: 3',
|
|
'load: 1',
|
|
'load: 2',
|
|
'load: 3',
|
|
];
|
|
$this->assertEquals($expectedCalls, $calls);
|
|
|
|
$expected = [
|
|
[
|
|
'data' => [
|
|
'dfd' => 'loaded: 1'
|
|
]
|
|
],
|
|
[
|
|
'data' => [
|
|
'dfd' => 'loaded: 2'
|
|
]
|
|
],
|
|
[
|
|
'data' => [
|
|
'dfd' => 'loaded: 3'
|
|
]
|
|
],
|
|
];
|
|
|
|
$this->assertEquals($expected[0], $result[0]->toArray());
|
|
$this->assertEquals($expected[1], $result[1]->toArray());
|
|
$this->assertEquals($expected[2], $result[2]->toArray());
|
|
}
|
|
|
|
public function testValidatesParamsBeforeExecution()
|
|
{
|
|
$op = OperationParams::create(['queryBad' => '{f1}']);
|
|
$helper = new Helper();
|
|
$result = $helper->executeOperation($this->config, $op);
|
|
$this->assertInstanceOf(ExecutionResult::class, $result);
|
|
|
|
$this->assertEquals(null, $result->data);
|
|
$this->assertCount(1, $result->errors);
|
|
|
|
$this->assertEquals(
|
|
'GraphQL Request must include at least one of those two parameters: "query" or "queryId"',
|
|
$result->errors[0]->getMessage()
|
|
);
|
|
|
|
$this->assertInstanceOf(
|
|
RequestError::class,
|
|
$result->errors[0]->getPrevious()
|
|
);
|
|
}
|
|
|
|
public function testAllowsContextAsClosure()
|
|
{
|
|
$called = false;
|
|
$params = $doc = $operationType = null;
|
|
|
|
$this->config->setContext(function($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
|
|
$called = true;
|
|
$params = $p;
|
|
$doc = $d;
|
|
$operationType = $o;
|
|
});
|
|
|
|
$this->assertFalse($called);
|
|
$this->executeQuery('{f1}');
|
|
$this->assertTrue($called);
|
|
$this->assertInstanceOf(OperationParams::class, $params);
|
|
$this->assertInstanceOf(DocumentNode::class, $doc);
|
|
$this->assertEquals('query', $operationType);
|
|
}
|
|
|
|
public function testAllowsRootValueAsClosure()
|
|
{
|
|
$called = false;
|
|
$params = $doc = $operationType = null;
|
|
|
|
$this->config->setRootValue(function($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
|
|
$called = true;
|
|
$params = $p;
|
|
$doc = $d;
|
|
$operationType = $o;
|
|
});
|
|
|
|
$this->assertFalse($called);
|
|
$this->executeQuery('{f1}');
|
|
$this->assertTrue($called);
|
|
$this->assertInstanceOf(OperationParams::class, $params);
|
|
$this->assertInstanceOf(DocumentNode::class, $doc);
|
|
$this->assertEquals('query', $operationType);
|
|
}
|
|
|
|
public function testAppliesErrorFormatter()
|
|
{
|
|
$called = false;
|
|
$error = null;
|
|
$this->config->setErrorFormatter(function($e) use (&$called, &$error) {
|
|
$called = true;
|
|
$error = $e;
|
|
return ['test' => 'formatted'];
|
|
});
|
|
|
|
$result = $this->executeQuery('{fieldWithException}');
|
|
$this->assertFalse($called);
|
|
$formatted = $result->toArray();
|
|
$expected = [
|
|
'errors' => [
|
|
['test' => 'formatted']
|
|
]
|
|
];
|
|
$this->assertTrue($called);
|
|
$this->assertArraySubset($expected, $formatted);
|
|
$this->assertInstanceOf(Error::class, $error);
|
|
|
|
// Assert debugging still works even with custom formatter
|
|
$formatted = $result->toArray(Debug::INCLUDE_TRACE);
|
|
$expected = [
|
|
'errors' => [
|
|
[
|
|
'test' => 'formatted',
|
|
'trace' => []
|
|
]
|
|
]
|
|
];
|
|
$this->assertArraySubset($expected, $formatted);
|
|
}
|
|
|
|
public function testAppliesErrorsHandler()
|
|
{
|
|
$called = false;
|
|
$errors = null;
|
|
$formatter = null;
|
|
$this->config->setErrorsHandler(function($e, $f) use (&$called, &$errors, &$formatter) {
|
|
$called = true;
|
|
$errors = $e;
|
|
$formatter = $f;
|
|
return [
|
|
['test' => 'handled']
|
|
];
|
|
});
|
|
|
|
$result = $this->executeQuery('{fieldWithException,test: fieldWithException}');
|
|
|
|
$this->assertFalse($called);
|
|
$formatted = $result->toArray();
|
|
$expected = [
|
|
'errors' => [
|
|
['test' => 'handled']
|
|
]
|
|
];
|
|
$this->assertTrue($called);
|
|
$this->assertArraySubset($expected, $formatted);
|
|
$this->assertInternalType('array', $errors);
|
|
$this->assertCount(2, $errors);
|
|
$this->assertInternalType('callable', $formatter);
|
|
$this->assertArraySubset($expected, $formatted);
|
|
}
|
|
|
|
private function executePersistedQuery($queryId, $variables = null)
|
|
{
|
|
$op = OperationParams::create(['queryId' => $queryId, 'variables' => $variables]);
|
|
$helper = new Helper();
|
|
$result = $helper->executeOperation($this->config, $op);
|
|
$this->assertInstanceOf(ExecutionResult::class, $result);
|
|
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);
|
|
$this->assertInstanceOf(ExecutionResult::class, $result);
|
|
return $result;
|
|
}
|
|
|
|
private function executeBatchedQuery(array $qs)
|
|
{
|
|
$batch = [];
|
|
foreach ($qs as $params) {
|
|
$batch[] = OperationParams::create($params);
|
|
}
|
|
$helper = new Helper();
|
|
$result = $helper->executeBatch($this->config, $batch);
|
|
$this->assertInternalType('array', $result);
|
|
$this->assertCount(count($qs), $result);
|
|
|
|
foreach ($result as $index => $entry) {
|
|
$this->assertInstanceOf(ExecutionResult::class, $entry, "Result at $index is not an instance of " . ExecutionResult::class);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
private function assertQueryResultEquals($expected, $query, $variables = null)
|
|
{
|
|
$result = $this->executeQuery($query, $variables);
|
|
$this->assertArraySubset($expected, $result->toArray(true));
|
|
return $result;
|
|
}
|
|
}
|