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

ExecutorLazySchemaTest::testDeepQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 34
c 0
b 0
f 0
rs 9.568
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Executor;
6
7
use GraphQL\Error\InvariantViolation;
8
use GraphQL\Error\Warning;
9
use GraphQL\Executor\ExecutionResult;
10
use GraphQL\Executor\Executor;
11
use GraphQL\Language\Parser;
12
use GraphQL\Tests\Executor\TestClasses\Cat;
13
use GraphQL\Tests\Executor\TestClasses\Dog;
14
use GraphQL\Type\Definition\CustomScalarType;
15
use GraphQL\Type\Definition\EnumType;
16
use GraphQL\Type\Definition\InputObjectType;
17
use GraphQL\Type\Definition\InterfaceType;
18
use GraphQL\Type\Definition\ObjectType;
19
use GraphQL\Type\Definition\ScalarType;
20
use GraphQL\Type\Definition\Type;
21
use GraphQL\Type\Definition\UnionType;
22
use GraphQL\Type\Schema;
23
use PHPUnit\Framework\Error\Error;
24
use PHPUnit\Framework\TestCase;
25
use function count;
26
27
class ExecutorLazySchemaTest extends TestCase
28
{
29
    /** @var ScalarType */
30
    public $someScalarType;
31
32
    /** @var ObjectType */
33
    public $someObjectType;
34
35
    /** @var ObjectType */
36
    public $otherObjectType;
37
38
    /** @var ObjectType */
39
    public $deeperObjectType;
40
41
    /** @var UnionType */
42
    public $someUnionType;
43
44
    /** @var InterfaceType */
45
    public $someInterfaceType;
46
47
    /** @var EnumType */
48
    public $someEnumType;
49
50
    /** @var InputObjectType */
51
    public $someInputObjectType;
52
53
    /** @var ObjectType */
54
    public $queryType;
55
56
    /** @var string[] */
57
    public $calls = [];
58
59
    /** @var bool[] */
60
    public $loadedTypes = [];
61
62
    public function testWarnsAboutSlowIsTypeOfForLazySchema() : void
63
    {
64
        // isTypeOf used to resolve runtime type for Interface
65
        $petType = new InterfaceType([
66
            'name'   => 'Pet',
67
            'fields' => function () {
68
                return [
69
                    'name' => ['type' => Type::string()],
70
                ];
71
            },
72
        ]);
73
74
        // Added to interface type when defined
75
        $dogType = new ObjectType([
76
            'name'       => 'Dog',
77
            'interfaces' => [$petType],
78
            'isTypeOf'   => function ($obj) {
79
                return $obj instanceof Dog;
80
            },
81
            'fields'     => function () {
82
                return [
83
                    'name'  => ['type' => Type::string()],
84
                    'woofs' => ['type' => Type::boolean()],
85
                ];
86
            },
87
        ]);
88
89
        $catType = new ObjectType([
90
            'name'       => 'Cat',
91
            'interfaces' => [$petType],
92
            'isTypeOf'   => function ($obj) {
93
                return $obj instanceof Cat;
94
            },
95
            'fields'     => function () {
96
                return [
97
                    'name'  => ['type' => Type::string()],
98
                    'meows' => ['type' => Type::boolean()],
99
                ];
100
            },
101
        ]);
102
103
        $schema = new Schema([
104
            'query'      => new ObjectType([
105
                'name'   => 'Query',
106
                'fields' => [
107
                    'pets' => [
108
                        'type'    => Type::listOf($petType),
109
                        'resolve' => function () {
110
                            return [new Dog('Odie', true), new Cat('Garfield', false)];
111
                        },
112
                    ],
113
                ],
114
            ]),
115
            'types'      => [$catType, $dogType],
116
            'typeLoader' => function ($name) use ($dogType, $petType, $catType) {
117
                switch ($name) {
118
                    case 'Dog':
119
                        return $dogType;
120
                    case 'Pet':
121
                        return $petType;
122
                    case 'Cat':
123
                        return $catType;
124
                }
125
            },
126
        ]);
127
128
        $query = '{
129
          pets {
130
            name
131
            ... on Dog {
132
              woofs
133
            }
134
            ... on Cat {
135
              meows
136
            }
137
          }
138
        }';
139
140
        $expected = new ExecutionResult([
141
            'pets' => [
142
                ['name' => 'Odie', 'woofs' => true],
143
                ['name' => 'Garfield', 'meows' => false],
144
            ],
145
        ]);
146
147
        Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN);
148
        $result = Executor::execute($schema, Parser::parse($query));
149
        self::assertEquals($expected, $result);
150
151
        Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN);
152
        $result = Executor::execute($schema, Parser::parse($query));
153
        self::assertEquals(1, count($result->errors));
0 ignored issues
show
Bug introduced by
The property errors does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
154
        self::assertInstanceOf(Error::class, $result->errors[0]->getPrevious());
155
156
        self::assertEquals(
157
            'GraphQL Interface Type `Pet` returned `null` from it`s `resolveType` function for value: instance of ' .
158
            'GraphQL\Tests\Executor\TestClasses\Dog. Switching to slow resolution method using `isTypeOf` of all possible ' .
159
            'implementations. It requires full schema scan and degrades query performance significantly.  ' .
160
            'Make sure your `resolveType` always returns valid implementation or throws.',
161
            $result->errors[0]->getMessage()
162
        );
163
    }
164
165
    public function testHintsOnConflictingTypeInstancesInDefinitions() : void
166
    {
167
        $calls      = [];
168
        $typeLoader = function ($name) use (&$calls) {
169
            $calls[] = $name;
170
            switch ($name) {
171
                case 'Test':
172
                    return new ObjectType([
173
                        'name'   => 'Test',
174
                        'fields' => function () {
175
                            return [
176
                                'test' => Type::string(),
177
                            ];
178
                        },
179
                    ]);
180
                default:
181
                    return null;
182
            }
183
        };
184
185
        $query = new ObjectType([
186
            'name'   => 'Query',
187
            'fields' => function () use ($typeLoader) {
188
                return [
189
                    'test' => $typeLoader('Test'),
190
                ];
191
            },
192
        ]);
193
194
        $schema = new Schema([
195
            'query'      => $query,
196
            'typeLoader' => $typeLoader,
197
        ]);
198
199
        $query = '
200
            {
201
                test {
202
                    test
203
                }
204
            }
205
        ';
206
207
        self::assertEquals([], $calls);
208
        $result = Executor::execute($schema, Parser::parse($query), ['test' => ['test' => 'value']]);
209
        self::assertEquals(['Test', 'Test'], $calls);
210
211
        self::assertEquals(
212
            'Schema must contain unique named types but contains multiple types named "Test". ' .
213
            'Make sure that type loader returns the same instance as defined in Query.test ' .
214
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
215
            $result->errors[0]->getMessage()
0 ignored issues
show
Bug introduced by
The property errors does not seem to exist on GraphQL\Executor\Promise\Promise.
Loading history...
216
        );
217
        self::assertInstanceOf(
218
            InvariantViolation::class,
219
            $result->errors[0]->getPrevious()
220
        );
221
    }
222
223
    public function testSimpleQuery() : void
224
    {
225
        $schema = new Schema([
226
            'query'      => $this->loadType('Query'),
227
            'typeLoader' => function ($name) {
228
                return $this->loadType($name, true);
229
            },
230
        ]);
231
232
        $query  = '{ object { string } }';
233
        $result = Executor::execute(
234
            $schema,
235
            Parser::parse($query),
236
            ['object' => ['string' => 'test']]
237
        );
238
239
        $expected              = [
240
            'data' => ['object' => ['string' => 'test']],
241
        ];
242
        $expectedExecutorCalls = [
243
            'Query.fields',
244
            'SomeObject',
245
            'SomeObject.fields',
246
        ];
247
        self::assertEquals($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

247
        self::assertEquals($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...
248
        self::assertEquals($expectedExecutorCalls, $this->calls);
249
    }
250
251
    public function loadType($name, $isExecutorCall = false)
252
    {
253
        if ($isExecutorCall) {
254
            $this->calls[] = $name;
255
        }
256
        $this->loadedTypes[$name] = true;
257
258
        switch ($name) {
259
            case 'Query':
260
                return $this->queryType ?: $this->queryType = new ObjectType([
261
                    'name'   => 'Query',
262
                    'fields' => function () {
263
                        $this->calls[] = 'Query.fields';
264
265
                        return [
266
                            'object' => ['type' => $this->loadType('SomeObject')],
267
                            'other'  => ['type' => $this->loadType('OtherObject')],
268
                        ];
269
                    },
270
                ]);
271
            case 'SomeObject':
272
                return $this->someObjectType ?: $this->someObjectType = new ObjectType([
273
                    'name'       => 'SomeObject',
274
                    'fields'     => function () {
275
                        $this->calls[] = 'SomeObject.fields';
276
277
                        return [
278
                            'string' => ['type' => Type::string()],
279
                            'object' => ['type' => $this->someObjectType],
280
                        ];
281
                    },
282
                    'interfaces' => function () {
283
                        $this->calls[] = 'SomeObject.interfaces';
284
285
                        return [
286
                            $this->loadType('SomeInterface'),
287
                        ];
288
                    },
289
                ]);
290
            case 'OtherObject':
291
                return $this->otherObjectType ?: $this->otherObjectType = new ObjectType([
292
                    'name'   => 'OtherObject',
293
                    'fields' => function () {
294
                        $this->calls[] = 'OtherObject.fields';
295
296
                        return [
297
                            'union' => ['type' => $this->loadType('SomeUnion')],
298
                            'iface' => ['type' => Type::nonNull($this->loadType('SomeInterface'))],
299
                        ];
300
                    },
301
                ]);
302
            case 'DeeperObject':
303
                return $this->deeperObjectType ?: $this->deeperObjectType = new ObjectType([
304
                    'name'   => 'DeeperObject',
305
                    'fields' => function () {
306
                        return [
307
                            'scalar' => ['type' => $this->loadType('SomeScalar')],
308
                        ];
309
                    },
310
                ]);
311
            case 'SomeScalar':
312
                return $this->someScalarType ?: $this->someScalarType = new CustomScalarType([
313
                    'name'         => 'SomeScalar',
314
                    'serialize'    => function ($value) {
315
                        return $value;
316
                    },
317
                    'parseValue'   => function ($value) {
318
                        return $value;
319
                    },
320
                    'parseLiteral' => function () {
321
                    },
322
                ]);
323
            case 'SomeUnion':
324
                return $this->someUnionType ?: $this->someUnionType = new UnionType([
325
                    'name'        => 'SomeUnion',
326
                    'resolveType' => function () {
327
                        $this->calls[] = 'SomeUnion.resolveType';
328
329
                        return $this->loadType('DeeperObject');
330
                    },
331
                    'types'       => function () {
332
                        $this->calls[] = 'SomeUnion.types';
333
334
                        return [$this->loadType('DeeperObject')];
335
                    },
336
                ]);
337
            case 'SomeInterface':
338
                return $this->someInterfaceType ?: $this->someInterfaceType = new InterfaceType([
339
                    'name'        => 'SomeInterface',
340
                    'resolveType' => function () {
341
                        $this->calls[] = 'SomeInterface.resolveType';
342
343
                        return $this->loadType('SomeObject');
344
                    },
345
                    'fields'      => function () {
346
                        $this->calls[] = 'SomeInterface.fields';
347
348
                        return [
349
                            'string' => ['type' => Type::string()],
350
                        ];
351
                    },
352
                ]);
353
            default:
354
                return null;
355
        }
356
    }
357
358
    public function testDeepQuery() : void
359
    {
360
        $schema = new Schema([
361
            'query'      => $this->loadType('Query'),
362
            'typeLoader' => function ($name) {
363
                return $this->loadType($name, true);
364
            },
365
        ]);
366
367
        $query  = '{ object { object { object { string } } } }';
368
        $result = Executor::execute(
369
            $schema,
370
            Parser::parse($query),
371
            ['object' => ['object' => ['object' => ['string' => 'test']]]]
372
        );
373
374
        $expected            = [
375
            'data' => ['object' => ['object' => ['object' => ['string' => 'test']]]],
376
        ];
377
        $expectedLoadedTypes = [
378
            'Query'       => true,
379
            'SomeObject'  => true,
380
            'OtherObject' => true,
381
        ];
382
383
        self::assertEquals($expected, $result->toArray(true));
384
        self::assertEquals($expectedLoadedTypes, $this->loadedTypes);
385
386
        $expectedExecutorCalls = [
387
            'Query.fields',
388
            'SomeObject',
389
            'SomeObject.fields',
390
        ];
391
        self::assertEquals($expectedExecutorCalls, $this->calls);
392
    }
393
394
    public function testResolveUnion() : void
395
    {
396
        $schema = new Schema([
397
            'query'      => $this->loadType('Query'),
398
            'typeLoader' => function ($name) {
399
                return $this->loadType($name, true);
400
            },
401
        ]);
402
403
        $query  = '
404
            { 
405
                other { 
406
                    union {
407
                        scalar 
408
                    } 
409
                } 
410
            }
411
        ';
412
        $result = Executor::execute(
413
            $schema,
414
            Parser::parse($query),
415
            ['other' => ['union' => ['scalar' => 'test']]]
416
        );
417
418
        $expected            = [
419
            'data' => ['other' => ['union' => ['scalar' => 'test']]],
420
        ];
421
        $expectedLoadedTypes = [
422
            'Query'         => true,
423
            'SomeObject'    => true,
424
            'OtherObject'   => true,
425
            'SomeUnion'     => true,
426
            'SomeInterface' => true,
427
            'DeeperObject'  => true,
428
            'SomeScalar'    => true,
429
        ];
430
431
        self::assertEquals($expected, $result->toArray(true));
432
        self::assertEquals($expectedLoadedTypes, $this->loadedTypes);
433
434
        $expectedCalls = [
435
            'Query.fields',
436
            'OtherObject',
437
            'OtherObject.fields',
438
            'SomeUnion',
439
            'SomeUnion.resolveType',
440
            'SomeUnion.types',
441
            'DeeperObject',
442
            'SomeScalar',
443
        ];
444
        self::assertEquals($expectedCalls, $this->calls);
445
    }
446
}
447