Passed
Pull Request — master (#586)
by Šimon
12:52
created

DefinitionTest   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 1674
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 73
eloc 794
dl 0
loc 1674
rs 2.366
c 1
b 0
f 0

How to fix   Complexity   

Complex Class

Complex classes like DefinitionTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefinitionTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Type;
6
7
use GraphQL\Error\InvariantViolation;
8
use GraphQL\Tests\Type\TestClasses\MyCustomType;
9
use GraphQL\Tests\Type\TestClasses\OtherCustom;
10
use GraphQL\Type\Definition\BooleanType;
11
use GraphQL\Type\Definition\CustomScalarType;
12
use GraphQL\Type\Definition\EnumType;
13
use GraphQL\Type\Definition\FloatType;
14
use GraphQL\Type\Definition\IDType;
15
use GraphQL\Type\Definition\InputObjectType;
16
use GraphQL\Type\Definition\InterfaceType;
17
use GraphQL\Type\Definition\IntType;
18
use GraphQL\Type\Definition\ListOfType;
19
use GraphQL\Type\Definition\NonNull;
20
use GraphQL\Type\Definition\ObjectType;
21
use GraphQL\Type\Definition\Type;
22
use GraphQL\Type\Definition\UnionType;
23
use GraphQL\Type\Schema;
24
use GraphQL\Utils\Utils;
25
use PHPUnit\Framework\TestCase;
26
use stdClass;
27
use Throwable;
28
use function count;
29
use function get_class;
30
use function json_encode;
31
use function sprintf;
32
33
class DefinitionTest extends TestCase
34
{
35
    /** @var ObjectType */
36
    public $blogImage;
37
38
    /** @var ObjectType */
39
    public $blogArticle;
40
41
    /** @var ObjectType */
42
    public $blogAuthor;
43
44
    /** @var ObjectType */
45
    public $blogMutation;
46
47
    /** @var ObjectType */
48
    public $blogQuery;
49
50
    /** @var ObjectType */
51
    public $blogSubscription;
52
53
    /** @var ObjectType */
54
    public $objectType;
55
56
    /** @var ObjectType */
57
    public $objectWithIsTypeOf;
58
59
    /** @var InterfaceType */
60
    public $interfaceType;
61
62
    /** @var UnionType */
63
    public $unionType;
64
65
    /** @var EnumType */
66
    public $enumType;
67
68
    /** @var InputObjectType */
69
    public $inputObjectType;
70
71
    /** @var CustomScalarType */
72
    public $scalarType;
73
74
    public function setUp()
75
    {
76
        $this->objectType      = new ObjectType(['name' => 'Object', 'fields' => ['tmp' => Type::string()]]);
77
        $this->interfaceType   = new InterfaceType(['name' => 'Interface']);
78
        $this->unionType       = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]);
79
        $this->enumType        = new EnumType(['name' => 'Enum']);
80
        $this->inputObjectType = new InputObjectType(['name' => 'InputObject']);
81
82
        $this->objectWithIsTypeOf = new ObjectType([
83
            'name'   => 'ObjectWithIsTypeOf',
84
            'fields' => ['f' => ['type' => Type::string()]],
85
        ]);
86
87
        $this->scalarType = new CustomScalarType([
88
            'name'         => 'Scalar',
89
            'serialize'    => static function () {
90
            },
91
            'parseValue'   => static function () {
92
            },
93
            'parseLiteral' => static function () {
94
            },
95
        ]);
96
97
        $this->blogImage = new ObjectType([
98
            'name'   => 'Image',
99
            'fields' => [
100
                'url'    => ['type' => Type::string()],
101
                'width'  => ['type' => Type::int()],
102
                'height' => ['type' => Type::int()],
103
            ],
104
        ]);
105
106
        $this->blogAuthor = new ObjectType([
107
            'name'   => 'Author',
108
            'fields' => function () {
109
                return [
110
                    'id'            => ['type' => Type::string()],
111
                    'name'          => ['type' => Type::string()],
112
                    'pic'           => [
113
                        'type' => $this->blogImage,
114
                        'args' => [
115
                            'width'  => ['type' => Type::int()],
116
                            'height' => ['type' => Type::int()],
117
                        ],
118
                    ],
119
                    'recentArticle' => $this->blogArticle,
120
                ];
121
            },
122
        ]);
123
124
        $this->blogArticle = new ObjectType([
125
            'name'   => 'Article',
126
            'fields' => [
127
                'id'          => ['type' => Type::string()],
128
                'isPublished' => ['type' => Type::boolean()],
129
                'author'      => ['type' => $this->blogAuthor],
130
                'title'       => ['type' => Type::string()],
131
                'body'        => ['type' => Type::string()],
132
            ],
133
        ]);
134
135
        $this->blogQuery = new ObjectType([
136
            'name'   => 'Query',
137
            'fields' => [
138
                'article' => [
139
                    'type' => $this->blogArticle,
140
                    'args' => [
141
                        'id' => ['type' => Type::string()],
142
                    ],
143
                ],
144
                'feed'    => ['type' => new ListOfType($this->blogArticle)],
145
            ],
146
        ]);
147
148
        $this->blogMutation = new ObjectType([
149
            'name'   => 'Mutation',
150
            'fields' => [
151
                'writeArticle' => ['type' => $this->blogArticle],
152
            ],
153
        ]);
154
155
        $this->blogSubscription = new ObjectType([
156
            'name'   => 'Subscription',
157
            'fields' => [
158
                'articleSubscribe' => [
159
                    'args' => ['id' => ['type' => Type::string()]],
160
                    'type' => $this->blogArticle,
161
                ],
162
            ],
163
        ]);
164
    }
165
166
    // Type System: Example
167
168
    /**
169
     * @see it('defines a query only schema')
170
     */
171
    public function testDefinesAQueryOnlySchema() : void
172
    {
173
        $blogSchema = new Schema([
174
            'query' => $this->blogQuery,
175
        ]);
176
177
        self::assertSame($blogSchema->getQueryType(), $this->blogQuery);
178
179
        $articleField = $this->blogQuery->getField('article');
180
        self::assertSame($articleField->getType(), $this->blogArticle);
181
        self::assertSame($articleField->getType()->name, 'Article');
182
        self::assertSame($articleField->name, 'article');
183
184
        /** @var ObjectType $articleFieldType */
185
        $articleFieldType = $articleField->getType();
186
        $titleField       = $articleFieldType->getField('title');
187
188
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $titleField);
189
        self::assertSame('title', $titleField->name);
190
        self::assertSame(Type::string(), $titleField->getType());
191
192
        $authorField = $articleFieldType->getField('author');
193
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $authorField);
194
195
        /** @var ObjectType $authorFieldType */
196
        $authorFieldType = $authorField->getType();
197
        self::assertSame($this->blogAuthor, $authorFieldType);
198
199
        $recentArticleField = $authorFieldType->getField('recentArticle');
200
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $recentArticleField);
201
        self::assertSame($this->blogArticle, $recentArticleField->getType());
202
203
        $feedField = $this->blogQuery->getField('feed');
204
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $feedField);
205
206
        /** @var ListOfType $feedFieldType */
207
        $feedFieldType = $feedField->getType();
208
        self::assertInstanceOf('GraphQL\Type\Definition\ListOfType', $feedFieldType);
209
        self::assertSame($this->blogArticle, $feedFieldType->getWrappedType());
210
    }
211
212
    /**
213
     * @see it('defines a mutation schema')
214
     */
215
    public function testDefinesAMutationSchema() : void
216
    {
217
        $schema = new Schema([
218
            'query'    => $this->blogQuery,
219
            'mutation' => $this->blogMutation,
220
        ]);
221
222
        self::assertSame($this->blogMutation, $schema->getMutationType());
223
        $writeMutation = $this->blogMutation->getField('writeArticle');
224
225
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $writeMutation);
226
        self::assertSame($this->blogArticle, $writeMutation->getType());
227
        self::assertSame('Article', $writeMutation->getType()->name);
228
        self::assertSame('writeArticle', $writeMutation->name);
229
    }
230
231
    /**
232
     * @see it('defines a subscription schema')
233
     */
234
    public function testDefinesSubscriptionSchema() : void
235
    {
236
        $schema = new Schema([
237
            'query'        => $this->blogQuery,
238
            'subscription' => $this->blogSubscription,
239
        ]);
240
241
        self::assertEquals($this->blogSubscription, $schema->getSubscriptionType());
242
243
        $sub = $this->blogSubscription->getField('articleSubscribe');
244
        self::assertEquals($sub->getType(), $this->blogArticle);
245
        self::assertEquals($sub->getType()->name, 'Article');
246
        self::assertEquals($sub->name, 'articleSubscribe');
247
    }
248
249
    /**
250
     * @see it('defines an enum type with deprecated value')
251
     */
252
    public function testDefinesEnumTypeWithDeprecatedValue() : void
253
    {
254
        $enumTypeWithDeprecatedValue = new EnumType([
255
            'name'   => 'EnumWithDeprecatedValue',
256
            'values' => [
257
                'foo' => ['deprecationReason' => 'Just because'],
258
            ],
259
        ]);
260
261
        $value = $enumTypeWithDeprecatedValue->getValues()[0];
262
263
        self::assertArraySubset(
264
            [
265
                'name'              => 'foo',
266
                'description'       => null,
267
                'deprecationReason' => 'Just because',
268
                'value'             => 'foo',
269
                'astNode'           => null,
270
            ],
271
            (array) $value
272
        );
273
274
        self::assertEquals(true, $value->isDeprecated());
275
    }
276
277
    /**
278
     * @see it('defines an enum type with a value of `null` and `undefined`')
279
     */
280
    public function testDefinesAnEnumTypeWithAValueOfNullAndUndefined() : void
281
    {
282
        $EnumTypeWithNullishValue = new EnumType([
283
            'name'   => 'EnumWithNullishValue',
284
            'values' => [
285
                'NULL'      => ['value' => null],
286
                'UNDEFINED' => ['value' => null],
287
            ],
288
        ]);
289
290
        $expected = [
291
            [
292
                'name'              => 'NULL',
293
                'description'       => null,
294
                'deprecationReason' => null,
295
                'value'             => null,
296
                'astNode'           => null,
297
            ],
298
            [
299
                'name'              => 'UNDEFINED',
300
                'description'       => null,
301
                'deprecationReason' => null,
302
                'value'             => null,
303
                'astNode'           => null,
304
            ],
305
        ];
306
307
        $actual = $EnumTypeWithNullishValue->getValues();
308
309
        self::assertEquals(count($expected), count($actual));
310
        self::assertArraySubset($expected[0], (array) $actual[0]);
311
        self::assertArraySubset($expected[1], (array) $actual[1]);
312
    }
313
314
    /**
315
     * @see it('defines an object type with deprecated field')
316
     */
317
    public function testDefinesAnObjectTypeWithDeprecatedField() : void
318
    {
319
        $TypeWithDeprecatedField = new ObjectType([
320
            'name'   => 'foo',
321
            'fields' => [
322
                'bar' => [
323
                    'type'              => Type::string(),
324
                    'deprecationReason' => 'A terrible reason',
325
                ],
326
            ],
327
        ]);
328
329
        $field = $TypeWithDeprecatedField->getField('bar');
330
331
        self::assertEquals(Type::string(), $field->getType());
332
        self::assertEquals(true, $field->isDeprecated());
333
        self::assertEquals('A terrible reason', $field->deprecationReason);
334
        self::assertEquals('bar', $field->name);
335
        self::assertEquals([], $field->args);
336
    }
337
338
    /**
339
     * @see it('includes nested input objects in the map')
340
     */
341
    public function testIncludesNestedInputObjectInTheMap() : void
342
    {
343
        $nestedInputObject = new InputObjectType([
344
            'name'   => 'NestedInputObject',
345
            'fields' => ['value' => ['type' => Type::string()]],
346
        ]);
347
        $someInputObject   = new InputObjectType([
348
            'name'   => 'SomeInputObject',
349
            'fields' => ['nested' => ['type' => $nestedInputObject]],
350
        ]);
351
        $someMutation      = new ObjectType([
352
            'name'   => 'SomeMutation',
353
            'fields' => [
354
                'mutateSomething' => [
355
                    'type' => $this->blogArticle,
356
                    'args' => ['input' => ['type' => $someInputObject]],
357
                ],
358
            ],
359
        ]);
360
361
        $schema = new Schema([
362
            'query'    => $this->blogQuery,
363
            'mutation' => $someMutation,
364
        ]);
365
        self::assertSame($nestedInputObject, $schema->getType('NestedInputObject'));
366
    }
367
368
    /**
369
     * @see it('includes interface possible types in the type map')
370
     */
371
    public function testIncludesInterfaceSubtypesInTheTypeMap() : void
372
    {
373
        $someInterface = new InterfaceType([
374
            'name'   => 'SomeInterface',
375
            'fields' => [
376
                'f' => ['type' => Type::int()],
377
            ],
378
        ]);
379
380
        $someSubtype = new ObjectType([
381
            'name'       => 'SomeSubtype',
382
            'fields'     => [
383
                'f' => ['type' => Type::int()],
384
            ],
385
            'interfaces' => [$someInterface],
386
        ]);
387
388
        $schema = new Schema([
389
            'query' => new ObjectType([
390
                'name'   => 'Query',
391
                'fields' => [
392
                    'iface' => ['type' => $someInterface],
393
                ],
394
            ]),
395
            'types' => [$someSubtype],
396
        ]);
397
        self::assertSame($someSubtype, $schema->getType('SomeSubtype'));
398
    }
399
400
    /**
401
     * @see it('includes interfaces' thunk subtypes in the type map')
402
     */
403
    public function testIncludesInterfacesThunkSubtypesInTheTypeMap() : void
404
    {
405
        $someInterface = null;
406
407
        $someSubtype = new ObjectType([
408
            'name'       => 'SomeSubtype',
409
            'fields'     => [
410
                'f' => ['type' => Type::int()],
411
            ],
412
            'interfaces' => static function () use (&$someInterface) {
413
                return [$someInterface];
414
            },
415
        ]);
416
417
        $someInterface = new InterfaceType([
418
            'name'   => 'SomeInterface',
419
            'fields' => [
420
                'f' => ['type' => Type::int()],
421
            ],
422
        ]);
423
424
        $schema = new Schema([
425
            'query' => new ObjectType([
426
                'name'   => 'Query',
427
                'fields' => [
428
                    'iface' => ['type' => $someInterface],
429
                ],
430
            ]),
431
            'types' => [$someSubtype],
432
        ]);
433
434
        self::assertSame($someSubtype, $schema->getType('SomeSubtype'));
435
    }
436
437
    /**
438
     * @see it('stringifies simple types')
439
     */
440
    public function testStringifiesSimpleTypes() : void
441
    {
442
        self::assertSame('Int', (string) Type::int());
443
        self::assertSame('Article', (string) $this->blogArticle);
444
445
        self::assertSame('Interface', (string) $this->interfaceType);
446
        self::assertSame('Union', (string) $this->unionType);
447
        self::assertSame('Enum', (string) $this->enumType);
448
        self::assertSame('InputObject', (string) $this->inputObjectType);
449
        self::assertSame('Object', (string) $this->objectType);
450
451
        self::assertSame('Int!', (string) new NonNull(Type::int()));
452
        self::assertSame('[Int]', (string) new ListOfType(Type::int()));
453
        self::assertSame('[Int]!', (string) new NonNull(new ListOfType(Type::int())));
454
        self::assertSame('[Int!]', (string) new ListOfType(new NonNull(Type::int())));
455
        self::assertSame('[[Int]]', (string) new ListOfType(new ListOfType(Type::int())));
456
    }
457
458
    /**
459
     * @see it('JSON stringifies simple types')
460
     */
461
    public function testJSONStringifiesSimpleTypes() : void
462
    {
463
        self::assertEquals('"Int"', json_encode(Type::int()));
464
        self::assertEquals('"Article"', json_encode($this->blogArticle));
465
        self::assertEquals('"Interface"', json_encode($this->interfaceType));
466
        self::assertEquals('"Union"', json_encode($this->unionType));
467
        self::assertEquals('"Enum"', json_encode($this->enumType));
468
        self::assertEquals('"InputObject"', json_encode($this->inputObjectType));
469
        self::assertEquals('"Int!"', json_encode(Type::nonNull(Type::int())));
470
        self::assertEquals('"[Int]"', json_encode(Type::listOf(Type::int())));
471
        self::assertEquals('"[Int]!"', json_encode(Type::nonNull(Type::listOf(Type::int()))));
472
        self::assertEquals('"[Int!]"', json_encode(Type::listOf(Type::nonNull(Type::int()))));
473
        self::assertEquals('"[[Int]]"', json_encode(Type::listOf(Type::listOf(Type::int()))));
474
    }
475
476
    /**
477
     * @see it('identifies input types')
478
     */
479
    public function testIdentifiesInputTypes() : void
480
    {
481
        $expected = [
482
            [Type::int(), true],
483
            [$this->objectType, false],
484
            [$this->interfaceType, false],
485
            [$this->unionType, false],
486
            [$this->enumType, true],
487
            [$this->inputObjectType, true],
488
489
            [Type::boolean(), true],
490
            [Type::float(),true ],
491
            [Type::id(), true],
492
            [Type::int(), true],
493
            [Type::listOf(Type::string()), true],
494
            [Type::listOf($this->objectType), false],
495
            [Type::nonNull(Type::string()), true],
496
            [Type::nonNull($this->objectType), false],
497
            [Type::string(), true],
498
        ];
499
500
        foreach ($expected as $index => $entry) {
501
            self::assertSame(
502
                $entry[1],
503
                Type::isInputType($entry[0]),
504
                sprintf('Type %s was detected incorrectly', $entry[0])
505
            );
506
        }
507
    }
508
509
    /**
510
     * @see it('identifies output types')
511
     */
512
    public function testIdentifiesOutputTypes() : void
513
    {
514
        $expected = [
515
            [Type::int(), true],
516
            [$this->objectType, true],
517
            [$this->interfaceType, true],
518
            [$this->unionType, true],
519
            [$this->enumType, true],
520
            [$this->inputObjectType, false],
521
522
            [Type::boolean(), true],
523
            [Type::float(),true ],
524
            [Type::id(), true],
525
            [Type::int(), true],
526
            [Type::listOf(Type::string()), true],
527
            [Type::listOf($this->objectType), true],
528
            [Type::nonNull(Type::string()), true],
529
            [Type::nonNull($this->objectType), true],
530
            [Type::string(), true],
531
        ];
532
533
        foreach ($expected as $index => $entry) {
534
            self::assertSame(
535
                $entry[1],
536
                Type::isOutputType($entry[0]),
537
                sprintf('Type %s was detected incorrectly', $entry[0])
538
            );
539
        }
540
    }
541
542
    /**
543
     * @see it('allows a thunk for Union member types')
544
     */
545
    public function testAllowsThunkForUnionTypes() : void
546
    {
547
        $union = new UnionType([
548
            'name'  => 'ThunkUnion',
549
            'types' => function () {
550
                return [$this->objectType];
551
            },
552
        ]);
553
554
        $types = $union->getTypes();
555
        self::assertEquals(1, count($types));
556
        self::assertSame($this->objectType, $types[0]);
557
    }
558
559
    public function testAllowsRecursiveDefinitions() : void
560
    {
561
        // See https://github.com/webonyx/graphql-php/issues/16
562
        $node = new InterfaceType([
563
            'name'   => 'Node',
564
            'fields' => [
565
                'id' => ['type' => Type::nonNull(Type::id())],
566
            ],
567
        ]);
568
569
        $blog   = null;
570
        $called = false;
571
572
        $user = new ObjectType([
573
            'name'       => 'User',
574
            'fields'     => static function () use (&$blog, &$called) {
575
                self::assertNotNull($blog, 'Blog type is expected to be defined at this point, but it is null');
576
                $called = true;
577
578
                return [
579
                    'id'    => ['type' => Type::nonNull(Type::id())],
580
                    'blogs' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($blog)))],
581
                ];
582
            },
583
            'interfaces' => static function () use ($node) {
584
                return [$node];
585
            },
586
        ]);
587
588
        $blog = new ObjectType([
589
            'name'       => 'Blog',
590
            'fields'     => static function () use ($user) {
591
                return [
592
                    'id'    => ['type' => Type::nonNull(Type::id())],
593
                    'owner' => ['type' => Type::nonNull($user)],
594
                ];
595
            },
596
            'interfaces' => static function () use ($node) {
597
                return [$node];
598
            },
599
        ]);
600
601
        $schema = new Schema([
602
            'query' => new ObjectType([
603
                'name'   => 'Query',
604
                'fields' => [
605
                    'node' => ['type' => $node],
606
                ],
607
            ]),
608
            'types' => [$user, $blog],
609
        ]);
610
611
        self::assertTrue($called);
612
        $schema->getType('Blog');
613
614
        self::assertEquals([$node], $blog->getInterfaces());
615
        self::assertEquals([$node], $user->getInterfaces());
616
617
        self::assertNotNull($user->getField('blogs'));
618
        /** @var NonNull $blogFieldReturnType */
619
        $blogFieldReturnType = $user->getField('blogs')->getType();
620
        self::assertSame($blog, $blogFieldReturnType->getWrappedType(true));
621
622
        self::assertNotNull($blog->getField('owner'));
623
        /** @var NonNull $ownerFieldReturnType */
624
        $ownerFieldReturnType = $blog->getField('owner')->getType();
625
        self::assertSame($user, $ownerFieldReturnType->getWrappedType(true));
626
    }
627
628
    public function testInputObjectTypeAllowsRecursiveDefinitions() : void
629
    {
630
        $called      = false;
631
        $inputObject = new InputObjectType([
632
            'name'   => 'InputObject',
633
            'fields' => static function () use (&$inputObject, &$called) {
634
                $called = true;
635
636
                return [
637
                    'value'  => ['type' => Type::string()],
638
                    'nested' => ['type' => $inputObject],
639
                ];
640
            },
641
        ]);
642
643
        $someMutation = new ObjectType([
644
            'name'   => 'SomeMutation',
645
            'fields' => [
646
                'mutateSomething' => [
647
                    'type' => $this->blogArticle,
648
                    'args' => ['input' => ['type' => $inputObject]],
649
                ],
650
            ],
651
        ]);
652
653
        $schema = new Schema([
654
            'query'    => $this->blogQuery,
655
            'mutation' => $someMutation,
656
        ]);
657
658
        self::assertSame($inputObject, $schema->getType('InputObject'));
659
        self::assertTrue($called);
660
        self::assertEquals(count($inputObject->getFields()), 2);
661
        self::assertSame($inputObject->getField('nested')->getType(), $inputObject);
662
        self::assertSame($someMutation->getField('mutateSomething')->getArg('input')->getType(), $inputObject);
663
    }
664
665
    public function testInterfaceTypeAllowsRecursiveDefinitions() : void
666
    {
667
        $called    = false;
668
        $interface = new InterfaceType([
669
            'name'   => 'SomeInterface',
670
            'fields' => static function () use (&$interface, &$called) {
671
                $called = true;
672
673
                return [
674
                    'value'  => ['type' => Type::string()],
675
                    'nested' => ['type' => $interface],
676
                ];
677
            },
678
        ]);
679
680
        $query = new ObjectType([
681
            'name'   => 'Query',
682
            'fields' => [
683
                'test' => ['type' => $interface],
684
            ],
685
        ]);
686
687
        $schema = new Schema(['query' => $query]);
688
689
        self::assertSame($interface, $schema->getType('SomeInterface'));
690
        self::assertTrue($called);
691
        self::assertEquals(count($interface->getFields()), 2);
692
        self::assertSame($interface->getField('nested')->getType(), $interface);
693
        self::assertSame($interface->getField('value')->getType(), Type::string());
694
    }
695
696
    public function testAllowsShorthandFieldDefinition() : void
697
    {
698
        $interface = new InterfaceType([
699
            'name'   => 'SomeInterface',
700
            'fields' => static function () use (&$interface) {
701
                return [
702
                    'value'   => Type::string(),
703
                    'nested'  => $interface,
704
                    'withArg' => [
705
                        'type' => Type::string(),
706
                        'args' => [
707
                            'arg1' => Type::int(),
708
                        ],
709
                    ],
710
                ];
711
            },
712
        ]);
713
714
        $query = new ObjectType([
715
            'name'   => 'Query',
716
            'fields' => ['test' => $interface],
717
        ]);
718
719
        $schema = new Schema(['query' => $query]);
720
721
        /** @var InterfaceType $SomeInterface */
722
        $SomeInterface = $schema->getType('SomeInterface');
723
724
        $valueField = $SomeInterface->getField('value');
725
        self::assertEquals(Type::string(), $valueField->getType());
726
727
        $nestedField = $SomeInterface->getField('nested');
728
        self::assertEquals($interface, $nestedField->getType());
729
730
        $withArg = $SomeInterface->getField('withArg');
731
        self::assertEquals(Type::string(), $withArg->getType());
732
733
        self::assertEquals('arg1', $withArg->args[0]->name);
734
        self::assertEquals(Type::int(), $withArg->args[0]->getType());
735
736
        /** @var ObjectType $Query */
737
        $Query     = $schema->getType('Query');
738
        $testField = $Query->getField('test');
739
        self::assertEquals($interface, $testField->getType());
740
        self::assertEquals('test', $testField->name);
741
    }
742
743
    public function testInfersNameFromClassname() : void
744
    {
745
        $myObj = new MyCustomType();
746
        self::assertEquals('MyCustom', $myObj->name);
747
748
        $otherCustom = new OtherCustom();
749
        self::assertEquals('OtherCustom', $otherCustom->name);
750
    }
751
752
    public function testAllowsOverridingInternalTypes() : void
753
    {
754
        $idType = new CustomScalarType([
755
            'name'         => 'ID',
756
            'serialize'    => static function () {
757
            },
758
            'parseValue'   => static function () {
759
            },
760
            'parseLiteral' => static function () {
761
            },
762
        ]);
763
764
        $schema = new Schema([
765
            'query' => new ObjectType(['name' => 'Query', 'fields' => []]),
766
            'types' => [$idType],
767
        ]);
768
769
        self::assertSame($idType, $schema->getType('ID'));
770
    }
771
772
    // Field config must be object
773
774
    /**
775
     * @see it('accepts an Object type with a field function')
776
     */
777
    public function testAcceptsAnObjectTypeWithAFieldFunction() : void
778
    {
779
        $objType = new ObjectType([
780
            'name'   => 'SomeObject',
781
            'fields' => static function () {
782
                return [
783
                    'f' => ['type' => Type::string()],
784
                ];
785
            },
786
        ]);
787
        $objType->assertValid();
788
        self::assertSame(Type::string(), $objType->getField('f')->getType());
789
    }
790
791
    /**
792
     * @see it('rejects an Object type field with undefined config')
793
     */
794
    public function testRejectsAnObjectTypeFieldWithUndefinedConfig() : void
795
    {
796
        $objType = new ObjectType([
797
            'name'   => 'SomeObject',
798
            'fields' => ['f' => null],
799
        ]);
800
        $this->expectException(InvariantViolation::class);
801
        $this->expectExceptionMessage(
802
            'SomeObject.f field config must be an array, but got: null'
803
        );
804
        $objType->getFields();
805
    }
806
807
    /**
808
     * @see it('rejects an Object type with incorrectly typed fields')
809
     */
810
    public function testRejectsAnObjectTypeWithIncorrectlyTypedFields() : void
811
    {
812
        $objType = new ObjectType([
813
            'name'   => 'SomeObject',
814
            'fields' => [['field' => Type::string()]],
815
        ]);
816
        $this->expectException(InvariantViolation::class);
817
        $this->expectExceptionMessage(
818
            'SomeObject fields must be an associative array with field names as keys or a ' .
819
            'function which returns such an array.'
820
        );
821
        $objType->getFields();
822
    }
823
824
    /**
825
     * @see it('rejects an Object type with a field function that returns incorrect type')
826
     */
827
    public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectType() : void
828
    {
829
        $objType = new ObjectType([
830
            'name'   => 'SomeObject',
831
            'fields' => static function () {
832
                return [['field' => Type::string()]];
833
            },
834
        ]);
835
        $this->expectException(InvariantViolation::class);
836
        $this->expectExceptionMessage(
837
            'SomeObject fields must be an associative array with field names as keys or a ' .
838
            'function which returns such an array.'
839
        );
840
        $objType->getFields();
841
    }
842
843
    // Field arg config must be object
844
845
    /**
846
     * @see it('accepts an Object type with field args')
847
     */
848
    public function testAcceptsAnObjectTypeWithFieldArgs() : void
849
    {
850
        $this->expectNotToPerformAssertions();
851
        $objType = new ObjectType([
852
            'name'   => 'SomeObject',
853
            'fields' => [
854
                'goodField' => [
855
                    'type' => Type::string(),
856
                    'args' => [
857
                        'goodArg' => ['type' => Type::string()],
858
                    ],
859
                ],
860
            ],
861
        ]);
862
        // Should not throw:
863
        $objType->assertValid();
864
    }
865
866
    // rejects an Object type with incorrectly typed field args
867
868
    /**
869
     * @see it('does not allow isDeprecated without deprecationReason on field')
870
     */
871
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField() : void
872
    {
873
        $OldObject = new ObjectType([
874
            'name'   => 'OldObject',
875
            'fields' => [
876
                'field' => [
877
                    'type'         => Type::string(),
878
                    'isDeprecated' => true,
879
                ],
880
            ],
881
        ]);
882
883
        $this->expectException(InvariantViolation::class);
884
        $this->expectExceptionMessage(
885
            'OldObject.field should provide "deprecationReason" instead of "isDeprecated".'
886
        );
887
        $OldObject->assertValid();
888
    }
889
890
    // Object interfaces must be array
891
892
    /**
893
     * @see it('accepts an Object type with array interfaces')
894
     */
895
    public function testAcceptsAnObjectTypeWithArrayInterfaces() : void
896
    {
897
        $objType = new ObjectType([
898
            'name'       => 'SomeObject',
899
            'interfaces' => [$this->interfaceType],
900
            'fields'     => ['f' => ['type' => Type::string()]],
901
        ]);
902
        self::assertSame($this->interfaceType, $objType->getInterfaces()[0]);
903
    }
904
905
    /**
906
     * @see it('accepts an Object type with interfaces as a function returning an array')
907
     */
908
    public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray() : void
909
    {
910
        $objType = new ObjectType([
911
            'name'       => 'SomeObject',
912
            'interfaces' => function () {
913
                return [$this->interfaceType];
914
            },
915
            'fields'     => ['f' => ['type' => Type::string()]],
916
        ]);
917
        self::assertSame($this->interfaceType, $objType->getInterfaces()[0]);
918
    }
919
920
    /**
921
     * @see it('rejects an Object type with incorrectly typed interfaces')
922
     */
923
    public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces() : void
924
    {
925
        $objType = new ObjectType([
926
            'name'       => 'SomeObject',
927
            'interfaces' => new stdClass(),
928
            'fields'     => ['f' => ['type' => Type::string()]],
929
        ]);
930
        $this->expectException(InvariantViolation::class);
931
        $this->expectExceptionMessage(
932
            'SomeObject interfaces must be an Array or a callable which returns an Array.'
933
        );
934
        $objType->getInterfaces();
935
    }
936
937
    /**
938
     * @see it('rejects an Object type with interfaces as a function returning an incorrect type')
939
     */
940
    public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType() : void
941
    {
942
        $objType = new ObjectType([
943
            'name'       => 'SomeObject',
944
            'interfaces' => static function () {
945
                return new stdClass();
946
            },
947
            'fields'     => ['f' => ['type' => Type::string()]],
948
        ]);
949
        $this->expectException(InvariantViolation::class);
950
        $this->expectExceptionMessage(
951
            'SomeObject interfaces must be an Array or a callable which returns an Array.'
952
        );
953
        $objType->getInterfaces();
954
    }
955
956
    // Type System: Object fields must have valid resolve values
957
958
    /**
959
     * @see it('accepts a lambda as an Object field resolver')
960
     */
961
    public function testAcceptsALambdaAsAnObjectFieldResolver() : void
962
    {
963
        $this->expectNotToPerformAssertions();
964
        // should not throw:
965
        $this->schemaWithObjectWithFieldResolver(static function () {
966
        });
967
    }
968
969
    private function schemaWithObjectWithFieldResolver($resolveValue)
970
    {
971
        $BadResolverType = new ObjectType([
972
            'name'   => 'BadResolver',
973
            'fields' => [
974
                'badField' => [
975
                    'type'    => Type::string(),
976
                    'resolve' => $resolveValue,
977
                ],
978
            ],
979
        ]);
980
981
        $schema = new Schema([
982
            'query' => new ObjectType([
983
                'name'   => 'Query',
984
                'fields' => [
985
                    'f' => ['type' => $BadResolverType],
986
                ],
987
            ]),
988
        ]);
989
        $schema->assertValid();
990
991
        return $schema;
992
    }
993
994
    /**
995
     * @see it('rejects an empty Object field resolver')
996
     */
997
    public function testRejectsAnEmptyObjectFieldResolver() : void
998
    {
999
        $this->expectException(InvariantViolation::class);
1000
        $this->expectExceptionMessage(
1001
            'BadResolver.badField field resolver must be a function if provided, but got: []'
1002
        );
1003
        $this->schemaWithObjectWithFieldResolver([]);
1004
    }
1005
1006
    /**
1007
     * @see it('rejects a constant scalar value resolver')
1008
     */
1009
    public function testRejectsAConstantScalarValueResolver() : void
1010
    {
1011
        $this->expectException(InvariantViolation::class);
1012
        $this->expectExceptionMessage(
1013
            'BadResolver.badField field resolver must be a function if provided, but got: 0'
1014
        );
1015
        $this->schemaWithObjectWithFieldResolver(0);
1016
    }
1017
1018
    // Type System: Interface types must be resolvable
1019
1020
    /**
1021
     * @see it('accepts an Interface type defining resolveType')
1022
     */
1023
    public function testAcceptsAnInterfaceTypeDefiningResolveType() : void
1024
    {
1025
        $this->expectNotToPerformAssertions();
1026
        $AnotherInterfaceType = new InterfaceType([
1027
            'name'   => 'AnotherInterface',
1028
            'fields' => ['f' => ['type' => Type::string()]],
1029
        ]);
1030
1031
        // Should not throw:
1032
        $this->schemaWithFieldType(
1033
            new ObjectType([
1034
                'name'       => 'SomeObject',
1035
                'interfaces' => [$AnotherInterfaceType],
1036
                'fields'     => ['f' => ['type' => Type::string()]],
1037
            ])
1038
        );
1039
    }
1040
1041
    private function schemaWithFieldType($type)
1042
    {
1043
        $schema = new Schema([
1044
            'query' => new ObjectType([
1045
                'name'   => 'Query',
1046
                'fields' => ['field' => ['type' => $type]],
1047
            ]),
1048
            'types' => [$type],
1049
        ]);
1050
        $schema->assertValid();
1051
1052
        return $schema;
1053
    }
1054
1055
    /**
1056
     * @see it('accepts an Interface with implementing type defining isTypeOf')
1057
     */
1058
    public function testAcceptsAnInterfaceWithImplementingTypeDefiningIsTypeOf() : void
1059
    {
1060
        $this->expectNotToPerformAssertions();
1061
        $InterfaceTypeWithoutResolveType = new InterfaceType([
1062
            'name'   => 'InterfaceTypeWithoutResolveType',
1063
            'fields' => ['f' => ['type' => Type::string()]],
1064
        ]);
1065
1066
        // Should not throw:
1067
        $this->schemaWithFieldType(
1068
            new ObjectType([
1069
                'name'       => 'SomeObject',
1070
                'interfaces' => [$InterfaceTypeWithoutResolveType],
1071
                'fields'     => ['f' => ['type' => Type::string()]],
1072
            ])
1073
        );
1074
    }
1075
1076
    /**
1077
     * @see it('accepts an Interface type defining resolveType with implementing type defining isTypeOf')
1078
     */
1079
    public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf() : void
1080
    {
1081
        $this->expectNotToPerformAssertions();
1082
        $AnotherInterfaceType = new InterfaceType([
1083
            'name'   => 'AnotherInterface',
1084
            'fields' => ['f' => ['type' => Type::string()]],
1085
        ]);
1086
1087
        // Should not throw:
1088
        $this->schemaWithFieldType(
1089
            new ObjectType([
1090
                'name'       => 'SomeObject',
1091
                'interfaces' => [$AnotherInterfaceType],
1092
                'fields'     => ['f' => ['type' => Type::string()]],
1093
            ])
1094
        );
1095
    }
1096
1097
    /**
1098
     * @see it('rejects an Interface type with an incorrect type for resolveType')
1099
     */
1100
    public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType() : void
1101
    {
1102
        $this->expectException(InvariantViolation::class);
1103
        $this->expectExceptionMessage(
1104
            'AnotherInterface must provide "resolveType" as a function, but got: instance of stdClass'
1105
        );
1106
1107
        $type = new InterfaceType([
1108
            'name'        => 'AnotherInterface',
1109
            'resolveType' => new stdClass(),
1110
            'fields'      => ['f' => ['type' => Type::string()]],
1111
        ]);
1112
        $type->assertValid();
1113
    }
1114
1115
    // Type System: Union types must be resolvable
1116
1117
    /**
1118
     * @see it('accepts a Union type defining resolveType')
1119
     */
1120
    public function testAcceptsAUnionTypeDefiningResolveType() : void
1121
    {
1122
        $this->expectNotToPerformAssertions();
1123
        // Should not throw:
1124
        $this->schemaWithFieldType(
1125
            new UnionType([
1126
                'name'  => 'SomeUnion',
1127
                'types' => [$this->objectType],
1128
            ])
1129
        );
1130
    }
1131
1132
    /**
1133
     * @see it('accepts a Union of Object types defining isTypeOf')
1134
     */
1135
    public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf() : void
1136
    {
1137
        $this->expectNotToPerformAssertions();
1138
        // Should not throw:
1139
        $this->schemaWithFieldType(
1140
            new UnionType([
1141
                'name'  => 'SomeUnion',
1142
                'types' => [$this->objectWithIsTypeOf],
1143
            ])
1144
        );
1145
    }
1146
1147
    /**
1148
     * @see it('accepts a Union type defining resolveType of Object types defining isTypeOf')
1149
     */
1150
    public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf() : void
1151
    {
1152
        $this->expectNotToPerformAssertions();
1153
        // Should not throw:
1154
        $this->schemaWithFieldType(
1155
            new UnionType([
1156
                'name'  => 'SomeUnion',
1157
                'types' => [$this->objectWithIsTypeOf],
1158
            ])
1159
        );
1160
    }
1161
1162
    /**
1163
     * @see it('rejects an Union type with an incorrect type for resolveType')
1164
     */
1165
    public function testRejectsAnUnionTypeWithAnIncorrectTypeForResolveType() : void
1166
    {
1167
        $this->expectException(InvariantViolation::class);
1168
        $this->expectExceptionMessage(
1169
            'SomeUnion must provide "resolveType" as a function, but got: instance of stdClass'
1170
        );
1171
        $this->schemaWithFieldType(
1172
            new UnionType([
1173
                'name'        => 'SomeUnion',
1174
                'resolveType' => new stdClass(),
1175
                'types'       => [$this->objectWithIsTypeOf],
1176
            ])
1177
        );
1178
    }
1179
1180
    /**
1181
     * @see it('accepts a Scalar type defining serialize')
1182
     */
1183
    public function testAcceptsAScalarTypeDefiningSerialize() : void
1184
    {
1185
        $this->expectNotToPerformAssertions();
1186
        // Should not throw
1187
        $this->schemaWithFieldType(
1188
            new CustomScalarType([
1189
                'name'      => 'SomeScalar',
1190
                'serialize' => static function () {
1191
                    return null;
1192
                },
1193
            ])
1194
        );
1195
    }
1196
1197
    // Type System: Scalar types must be serializable
1198
1199
    /**
1200
     * @see it('rejects a Scalar type not defining serialize')
1201
     */
1202
    public function testRejectsAScalarTypeNotDefiningSerialize() : void
1203
    {
1204
        $this->expectException(InvariantViolation::class);
1205
        $this->expectExceptionMessage(
1206
            'SomeScalar must provide "serialize" function. If this custom Scalar ' .
1207
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
1208
            'functions are also provided.'
1209
        );
1210
        $this->schemaWithFieldType(
1211
            new CustomScalarType(['name' => 'SomeScalar'])
1212
        );
1213
    }
1214
1215
    /**
1216
     * @see it('rejects a Scalar type defining serialize with an incorrect type')
1217
     */
1218
    public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType() : void
1219
    {
1220
        $this->expectException(InvariantViolation::class);
1221
        $this->expectExceptionMessage(
1222
            'SomeScalar must provide "serialize" function. If this custom Scalar ' .
1223
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
1224
            'functions are also provided.'
1225
        );
1226
        $this->schemaWithFieldType(
1227
            new CustomScalarType([
1228
                'name'      => 'SomeScalar',
1229
                'serialize' => new stdClass(),
1230
            ])
1231
        );
1232
    }
1233
1234
    /**
1235
     * @see it('accepts a Scalar type defining parseValue and parseLiteral')
1236
     */
1237
    public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral() : void
1238
    {
1239
        $this->expectNotToPerformAssertions();
1240
        // Should not throw:
1241
        $this->schemaWithFieldType(
1242
            new CustomScalarType([
1243
                'name'         => 'SomeScalar',
1244
                'serialize'    => static function () {
1245
                },
1246
                'parseValue'   => static function () {
1247
                },
1248
                'parseLiteral' => static function () {
1249
                },
1250
            ])
1251
        );
1252
    }
1253
1254
    /**
1255
     * @see it('rejects a Scalar type defining parseValue but not parseLiteral')
1256
     */
1257
    public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral() : void
1258
    {
1259
        $this->expectException(InvariantViolation::class);
1260
        $this->expectExceptionMessage(
1261
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1262
        );
1263
        $this->schemaWithFieldType(
1264
            new CustomScalarType([
1265
                'name'       => 'SomeScalar',
1266
                'serialize'  => static function () {
1267
                },
1268
                'parseValue' => static function () {
1269
                },
1270
            ])
1271
        );
1272
    }
1273
1274
    /**
1275
     * @see it('rejects a Scalar type defining parseLiteral but not parseValue')
1276
     */
1277
    public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue() : void
1278
    {
1279
        $this->expectException(InvariantViolation::class);
1280
        $this->expectExceptionMessage(
1281
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1282
        );
1283
        $this->schemaWithFieldType(
1284
            new CustomScalarType([
1285
                'name'         => 'SomeScalar',
1286
                'serialize'    => static function () {
1287
                },
1288
                'parseLiteral' => static function () {
1289
                },
1290
            ])
1291
        );
1292
    }
1293
1294
    /**
1295
     * @see it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type')
1296
     */
1297
    public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType() : void
1298
    {
1299
        $this->expectException(InvariantViolation::class);
1300
        $this->expectExceptionMessage(
1301
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1302
        );
1303
        $this->schemaWithFieldType(
1304
            new CustomScalarType([
1305
                'name'         => 'SomeScalar',
1306
                'serialize'    => static function () {
1307
                },
1308
                'parseValue'   => new stdClass(),
1309
                'parseLiteral' => new stdClass(),
1310
            ])
1311
        );
1312
    }
1313
1314
    /**
1315
     * @see it('accepts an Object type with an isTypeOf function')
1316
     */
1317
    public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction() : void
1318
    {
1319
        $this->expectNotToPerformAssertions();
1320
        // Should not throw
1321
        $this->schemaWithFieldType(
1322
            new ObjectType([
1323
                'name'   => 'AnotherObject',
1324
                'fields' => ['f' => ['type' => Type::string()]],
1325
            ])
1326
        );
1327
    }
1328
1329
    // Type System: Object types must be assertable
1330
1331
    /**
1332
     * @see it('rejects an Object type with an incorrect type for isTypeOf')
1333
     */
1334
    public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf() : void
1335
    {
1336
        $this->expectException(InvariantViolation::class);
1337
        $this->expectExceptionMessage(
1338
            'AnotherObject must provide "isTypeOf" as a function, but got: instance of stdClass'
1339
        );
1340
        $this->schemaWithFieldType(
1341
            new ObjectType([
1342
                'name'     => 'AnotherObject',
1343
                'isTypeOf' => new stdClass(),
1344
                'fields'   => ['f' => ['type' => Type::string()]],
1345
            ])
1346
        );
1347
    }
1348
1349
    /**
1350
     * @see it('accepts a Union type with array types')
1351
     */
1352
    public function testAcceptsAUnionTypeWithArrayTypes() : void
1353
    {
1354
        $this->expectNotToPerformAssertions();
1355
        // Should not throw:
1356
        $this->schemaWithFieldType(
1357
            new UnionType([
1358
                'name'  => 'SomeUnion',
1359
                'types' => [$this->objectType],
1360
            ])
1361
        );
1362
    }
1363
1364
    // Type System: Union types must be array
1365
1366
    /**
1367
     * @see it('accepts a Union type with function returning an array of types')
1368
     */
1369
    public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes() : void
1370
    {
1371
        $this->expectNotToPerformAssertions();
1372
        $this->schemaWithFieldType(
1373
            new UnionType([
1374
                'name'  => 'SomeUnion',
1375
                'types' => function () {
1376
                    return [$this->objectType];
1377
                },
1378
            ])
1379
        );
1380
    }
1381
1382
    /**
1383
     * @see it('rejects a Union type without types')
1384
     */
1385
    public function testRejectsAUnionTypeWithoutTypes() : void
1386
    {
1387
        $this->expectException(InvariantViolation::class);
1388
        $this->expectExceptionMessage(
1389
            'Must provide Array of types or a callable which returns such an array for Union SomeUnion'
1390
        );
1391
        $this->schemaWithFieldType(
1392
            new UnionType(['name' => 'SomeUnion'])
1393
        );
1394
    }
1395
1396
    /**
1397
     * @see it('rejects a Union type with incorrectly typed types')
1398
     */
1399
    public function testRejectsAUnionTypeWithIncorrectlyTypedTypes() : void
1400
    {
1401
        $this->expectException(InvariantViolation::class);
1402
        $this->expectExceptionMessage(
1403
            'Must provide Array of types or a callable which returns such an array for Union SomeUnion'
1404
        );
1405
        $this->schemaWithFieldType(
1406
            new UnionType([
1407
                'name'  => 'SomeUnion',
1408
                'types' => (object) ['test' => $this->objectType],
1409
            ])
1410
        );
1411
    }
1412
1413
    /**
1414
     * @see it('accepts an Input Object type with fields')
1415
     */
1416
    public function testAcceptsAnInputObjectTypeWithFields() : void
1417
    {
1418
        $inputObjType = new InputObjectType([
1419
            'name'   => 'SomeInputObject',
1420
            'fields' => [
1421
                'f' => ['type' => Type::string()],
1422
            ],
1423
        ]);
1424
        $inputObjType->assertValid();
1425
        self::assertSame(Type::string(), $inputObjType->getField('f')->getType());
1426
    }
1427
1428
    // Type System: Input Objects must have fields
1429
1430
    /**
1431
     * @see it('accepts an Input Object type with a field function')
1432
     */
1433
    public function testAcceptsAnInputObjectTypeWithAFieldFunction() : void
1434
    {
1435
        $inputObjType = new InputObjectType([
1436
            'name'   => 'SomeInputObject',
1437
            'fields' => static function () {
1438
                return [
1439
                    'f' => ['type' => Type::string()],
1440
                ];
1441
            },
1442
        ]);
1443
        $inputObjType->assertValid();
1444
        self::assertSame(Type::string(), $inputObjType->getField('f')->getType());
1445
    }
1446
1447
    /**
1448
     * @see it('rejects an Input Object type with incorrect fields')
1449
     */
1450
    public function testRejectsAnInputObjectTypeWithIncorrectFields() : void
1451
    {
1452
        $inputObjType = new InputObjectType([
1453
            'name'   => 'SomeInputObject',
1454
            'fields' => [],
1455
        ]);
1456
        $this->expectException(InvariantViolation::class);
1457
        $this->expectExceptionMessage(
1458
            'SomeInputObject fields must be an associative array with field names as keys or a callable ' .
1459
            'which returns such an array.'
1460
        );
1461
        $inputObjType->assertValid();
1462
    }
1463
1464
    /**
1465
     * @see it('rejects an Input Object type with fields function that returns incorrect type')
1466
     */
1467
    public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorrectType() : void
1468
    {
1469
        $inputObjType = new InputObjectType([
1470
            'name'   => 'SomeInputObject',
1471
            'fields' => static function () {
1472
                return [];
1473
            },
1474
        ]);
1475
        $this->expectException(InvariantViolation::class);
1476
        $this->expectExceptionMessage(
1477
            'SomeInputObject fields must be an associative array with field names as keys or a ' .
1478
            'callable which returns such an array.'
1479
        );
1480
        $inputObjType->assertValid();
1481
    }
1482
1483
    /**
1484
     * @see it('rejects an Input Object type with resolvers')
1485
     */
1486
    public function testRejectsAnInputObjectTypeWithResolvers() : void
1487
    {
1488
        $inputObjType = new InputObjectType([
1489
            'name'   => 'SomeInputObject',
1490
            'fields' => [
1491
                'f' => [
1492
                    'type'    => Type::string(),
1493
                    'resolve' => static function () {
1494
                        return 0;
1495
                    },
1496
                ],
1497
            ],
1498
        ]);
1499
        $this->expectException(InvariantViolation::class);
1500
        $this->expectExceptionMessage(
1501
            'SomeInputObject.f field type has a resolve property, ' .
1502
            'but Input Types cannot define resolvers.'
1503
        );
1504
        $inputObjType->assertValid();
1505
    }
1506
1507
    // Type System: Input Object fields must not have resolvers
1508
1509
    /**
1510
     * @see it('rejects an Input Object type with resolver constant')
1511
     */
1512
    public function testRejectsAnInputObjectTypeWithResolverConstant() : void
1513
    {
1514
        $inputObjType = new InputObjectType([
1515
            'name'   => 'SomeInputObject',
1516
            'fields' => [
1517
                'f' => [
1518
                    'type'    => Type::string(),
1519
                    'resolve' => new stdClass(),
1520
                ],
1521
            ],
1522
        ]);
1523
        $this->expectException(InvariantViolation::class);
1524
        $this->expectExceptionMessage(
1525
            'SomeInputObject.f field type has a resolve property, ' .
1526
            'but Input Types cannot define resolvers.'
1527
        );
1528
        $inputObjType->assertValid();
1529
    }
1530
1531
    /**
1532
     * @see it('accepts a well defined Enum type with empty value definition')
1533
     */
1534
    public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition() : void
1535
    {
1536
        $enumType = new EnumType([
1537
            'name'   => 'SomeEnum',
1538
            'values' => [
1539
                'FOO' => [],
1540
                'BAR' => [],
1541
            ],
1542
        ]);
1543
        self::assertEquals('FOO', $enumType->getValue('FOO')->value);
1544
        self::assertEquals('BAR', $enumType->getValue('BAR')->value);
1545
    }
1546
1547
    // Type System: Enum types must be well defined
1548
1549
    /**
1550
     * @see it('accepts a well defined Enum type with internal value definition')
1551
     */
1552
    public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition() : void
1553
    {
1554
        $enumType = new EnumType([
1555
            'name'   => 'SomeEnum',
1556
            'values' => [
1557
                'FOO' => ['value' => 10],
1558
                'BAR' => ['value' => 20],
1559
            ],
1560
        ]);
1561
        self::assertEquals(10, $enumType->getValue('FOO')->value);
1562
        self::assertEquals(20, $enumType->getValue('BAR')->value);
1563
    }
1564
1565
    /**
1566
     * @see it('rejects an Enum type with incorrectly typed values')
1567
     */
1568
    public function testRejectsAnEnumTypeWithIncorrectlyTypedValues() : void
1569
    {
1570
        $enumType = new EnumType([
1571
            'name'   => 'SomeEnum',
1572
            'values' => [['FOO' => 10]],
1573
        ]);
1574
        $this->expectException(InvariantViolation::class);
1575
        $this->expectExceptionMessage(
1576
            'SomeEnum values must be an array with value names as keys.'
1577
        );
1578
        $enumType->assertValid();
1579
    }
1580
1581
    /**
1582
     * @see it('does not allow isDeprecated without deprecationReason on enum')
1583
     */
1584
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() : void
1585
    {
1586
        $enumType = new EnumType([
1587
            'name'   => 'SomeEnum',
1588
            'values' => [
1589
                'FOO' => ['isDeprecated' => true],
1590
            ],
1591
        ]);
1592
        $this->expectException(InvariantViolation::class);
1593
        $this->expectExceptionMessage(
1594
            'SomeEnum.FOO should provide "deprecationReason" instead ' .
1595
            'of "isDeprecated".'
1596
        );
1597
        $enumType->assertValid();
1598
    }
1599
1600
    /**
1601
     * @see it('rejects a Schema which redefines a built-in type')
1602
     */
1603
    public function testRejectsASchemaWhichRedefinesABuiltInType() : void
1604
    {
1605
        $FakeString = new CustomScalarType([
1606
            'name'      => 'String',
1607
            'serialize' => static function () {
1608
            },
1609
        ]);
1610
1611
        $QueryType = new ObjectType([
1612
            'name'   => 'Query',
1613
            'fields' => [
1614
                'normal' => ['type' => Type::string()],
1615
                'fake'   => ['type' => $FakeString],
1616
            ],
1617
        ]);
1618
1619
        $this->expectException(InvariantViolation::class);
1620
        $this->expectExceptionMessage(
1621
            'Schema must contain unique named types but contains multiple types named "String" ' .
1622
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1623
        );
1624
        $schema = new Schema(['query' => $QueryType]);
1625
        $schema->assertValid();
1626
    }
1627
1628
    // Type System: A Schema must contain uniquely named types
1629
1630
    /**
1631
     * @see it('rejects a Schema which defines an object type twice')
1632
     */
1633
    public function testRejectsASchemaWhichDefinesAnObjectTypeTwice() : void
1634
    {
1635
        $A = new ObjectType([
1636
            'name'   => 'SameName',
1637
            'fields' => ['f' => ['type' => Type::string()]],
1638
        ]);
1639
1640
        $B = new ObjectType([
1641
            'name'   => 'SameName',
1642
            'fields' => ['f' => ['type' => Type::string()]],
1643
        ]);
1644
1645
        $QueryType = new ObjectType([
1646
            'name'   => 'Query',
1647
            'fields' => [
1648
                'a' => ['type' => $A],
1649
                'b' => ['type' => $B],
1650
            ],
1651
        ]);
1652
        $this->expectException(InvariantViolation::class);
1653
        $this->expectExceptionMessage(
1654
            'Schema must contain unique named types but contains multiple types named "SameName" ' .
1655
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1656
        );
1657
        $schema = new Schema(['query' => $QueryType]);
1658
        $schema->assertValid();
1659
    }
1660
1661
    /**
1662
     * @see it('rejects a Schema which have same named objects implementing an interface')
1663
     */
1664
    public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface() : void
1665
    {
1666
        $AnotherInterface = new InterfaceType([
1667
            'name'   => 'AnotherInterface',
1668
            'fields' => ['f' => ['type' => Type::string()]],
1669
        ]);
1670
1671
        $FirstBadObject = new ObjectType([
1672
            'name'       => 'BadObject',
1673
            'interfaces' => [$AnotherInterface],
1674
            'fields'     => ['f' => ['type' => Type::string()]],
1675
        ]);
1676
1677
        $SecondBadObject = new ObjectType([
1678
            'name'       => 'BadObject',
1679
            'interfaces' => [$AnotherInterface],
1680
            'fields'     => ['f' => ['type' => Type::string()]],
1681
        ]);
1682
1683
        $QueryType = new ObjectType([
1684
            'name'   => 'Query',
1685
            'fields' => [
1686
                'iface' => ['type' => $AnotherInterface],
1687
            ],
1688
        ]);
1689
1690
        $this->expectException(InvariantViolation::class);
1691
        $this->expectExceptionMessage(
1692
            'Schema must contain unique named types but contains multiple types named "BadObject" ' .
1693
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1694
        );
1695
        $schema = new Schema([
1696
            'query' => $QueryType,
1697
            'types' => [$FirstBadObject, $SecondBadObject],
1698
        ]);
1699
        $schema->assertValid();
1700
    }
1701
1702
    public function objectWithIsTypeOf() : ObjectType
1703
    {
1704
        return new ObjectType([
1705
            'name'   => 'ObjectWithIsTypeOf',
1706
            'fields' => ['f' => ['type' => Type::string()]],
1707
        ]);
1708
    }
1709
}
1710