Failed Conditions
Pull Request — master (#333)
by Jérémiah
04:09
created

tests/Server/QueryExecutionTest.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Server;
6
7
use GraphQL\Error\Debug;
8
use GraphQL\Error\Error;
9
use GraphQL\Error\InvariantViolation;
10
use GraphQL\Executor\ExecutionResult;
11
use GraphQL\Language\AST\DocumentNode;
12
use GraphQL\Language\Parser;
13
use GraphQL\Server\Helper;
14
use GraphQL\Server\OperationParams;
15
use GraphQL\Server\RequestError;
16
use GraphQL\Server\ServerConfig;
17
use GraphQL\Validator\DocumentValidator;
18
use GraphQL\Validator\Rules\CustomValidationRule;
19
use GraphQL\Validator\ValidationContext;
20
use function count;
21
use function sprintf;
22
23
class QueryExecutionTest extends ServerTestCase
24
{
25
    /** @var ServerConfig */
26
    private $config;
27
28
    public function setUp()
29
    {
30
        $schema       = $this->buildSchema();
31
        $this->config = ServerConfig::create()
32
            ->setSchema($schema);
33
    }
34
35
    public function testSimpleQueryExecution() : void
36
    {
37
        $query = '{f1}';
38
39
        $expected = [
40
            'data' => ['f1' => 'f1'],
41
        ];
42
43
        $this->assertQueryResultEquals($expected, $query);
44
    }
45
46
    private function assertQueryResultEquals($expected, $query, $variables = null)
47
    {
48
        $result = $this->executeQuery($query, $variables);
49
        $this->assertArraySubset($expected, $result->toArray(true));
50
51
        return $result;
52
    }
53
54
    private function executeQuery($query, $variables = null, $readonly = false)
55
    {
56
        $op     = OperationParams::create(['query' => $query, 'variables' => $variables], $readonly);
57
        $helper = new Helper();
58
        $result = $helper->executeOperation($this->config, $op);
59
        $this->assertInstanceOf(ExecutionResult::class, $result);
60
61
        return $result;
62
    }
63
64
    public function testReturnsSyntaxErrors() : void
65
    {
66
        $query = '{f1';
67
68
        $result = $this->executeQuery($query);
69
        $this->assertNull($result->data);
0 ignored issues
show
The property data does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
70
        $this->assertCount(1, $result->errors);
71
        $this->assertContains(
72
            'Syntax Error: Expected Name, found <EOF>',
73
            $result->errors[0]->getMessage()
74
        );
75
    }
76
77
    public function testDebugExceptions() : void
78
    {
79
        $debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
80
        $this->config->setDebug($debug);
81
82
        $query = '
83
        {
84
            fieldWithException
85
            f1
86
        }
87
        ';
88
89
        $expected = [
90
            'data'   => [
91
                'fieldWithException' => null,
92
                'f1'                 => 'f1',
93
            ],
94
            'errors' => [
95
                [
96
                    'message' => 'This is the exception we want',
97
                    'path'    => ['fieldWithException'],
98
                    'trace'   => [],
99
                ],
100
            ],
101
        ];
102
103
        $result = $this->executeQuery($query)->toArray();
104
        $this->assertArraySubset($expected, $result);
105
    }
106
107
    public function testPassesRootValueAndContext() : void
108
    {
109
        $rootValue = 'myRootValue';
110
        $context   = new \stdClass();
111
112
        $this->config
113
            ->setContext($context)
114
            ->setRootValue($rootValue);
115
116
        $query = '
117
        {
118
            testContextAndRootValue
119
        }
120
        ';
121
122
        $this->assertTrue(! isset($context->testedRootValue));
123
        $this->executeQuery($query);
124
        $this->assertSame($rootValue, $context->testedRootValue);
125
    }
126
127
    public function testPassesVariables() : void
128
    {
129
        $variables = ['a' => 'a', 'b' => 'b'];
130
        $query     = '
131
            query ($a: String!, $b: String!) {
132
                a: fieldWithArg(arg: $a)
133
                b: fieldWithArg(arg: $b)
134
            }
135
        ';
136
        $expected  = [
137
            'data' => [
138
                'a' => 'a',
139
                'b' => 'b',
140
            ],
141
        ];
142
        $this->assertQueryResultEquals($expected, $query, $variables);
143
    }
144
145
    public function testPassesCustomValidationRules() : void
146
    {
147
        $query    = '
148
            {nonExistentField}
149
        ';
150
        $expected = [
151
            'errors' => [
152
                ['message' => 'Cannot query field "nonExistentField" on type "Query".'],
153
            ],
154
        ];
155
156
        $this->assertQueryResultEquals($expected, $query);
157
158
        $called = false;
159
160
        $rules = [
161
            new CustomValidationRule('SomeRule', function () use (&$called) {
162
                $called = true;
163
164
                return [];
165
            }),
166
        ];
167
168
        $this->config->setValidationRules($rules);
169
        $expected = [
170
            'data' => [],
171
        ];
172
        $this->assertQueryResultEquals($expected, $query);
173
        $this->assertTrue($called);
174
    }
175
176
    public function testAllowsValidationRulesAsClosure() : void
177
    {
178
        $called = false;
179
        $params = $doc = $operationType = null;
180
181
        $this->config->setValidationRules(function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
182
            $called        = true;
183
            $params        = $p;
184
            $doc           = $d;
185
            $operationType = $o;
186
187
            return [];
188
        });
189
190
        $this->assertFalse($called);
191
        $this->executeQuery('{f1}');
192
        $this->assertTrue($called);
193
        $this->assertInstanceOf(OperationParams::class, $params);
194
        $this->assertInstanceOf(DocumentNode::class, $doc);
195
        $this->assertEquals('query', $operationType);
196
    }
197
198
    public function testAllowsDifferentValidationRulesDependingOnOperation() : void
199
    {
200
        $q1      = '{f1}';
201
        $q2      = '{invalid}';
202
        $called1 = false;
203
        $called2 = false;
204
205
        $this->config->setValidationRules(function (OperationParams $params) use ($q1, &$called1, &$called2) {
206
            if ($params->query === $q1) {
207
                $called1 = true;
208
209
                return DocumentValidator::allRules();
210
            }
211
212
            $called2 = true;
213
214
            return [
215
                new CustomValidationRule('MyRule', function (ValidationContext $context) {
216
                    $context->reportError(new Error('This is the error we are looking for!'));
217
                }),
218
            ];
219
        });
220
221
        $expected = ['data' => ['f1' => 'f1']];
222
        $this->assertQueryResultEquals($expected, $q1);
223
        $this->assertTrue($called1);
224
        $this->assertFalse($called2);
225
226
        $called1  = false;
227
        $called2  = false;
228
        $expected = ['errors' => [['message' => 'This is the error we are looking for!']]];
229
        $this->assertQueryResultEquals($expected, $q2);
230
        $this->assertFalse($called1);
231
        $this->assertTrue($called2);
232
    }
233
234
    public function testAllowsSkippingValidation() : void
235
    {
236
        $this->config->setValidationRules([]);
237
        $query    = '{nonExistentField}';
238
        $expected = ['data' => []];
239
        $this->assertQueryResultEquals($expected, $query);
240
    }
241
242
    public function testPersistedQueriesAreDisabledByDefault() : void
243
    {
244
        $result = $this->executePersistedQuery('some-id');
245
246
        $expected = [
247
            'errors' => [
248
                [
249
                    'message'  => 'Persisted queries are not supported by this server',
250
                    'category' => 'request',
251
                ],
252
            ],
253
        ];
254
        $this->assertEquals($expected, $result->toArray());
255
    }
256
257
    private function executePersistedQuery($queryId, $variables = null)
258
    {
259
        $op     = OperationParams::create(['queryId' => $queryId, 'variables' => $variables]);
260
        $helper = new Helper();
261
        $result = $helper->executeOperation($this->config, $op);
262
        $this->assertInstanceOf(ExecutionResult::class, $result);
263
264
        return $result;
265
    }
266
267
    public function testBatchedQueriesAreDisabledByDefault() : void
268
    {
269
        $batch = [
270
            ['query' => '{invalid}'],
271
            ['query' => '{f1,fieldWithException}'],
272
        ];
273
274
        $result = $this->executeBatchedQuery($batch);
275
276
        $expected = [
277
            [
278
                'errors' => [
279
                    [
280
                        'message'  => 'Batched queries are not supported by this server',
281
                        'category' => 'request',
282
                    ],
283
                ],
284
            ],
285
            [
286
                'errors' => [
287
                    [
288
                        'message'  => 'Batched queries are not supported by this server',
289
                        'category' => 'request',
290
                    ],
291
                ],
292
            ],
293
        ];
294
295
        $this->assertEquals($expected[0], $result[0]->toArray());
296
        $this->assertEquals($expected[1], $result[1]->toArray());
297
    }
298
299
    /**
300
     * @param mixed[][] $qs
301
     */
302
    private function executeBatchedQuery(array $qs)
303
    {
304
        $batch = [];
305
        foreach ($qs as $params) {
306
            $batch[] = OperationParams::create($params);
307
        }
308
        $helper = new Helper();
309
        $result = $helper->executeBatch($this->config, $batch);
310
        $this->assertInternalType('array', $result);
311
        $this->assertCount(count($qs), $result);
312
313
        foreach ($result as $index => $entry) {
314
            $this->assertInstanceOf(
315
                ExecutionResult::class,
316
                $entry,
317
                sprintf('Result at %s is not an instance of %s', $index, ExecutionResult::class)
318
            );
319
        }
320
321
        return $result;
322
    }
323
324
    public function testMutationsAreNotAllowedInReadonlyMode() : void
325
    {
326
        $mutation = 'mutation { a }';
327
328
        $expected = [
329
            'errors' => [
330
                [
331
                    'message'  => 'GET supports only query operation',
332
                    'category' => 'request',
333
                ],
334
            ],
335
        ];
336
337
        $result = $this->executeQuery($mutation, null, true);
338
        $this->assertEquals($expected, $result->toArray());
339
    }
340
341
    public function testAllowsPersistentQueries() : void
342
    {
343
        $called = false;
344
        $this->config->setPersistentQueryLoader(function ($queryId, OperationParams $params) use (&$called) {
345
            $called = true;
346
            $this->assertEquals('some-id', $queryId);
347
348
            return '{f1}';
349
        });
350
351
        $result = $this->executePersistedQuery('some-id');
352
        $this->assertTrue($called);
353
354
        $expected = [
355
            'data' => ['f1' => 'f1'],
356
        ];
357
        $this->assertEquals($expected, $result->toArray());
358
359
        // Make sure it allows returning document node:
360
        $called = false;
361
        $this->config->setPersistentQueryLoader(function ($queryId, OperationParams $params) use (&$called) {
362
            $called = true;
363
            $this->assertEquals('some-id', $queryId);
364
365
            return Parser::parse('{f1}');
366
        });
367
        $result = $this->executePersistedQuery('some-id');
368
        $this->assertTrue($called);
369
        $this->assertEquals($expected, $result->toArray());
370
    }
371
372
    public function testProhibitsInvalidPersistedQueryLoader() : void
373
    {
374
        $this->expectException(InvariantViolation::class);
375
        $this->expectExceptionMessage(
376
            'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode ' .
377
            'but got: {"err":"err"}'
378
        );
379
        $this->config->setPersistentQueryLoader(function () {
380
            return ['err' => 'err'];
381
        });
382
        $this->executePersistedQuery('some-id');
383
    }
384
385
    public function testPersistedQueriesAreStillValidatedByDefault() : void
386
    {
387
        $this->config->setPersistentQueryLoader(function () {
388
            return '{invalid}';
389
        });
390
        $result   = $this->executePersistedQuery('some-id');
391
        $expected = [
392
            'errors' => [
393
                [
394
                    'message'   => 'Cannot query field "invalid" on type "Query".',
395
                    'locations' => [['line' => 1, 'column' => 2]],
396
                    'category'  => 'graphql',
397
                ],
398
            ],
399
        ];
400
        $this->assertEquals($expected, $result->toArray());
401
    }
402
403
    public function testAllowSkippingValidationForPersistedQueries() : void
404
    {
405
        $this->config
406
            ->setPersistentQueryLoader(function ($queryId) {
407
                if ($queryId === 'some-id') {
408
                    return '{invalid}';
409
                }
410
411
                return '{invalid2}';
412
            })
413
            ->setValidationRules(function (OperationParams $params) {
414
                if ($params->queryId === 'some-id') {
415
                    return [];
416
                }
417
418
                return DocumentValidator::allRules();
419
            });
420
421
        $result   = $this->executePersistedQuery('some-id');
422
        $expected = [
423
            'data' => [],
424
        ];
425
        $this->assertEquals($expected, $result->toArray());
426
427
        $result   = $this->executePersistedQuery('some-other-id');
428
        $expected = [
429
            'errors' => [
430
                [
431
                    'message'   => 'Cannot query field "invalid2" on type "Query".',
432
                    'locations' => [['line' => 1, 'column' => 2]],
433
                    'category'  => 'graphql',
434
                ],
435
            ],
436
        ];
437
        $this->assertEquals($expected, $result->toArray());
438
    }
439
440
    public function testProhibitsUnexpectedValidationRules() : void
441
    {
442
        $this->expectException(InvariantViolation::class);
443
        $this->expectExceptionMessage('Expecting validation rules to be array or callable returning array, but got: instance of stdClass');
444
        $this->config->setValidationRules(function (OperationParams $params) {
445
            return new \stdClass();
446
        });
447
        $this->executeQuery('{f1}');
448
    }
449
450
    public function testExecutesBatchedQueries() : void
451
    {
452
        $this->config->setQueryBatching(true);
453
454
        $batch = [
455
            ['query' => '{invalid}'],
456
            ['query' => '{f1,fieldWithException}'],
457
            [
458
                'query'     => '
459
                    query ($a: String!, $b: String!) {
460
                        a: fieldWithArg(arg: $a)
461
                        b: fieldWithArg(arg: $b)
462
                    }
463
                ',
464
                'variables' => ['a' => 'a', 'b' => 'b'],
465
            ],
466
        ];
467
468
        $result = $this->executeBatchedQuery($batch);
469
470
        $expected = [
471
            [
472
                'errors' => [['message' => 'Cannot query field "invalid" on type "Query".']],
473
            ],
474
            [
475
                'data'   => [
476
                    'f1'                 => 'f1',
477
                    'fieldWithException' => null,
478
                ],
479
                'errors' => [
480
                    ['message' => 'This is the exception we want'],
481
                ],
482
            ],
483
            [
484
                'data' => [
485
                    'a' => 'a',
486
                    'b' => 'b',
487
                ],
488
            ],
489
        ];
490
491
        $this->assertArraySubset($expected[0], $result[0]->toArray());
492
        $this->assertArraySubset($expected[1], $result[1]->toArray());
493
        $this->assertArraySubset($expected[2], $result[2]->toArray());
494
    }
495
496
    public function testDeferredsAreSharedAmongAllBatchedQueries() : void
497
    {
498
        $batch = [
499
            ['query' => '{dfd(num: 1)}'],
500
            ['query' => '{dfd(num: 2)}'],
501
            ['query' => '{dfd(num: 3)}'],
502
        ];
503
504
        $calls = [];
505
506
        $this->config
507
            ->setQueryBatching(true)
508
            ->setRootValue('1')
509
            ->setContext([
510
                'buffer' => function ($num) use (&$calls) {
511
                    $calls[] = sprintf('buffer: %d', $num);
512
                },
513
                'load'   => function ($num) use (&$calls) {
514
                    $calls[] = sprintf('load: %d', $num);
515
516
                    return sprintf('loaded: %d', $num);
517
                },
518
            ]);
519
520
        $result = $this->executeBatchedQuery($batch);
521
522
        $expectedCalls = [
523
            'buffer: 1',
524
            'buffer: 2',
525
            'buffer: 3',
526
            'load: 1',
527
            'load: 2',
528
            'load: 3',
529
        ];
530
        $this->assertEquals($expectedCalls, $calls);
531
532
        $expected = [
533
            [
534
                'data' => ['dfd' => 'loaded: 1'],
535
            ],
536
            [
537
                'data' => ['dfd' => 'loaded: 2'],
538
            ],
539
            [
540
                'data' => ['dfd' => 'loaded: 3'],
541
            ],
542
        ];
543
544
        $this->assertEquals($expected[0], $result[0]->toArray());
545
        $this->assertEquals($expected[1], $result[1]->toArray());
546
        $this->assertEquals($expected[2], $result[2]->toArray());
547
    }
548
549
    public function testValidatesParamsBeforeExecution() : void
550
    {
551
        $op     = OperationParams::create(['queryBad' => '{f1}']);
552
        $helper = new Helper();
553
        $result = $helper->executeOperation($this->config, $op);
554
        $this->assertInstanceOf(ExecutionResult::class, $result);
555
556
        $this->assertEquals(null, $result->data);
557
        $this->assertCount(1, $result->errors);
558
559
        $this->assertEquals(
560
            'GraphQL Request must include at least one of those two parameters: "query" or "queryId"',
561
            $result->errors[0]->getMessage()
562
        );
563
564
        $this->assertInstanceOf(
565
            RequestError::class,
566
            $result->errors[0]->getPrevious()
567
        );
568
    }
569
570
    public function testAllowsContextAsClosure() : void
571
    {
572
        $called = false;
573
        $params = $doc = $operationType = null;
574
575
        $this->config->setContext(function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
576
            $called        = true;
577
            $params        = $p;
578
            $doc           = $d;
579
            $operationType = $o;
580
        });
581
582
        $this->assertFalse($called);
583
        $this->executeQuery('{f1}');
584
        $this->assertTrue($called);
585
        $this->assertInstanceOf(OperationParams::class, $params);
586
        $this->assertInstanceOf(DocumentNode::class, $doc);
587
        $this->assertEquals('query', $operationType);
588
    }
589
590
    public function testAllowsRootValueAsClosure() : void
591
    {
592
        $called = false;
593
        $params = $doc = $operationType = null;
594
595
        $this->config->setRootValue(function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
596
            $called        = true;
597
            $params        = $p;
598
            $doc           = $d;
599
            $operationType = $o;
600
        });
601
602
        $this->assertFalse($called);
603
        $this->executeQuery('{f1}');
604
        $this->assertTrue($called);
605
        $this->assertInstanceOf(OperationParams::class, $params);
606
        $this->assertInstanceOf(DocumentNode::class, $doc);
607
        $this->assertEquals('query', $operationType);
608
    }
609
610
    public function testAppliesErrorFormatter() : void
611
    {
612
        $called = false;
613
        $error  = null;
614
        $this->config->setErrorFormatter(function ($e) use (&$called, &$error) {
615
            $called = true;
616
            $error  = $e;
617
618
            return ['test' => 'formatted'];
619
        });
620
621
        $result = $this->executeQuery('{fieldWithException}');
622
        $this->assertFalse($called);
623
        $formatted = $result->toArray();
624
        $expected  = [
625
            'errors' => [
626
                ['test' => 'formatted'],
627
            ],
628
        ];
629
        $this->assertTrue($called);
630
        $this->assertArraySubset($expected, $formatted);
631
        $this->assertInstanceOf(Error::class, $error);
632
633
        // Assert debugging still works even with custom formatter
634
        $formatted = $result->toArray(Debug::INCLUDE_TRACE);
635
        $expected  = [
636
            'errors' => [
637
                [
638
                    'test'  => 'formatted',
639
                    'trace' => [],
640
                ],
641
            ],
642
        ];
643
        $this->assertArraySubset($expected, $formatted);
644
    }
645
646
    public function testAppliesErrorsHandler() : void
647
    {
648
        $called    = false;
649
        $errors    = null;
650
        $formatter = null;
651
        $this->config->setErrorsHandler(function ($e, $f) use (&$called, &$errors, &$formatter) {
652
            $called    = true;
653
            $errors    = $e;
654
            $formatter = $f;
655
656
            return [
657
                ['test' => 'handled'],
658
            ];
659
        });
660
661
        $result = $this->executeQuery('{fieldWithException,test: fieldWithException}');
662
663
        $this->assertFalse($called);
664
        $formatted = $result->toArray();
665
        $expected  = [
666
            'errors' => [
667
                ['test' => 'handled'],
668
            ],
669
        ];
670
        $this->assertTrue($called);
671
        $this->assertArraySubset($expected, $formatted);
672
        $this->assertInternalType('array', $errors);
673
        $this->assertCount(2, $errors);
674
        $this->assertInternalType('callable', $formatter);
675
        $this->assertArraySubset($expected, $formatted);
676
    }
677
}
678