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 ', $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->expectException(InvariantViolation::class); $this->expectExceptionMessage( '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->expectException(InvariantViolation::class); $this->expectExceptionMessage('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; } }