Failed Conditions
Push — master ( 84f136...849c15 )
by Vladimir
15:41 queued 12:38
created

testPassesCustomValidationRules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 29
rs 9.7666
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
        self::assertArraySubset($expected, $result->toArray(true));
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on GraphQL\Executor\Promise\Promise. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

49
        self::assertArraySubset($expected, $result->/** @scrutinizer ignore-call */ toArray(true));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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
        self::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
        self::assertNull($result->data);
0 ignored issues
show
Bug introduced by
The property data does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
70
        self::assertCount(1, $result->errors);
0 ignored issues
show
Bug introduced by
The property errors does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
71
        self::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
            fieldWithSafeException
85
            f1
86
        }
87
        ';
88
89
        $expected = [
90
            'data' => [
91
                'fieldWithSafeException' => null,
92
                'f1'                 => 'f1',
93
            ],
94
            'errors' => [
95
                [
96
                    'message' => 'This is the exception we want',
97
                    'path' => ['fieldWithSafeException'],
98
                    'trace' => [],
99
                ],
100
            ],
101
        ];
102
103
        $result = $this->executeQuery($query)->toArray();
104
        self::assertArraySubset($expected, $result);
105
    }
106
107
    public function testRethrowUnsafeExceptions() : void
108
    {
109
        $this->config->setDebug(Debug::RETHROW_UNSAFE_EXCEPTIONS);
110
        $this->expectException(Unsafe::class);
111
112
        $this->executeQuery('
113
        {
114
            fieldWithUnsafeException
115
        }
116
        ')->toArray();
117
    }
118
119
    public function testPassesRootValueAndContext() : void
120
    {
121
        $rootValue = 'myRootValue';
122
        $context   = new \stdClass();
123
124
        $this->config
125
            ->setContext($context)
126
            ->setRootValue($rootValue);
127
128
        $query = '
129
        {
130
            testContextAndRootValue
131
        }
132
        ';
133
134
        self::assertTrue(! isset($context->testedRootValue));
135
        $this->executeQuery($query);
136
        self::assertSame($rootValue, $context->testedRootValue);
137
    }
138
139
    public function testPassesVariables() : void
140
    {
141
        $variables = ['a' => 'a', 'b' => 'b'];
142
        $query     = '
143
            query ($a: String!, $b: String!) {
144
                a: fieldWithArg(arg: $a)
145
                b: fieldWithArg(arg: $b)
146
            }
147
        ';
148
        $expected  = [
149
            'data' => [
150
                'a' => 'a',
151
                'b' => 'b',
152
            ],
153
        ];
154
        $this->assertQueryResultEquals($expected, $query, $variables);
155
    }
156
157
    public function testPassesCustomValidationRules() : void
158
    {
159
        $query    = '
160
            {nonExistentField}
161
        ';
162
        $expected = [
163
            'errors' => [
164
                ['message' => 'Cannot query field "nonExistentField" on type "Query".'],
165
            ],
166
        ];
167
168
        $this->assertQueryResultEquals($expected, $query);
169
170
        $called = false;
171
172
        $rules = [
173
            new CustomValidationRule('SomeRule', function () use (&$called) {
174
                $called = true;
175
176
                return [];
177
            }),
178
        ];
179
180
        $this->config->setValidationRules($rules);
181
        $expected = [
182
            'data' => [],
183
        ];
184
        $this->assertQueryResultEquals($expected, $query);
185
        self::assertTrue($called);
186
    }
187
188
    public function testAllowsValidationRulesAsClosure() : void
189
    {
190
        $called = false;
191
        $params = $doc = $operationType = null;
192
193
        $this->config->setValidationRules(function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
194
            $called        = true;
195
            $params        = $p;
196
            $doc           = $d;
197
            $operationType = $o;
198
199
            return [];
200
        });
201
202
        self::assertFalse($called);
203
        $this->executeQuery('{f1}');
204
        self::assertTrue($called);
205
        self::assertInstanceOf(OperationParams::class, $params);
206
        self::assertInstanceOf(DocumentNode::class, $doc);
207
        self::assertEquals('query', $operationType);
208
    }
209
210
    public function testAllowsDifferentValidationRulesDependingOnOperation() : void
211
    {
212
        $q1      = '{f1}';
213
        $q2      = '{invalid}';
214
        $called1 = false;
215
        $called2 = false;
216
217
        $this->config->setValidationRules(function (OperationParams $params) use ($q1, &$called1, &$called2) {
218
            if ($params->query === $q1) {
219
                $called1 = true;
220
221
                return DocumentValidator::allRules();
222
            }
223
224
            $called2 = true;
225
226
            return [
227
                new CustomValidationRule('MyRule', function (ValidationContext $context) {
228
                    $context->reportError(new Error('This is the error we are looking for!'));
229
                }),
230
            ];
231
        });
232
233
        $expected = ['data' => ['f1' => 'f1']];
234
        $this->assertQueryResultEquals($expected, $q1);
235
        self::assertTrue($called1);
236
        self::assertFalse($called2);
237
238
        $called1  = false;
239
        $called2  = false;
240
        $expected = ['errors' => [['message' => 'This is the error we are looking for!']]];
241
        $this->assertQueryResultEquals($expected, $q2);
242
        self::assertFalse($called1);
243
        self::assertTrue($called2);
244
    }
245
246
    public function testAllowsSkippingValidation() : void
247
    {
248
        $this->config->setValidationRules([]);
249
        $query    = '{nonExistentField}';
250
        $expected = ['data' => []];
251
        $this->assertQueryResultEquals($expected, $query);
252
    }
253
254
    public function testPersistedQueriesAreDisabledByDefault() : void
255
    {
256
        $result = $this->executePersistedQuery('some-id');
257
258
        $expected = [
259
            'errors' => [
260
                [
261
                    'message'  => 'Persisted queries are not supported by this server',
262
                    'category' => 'request',
263
                ],
264
            ],
265
        ];
266
        self::assertEquals($expected, $result->toArray());
267
    }
268
269
    private function executePersistedQuery($queryId, $variables = null)
270
    {
271
        $op     = OperationParams::create(['queryId' => $queryId, 'variables' => $variables]);
272
        $helper = new Helper();
273
        $result = $helper->executeOperation($this->config, $op);
274
        self::assertInstanceOf(ExecutionResult::class, $result);
275
276
        return $result;
277
    }
278
279
    public function testBatchedQueriesAreDisabledByDefault() : void
280
    {
281
        $batch = [
282
            ['query' => '{invalid}'],
283
            ['query' => '{f1,fieldWithSafeException}'],
284
        ];
285
286
        $result = $this->executeBatchedQuery($batch);
287
288
        $expected = [
289
            [
290
                'errors' => [
291
                    [
292
                        'message'  => 'Batched queries are not supported by this server',
293
                        'category' => 'request',
294
                    ],
295
                ],
296
            ],
297
            [
298
                'errors' => [
299
                    [
300
                        'message'  => 'Batched queries are not supported by this server',
301
                        'category' => 'request',
302
                    ],
303
                ],
304
            ],
305
        ];
306
307
        self::assertEquals($expected[0], $result[0]->toArray());
308
        self::assertEquals($expected[1], $result[1]->toArray());
309
    }
310
311
    /**
312
     * @param mixed[][] $qs
313
     */
314
    private function executeBatchedQuery(array $qs)
315
    {
316
        $batch = [];
317
        foreach ($qs as $params) {
318
            $batch[] = OperationParams::create($params);
319
        }
320
        $helper = new Helper();
321
        $result = $helper->executeBatch($this->config, $batch);
322
        self::assertInternalType('array', $result);
323
        self::assertCount(count($qs), $result);
0 ignored issues
show
Bug introduced by
$result of type GraphQL\Executor\Executi...xecutor\Promise\Promise is incompatible with the type Countable|iterable expected by parameter $haystack of PHPUnit\Framework\Assert::assertCount(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

323
        self::assertCount(count($qs), /** @scrutinizer ignore-type */ $result);
Loading history...
324
325
        foreach ($result as $index => $entry) {
326
            self::assertInstanceOf(
327
                ExecutionResult::class,
328
                $entry,
329
                sprintf('Result at %s is not an instance of %s', $index, ExecutionResult::class)
330
            );
331
        }
332
333
        return $result;
334
    }
335
336
    public function testMutationsAreNotAllowedInReadonlyMode() : void
337
    {
338
        $mutation = 'mutation { a }';
339
340
        $expected = [
341
            'errors' => [
342
                [
343
                    'message'  => 'GET supports only query operation',
344
                    'category' => 'request',
345
                ],
346
            ],
347
        ];
348
349
        $result = $this->executeQuery($mutation, null, true);
350
        self::assertEquals($expected, $result->toArray());
351
    }
352
353
    public function testAllowsPersistentQueries() : void
354
    {
355
        $called = false;
356
        $this->config->setPersistentQueryLoader(function ($queryId, OperationParams $params) use (&$called) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

356
        $this->config->setPersistentQueryLoader(function ($queryId, /** @scrutinizer ignore-unused */ OperationParams $params) use (&$called) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
357
            $called = true;
358
            self::assertEquals('some-id', $queryId);
359
360
            return '{f1}';
361
        });
362
363
        $result = $this->executePersistedQuery('some-id');
364
        self::assertTrue($called);
365
366
        $expected = [
367
            'data' => ['f1' => 'f1'],
368
        ];
369
        self::assertEquals($expected, $result->toArray());
370
371
        // Make sure it allows returning document node:
372
        $called = false;
373
        $this->config->setPersistentQueryLoader(function ($queryId, OperationParams $params) use (&$called) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

373
        $this->config->setPersistentQueryLoader(function ($queryId, /** @scrutinizer ignore-unused */ OperationParams $params) use (&$called) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
374
            $called = true;
375
            self::assertEquals('some-id', $queryId);
376
377
            return Parser::parse('{f1}');
378
        });
379
        $result = $this->executePersistedQuery('some-id');
380
        self::assertTrue($called);
381
        self::assertEquals($expected, $result->toArray());
382
    }
383
384
    public function testProhibitsInvalidPersistedQueryLoader() : void
385
    {
386
        $this->expectException(InvariantViolation::class);
387
        $this->expectExceptionMessage(
388
            'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode ' .
389
            'but got: {"err":"err"}'
390
        );
391
        $this->config->setPersistentQueryLoader(function () {
392
            return ['err' => 'err'];
393
        });
394
        $this->executePersistedQuery('some-id');
395
    }
396
397
    public function testPersistedQueriesAreStillValidatedByDefault() : void
398
    {
399
        $this->config->setPersistentQueryLoader(function () {
400
            return '{invalid}';
401
        });
402
        $result   = $this->executePersistedQuery('some-id');
403
        $expected = [
404
            'errors' => [
405
                [
406
                    'message'   => 'Cannot query field "invalid" on type "Query".',
407
                    'locations' => [['line' => 1, 'column' => 2]],
408
                    'category'  => 'graphql',
409
                ],
410
            ],
411
        ];
412
        self::assertEquals($expected, $result->toArray());
413
    }
414
415
    public function testAllowSkippingValidationForPersistedQueries() : void
416
    {
417
        $this->config
418
            ->setPersistentQueryLoader(function ($queryId) {
419
                if ($queryId === 'some-id') {
420
                    return '{invalid}';
421
                }
422
423
                return '{invalid2}';
424
            })
425
            ->setValidationRules(function (OperationParams $params) {
426
                if ($params->queryId === 'some-id') {
427
                    return [];
428
                }
429
430
                return DocumentValidator::allRules();
431
            });
432
433
        $result   = $this->executePersistedQuery('some-id');
434
        $expected = [
435
            'data' => [],
436
        ];
437
        self::assertEquals($expected, $result->toArray());
438
439
        $result   = $this->executePersistedQuery('some-other-id');
440
        $expected = [
441
            'errors' => [
442
                [
443
                    'message'   => 'Cannot query field "invalid2" on type "Query".',
444
                    'locations' => [['line' => 1, 'column' => 2]],
445
                    'category'  => 'graphql',
446
                ],
447
            ],
448
        ];
449
        self::assertEquals($expected, $result->toArray());
450
    }
451
452
    public function testProhibitsUnexpectedValidationRules() : void
453
    {
454
        $this->expectException(InvariantViolation::class);
455
        $this->expectExceptionMessage('Expecting validation rules to be array or callable returning array, but got: instance of stdClass');
456
        $this->config->setValidationRules(function (OperationParams $params) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

456
        $this->config->setValidationRules(function (/** @scrutinizer ignore-unused */ OperationParams $params) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
457
            return new \stdClass();
458
        });
459
        $this->executeQuery('{f1}');
460
    }
461
462
    public function testExecutesBatchedQueries() : void
463
    {
464
        $this->config->setQueryBatching(true);
465
466
        $batch = [
467
            ['query' => '{invalid}'],
468
            ['query' => '{f1,fieldWithSafeException}'],
469
            [
470
                'query'     => '
471
                    query ($a: String!, $b: String!) {
472
                        a: fieldWithArg(arg: $a)
473
                        b: fieldWithArg(arg: $b)
474
                    }
475
                ',
476
                'variables' => ['a' => 'a', 'b' => 'b'],
477
            ],
478
        ];
479
480
        $result = $this->executeBatchedQuery($batch);
481
482
        $expected = [
483
            [
484
                'errors' => [['message' => 'Cannot query field "invalid" on type "Query".']],
485
            ],
486
            [
487
                'data' => [
488
                    'f1' => 'f1',
489
                    'fieldWithSafeException' => null,
490
                ],
491
                'errors' => [
492
                    ['message' => 'This is the exception we want'],
493
                ],
494
            ],
495
            [
496
                'data' => [
497
                    'a' => 'a',
498
                    'b' => 'b',
499
                ],
500
            ],
501
        ];
502
503
        self::assertArraySubset($expected[0], $result[0]->toArray());
504
        self::assertArraySubset($expected[1], $result[1]->toArray());
505
        self::assertArraySubset($expected[2], $result[2]->toArray());
506
    }
507
508
    public function testDeferredsAreSharedAmongAllBatchedQueries() : void
509
    {
510
        $batch = [
511
            ['query' => '{dfd(num: 1)}'],
512
            ['query' => '{dfd(num: 2)}'],
513
            ['query' => '{dfd(num: 3)}'],
514
        ];
515
516
        $calls = [];
517
518
        $this->config
519
            ->setQueryBatching(true)
520
            ->setRootValue('1')
521
            ->setContext([
522
                'buffer' => function ($num) use (&$calls) {
523
                    $calls[] = sprintf('buffer: %d', $num);
524
                },
525
                'load'   => function ($num) use (&$calls) {
526
                    $calls[] = sprintf('load: %d', $num);
527
528
                    return sprintf('loaded: %d', $num);
529
                },
530
            ]);
531
532
        $result = $this->executeBatchedQuery($batch);
533
534
        $expectedCalls = [
535
            'buffer: 1',
536
            'buffer: 2',
537
            'buffer: 3',
538
            'load: 1',
539
            'load: 2',
540
            'load: 3',
541
        ];
542
        self::assertEquals($expectedCalls, $calls);
543
544
        $expected = [
545
            [
546
                'data' => ['dfd' => 'loaded: 1'],
547
            ],
548
            [
549
                'data' => ['dfd' => 'loaded: 2'],
550
            ],
551
            [
552
                'data' => ['dfd' => 'loaded: 3'],
553
            ],
554
        ];
555
556
        self::assertEquals($expected[0], $result[0]->toArray());
557
        self::assertEquals($expected[1], $result[1]->toArray());
558
        self::assertEquals($expected[2], $result[2]->toArray());
559
    }
560
561
    public function testValidatesParamsBeforeExecution() : void
562
    {
563
        $op     = OperationParams::create(['queryBad' => '{f1}']);
564
        $helper = new Helper();
565
        $result = $helper->executeOperation($this->config, $op);
566
        self::assertInstanceOf(ExecutionResult::class, $result);
567
568
        self::assertEquals(null, $result->data);
0 ignored issues
show
Bug introduced by
The property data does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
569
        self::assertCount(1, $result->errors);
0 ignored issues
show
Bug introduced by
The property errors does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
570
571
        self::assertEquals(
572
            'GraphQL Request must include at least one of those two parameters: "query" or "queryId"',
573
            $result->errors[0]->getMessage()
574
        );
575
576
        self::assertInstanceOf(
577
            RequestError::class,
578
            $result->errors[0]->getPrevious()
579
        );
580
    }
581
582
    public function testAllowsContextAsClosure() : void
583
    {
584
        $called = false;
585
        $params = $doc = $operationType = null;
586
587
        $this->config->setContext(function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
588
            $called        = true;
589
            $params        = $p;
590
            $doc           = $d;
591
            $operationType = $o;
592
        });
593
594
        self::assertFalse($called);
595
        $this->executeQuery('{f1}');
596
        self::assertTrue($called);
597
        self::assertInstanceOf(OperationParams::class, $params);
598
        self::assertInstanceOf(DocumentNode::class, $doc);
599
        self::assertEquals('query', $operationType);
600
    }
601
602
    public function testAllowsRootValueAsClosure() : void
603
    {
604
        $called = false;
605
        $params = $doc = $operationType = null;
606
607
        $this->config->setRootValue(function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) {
608
            $called        = true;
609
            $params        = $p;
610
            $doc           = $d;
611
            $operationType = $o;
612
        });
613
614
        self::assertFalse($called);
615
        $this->executeQuery('{f1}');
616
        self::assertTrue($called);
617
        self::assertInstanceOf(OperationParams::class, $params);
618
        self::assertInstanceOf(DocumentNode::class, $doc);
619
        self::assertEquals('query', $operationType);
620
    }
621
622
    public function testAppliesErrorFormatter() : void
623
    {
624
        $called = false;
625
        $error  = null;
626
        $this->config->setErrorFormatter(function ($e) use (&$called, &$error) {
627
            $called = true;
628
            $error  = $e;
629
630
            return ['test' => 'formatted'];
631
        });
632
633
        $result = $this->executeQuery('{fieldWithSafeException}');
634
        self::assertFalse($called);
635
        $formatted = $result->toArray();
636
        $expected  = [
637
            'errors' => [
638
                ['test' => 'formatted'],
639
            ],
640
        ];
641
        self::assertTrue($called);
642
        self::assertArraySubset($expected, $formatted);
643
        self::assertInstanceOf(Error::class, $error);
644
645
        // Assert debugging still works even with custom formatter
646
        $formatted = $result->toArray(Debug::INCLUDE_TRACE);
647
        $expected  = [
648
            'errors' => [
649
                [
650
                    'test'  => 'formatted',
651
                    'trace' => [],
652
                ],
653
            ],
654
        ];
655
        self::assertArraySubset($expected, $formatted);
656
    }
657
658
    public function testAppliesErrorsHandler() : void
659
    {
660
        $called    = false;
661
        $errors    = null;
662
        $formatter = null;
663
        $this->config->setErrorsHandler(function ($e, $f) use (&$called, &$errors, &$formatter) {
664
            $called    = true;
665
            $errors    = $e;
666
            $formatter = $f;
667
668
            return [
669
                ['test' => 'handled'],
670
            ];
671
        });
672
673
        $result = $this->executeQuery('{fieldWithSafeException,test: fieldWithSafeException}');
674
675
        self::assertFalse($called);
676
        $formatted = $result->toArray();
677
        $expected  = [
678
            'errors' => [
679
                ['test' => 'handled'],
680
            ],
681
        ];
682
        self::assertTrue($called);
683
        self::assertArraySubset($expected, $formatted);
684
        self::assertInternalType('array', $errors);
685
        self::assertCount(2, $errors);
686
        self::assertInternalType('callable', $formatter);
687
        self::assertArraySubset($expected, $formatted);
688
    }
689
}
690