Failed Conditions
Push — master ( 392b56...c3d69c )
by Vladimir
03:59
created

testAcceptsAnObjectTypeWithFieldArgs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 16
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
namespace GraphQL\Tests\Type;
3
4
require_once __DIR__ . '/TestClasses.php';
5
6
use GraphQL\Error\InvariantViolation;
7
use GraphQL\Type\Definition\CustomScalarType;
8
use GraphQL\Type\Schema;
9
use GraphQL\Type\Definition\EnumType;
10
use GraphQL\Type\Definition\InputObjectType;
11
use GraphQL\Type\Definition\InterfaceType;
12
use GraphQL\Type\Definition\ListOfType;
13
use GraphQL\Type\Definition\NonNull;
14
use GraphQL\Type\Definition\ObjectType;
15
use GraphQL\Type\Definition\Type;
16
use GraphQL\Type\Definition\UnionType;
17
use GraphQL\Utils\Utils;
18
use PHPUnit\Framework\TestCase;
19
20
class DefinitionTest extends TestCase
21
{
22
    /**
23
     * @var ObjectType
24
     */
25
    public $blogImage;
26
27
    /**
28
     * @var ObjectType
29
     */
30
    public $blogArticle;
31
32
    /**
33
     * @var ObjectType
34
     */
35
    public $blogAuthor;
36
37
    /**
38
     * @var ObjectType
39
     */
40
    public $blogMutation;
41
42
    /**
43
     * @var ObjectType
44
     */
45
    public $blogQuery;
46
47
    /**
48
     * @var ObjectType
49
     */
50
    public $blogSubscription;
51
52
    /**
53
     * @var ObjectType
54
     */
55
    public $objectType;
56
57
    /**
58
     * @var ObjectType
59
     */
60
    public $objectWithIsTypeOf;
61
62
    /**
63
     * @var InterfaceType
64
     */
65
    public $interfaceType;
66
67
    /**
68
     * @var UnionType
69
     */
70
    public $unionType;
71
72
    /**
73
     * @var EnumType
74
     */
75
    public $enumType;
76
77
    /**
78
     * @var InputObjectType
79
     */
80
    public $inputObjectType;
81
82
    /**
83
     * @var CustomScalarType
84
     */
85
    public $scalarType;
86
87
    public function setUp()
88
    {
89
        $this->objectType = new ObjectType(['name' => 'Object', 'fields' => ['tmp' => Type::string()]]);
90
        $this->interfaceType = new InterfaceType(['name' => 'Interface']);
91
        $this->unionType = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]);
92
        $this->enumType = new EnumType(['name' => 'Enum']);
93
        $this->inputObjectType = new InputObjectType(['name' => 'InputObject']);
94
95
        $this->objectWithIsTypeOf = new ObjectType([
96
            'name' => 'ObjectWithIsTypeOf',
97
            'fields' => ['f' => ['type' => Type::string()]],
98
        ]);
99
100
        $this->scalarType = new CustomScalarType([
101
            'name' => 'Scalar',
102
            'serialize' => function () {},
103
            'parseValue' => function () {},
104
            'parseLiteral' => function () {},
105
        ]);
106
107
        $this->blogImage = new ObjectType([
108
            'name' => 'Image',
109
            'fields' => [
110
                'url' => ['type' => Type::string()],
111
                'width' => ['type' => Type::int()],
112
                'height' => ['type' => Type::int()]
113
            ]
114
        ]);
115
116
        $this->blogAuthor = new ObjectType([
117
            'name' => 'Author',
118
            'fields' => function() {
119
                return [
120
                    'id' => ['type' => Type::string()],
121
                    'name' => ['type' => Type::string()],
122
                    'pic' => [ 'type' => $this->blogImage, 'args' => [
123
                        'width' => ['type' => Type::int()],
124
                        'height' => ['type' => Type::int()]
125
                    ]],
126
                    'recentArticle' => $this->blogArticle,
127
                ];
128
            },
129
        ]);
130
131
        $this->blogArticle = new ObjectType([
132
            'name' => 'Article',
133
            'fields' => [
134
                'id' => ['type' => Type::string()],
135
                'isPublished' => ['type' => Type::boolean()],
136
                'author' => ['type' => $this->blogAuthor],
137
                'title' => ['type' => Type::string()],
138
                'body' => ['type' => Type::string()]
139
            ]
140
        ]);
141
142
        $this->blogQuery = new ObjectType([
143
            'name' => 'Query',
144
            'fields' => [
145
                'article' => ['type' => $this->blogArticle, 'args' => [
146
                    'id' => ['type' => Type::string()]
147
                ]],
148
                'feed' => ['type' => new ListOfType($this->blogArticle)]
149
            ]
150
        ]);
151
152
        $this->blogMutation = new ObjectType([
153
            'name' => 'Mutation',
154
            'fields' => [
155
                'writeArticle' => ['type' => $this->blogArticle]
156
            ]
157
        ]);
158
159
        $this->blogSubscription = new ObjectType([
160
            'name' => 'Subscription',
161
            'fields' => [
162
                'articleSubscribe' => [
163
                    'args' => [ 'id' => [ 'type' => Type::string() ]],
164
                    'type' => $this->blogArticle
165
                ]
166
            ]
167
        ]);
168
    }
169
170
    // Type System: Example
171
172
    /**
173
     * @it defines a query only schema
174
     */
175
    public function testDefinesAQueryOnlySchema()
176
    {
177
        $blogSchema = new Schema([
178
            'query' => $this->blogQuery
179
        ]);
180
181
        $this->assertSame($blogSchema->getQueryType(), $this->blogQuery);
182
183
        $articleField = $this->blogQuery->getField('article');
184
        $this->assertSame($articleField->getType(), $this->blogArticle);
185
        $this->assertSame($articleField->getType()->name, 'Article');
186
        $this->assertSame($articleField->name, 'article');
187
188
        /** @var ObjectType $articleFieldType */
189
        $articleFieldType = $articleField->getType();
190
        $titleField = $articleFieldType->getField('title');
191
192
        $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $titleField);
193
        $this->assertSame('title', $titleField->name);
194
        $this->assertSame(Type::string(), $titleField->getType());
195
196
        $authorField = $articleFieldType->getField('author');
197
        $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $authorField);
198
199
        /** @var ObjectType $authorFieldType */
200
        $authorFieldType = $authorField->getType();
201
        $this->assertSame($this->blogAuthor, $authorFieldType);
202
203
        $recentArticleField = $authorFieldType->getField('recentArticle');
204
        $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $recentArticleField);
205
        $this->assertSame($this->blogArticle, $recentArticleField->getType());
206
207
        $feedField = $this->blogQuery->getField('feed');
208
        $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $feedField);
209
210
        /** @var ListOfType $feedFieldType */
211
        $feedFieldType = $feedField->getType();
212
        $this->assertInstanceOf('GraphQL\Type\Definition\ListOfType', $feedFieldType);
213
        $this->assertSame($this->blogArticle, $feedFieldType->getWrappedType());
214
    }
215
216
    /**
217
     * @it defines a mutation schema
218
     */
219
    public function testDefinesAMutationSchema()
220
    {
221
        $schema = new Schema([
222
            'query' => $this->blogQuery,
223
            'mutation' => $this->blogMutation
224
        ]);
225
226
        $this->assertSame($this->blogMutation, $schema->getMutationType());
227
        $writeMutation = $this->blogMutation->getField('writeArticle');
228
229
        $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $writeMutation);
230
        $this->assertSame($this->blogArticle, $writeMutation->getType());
231
        $this->assertSame('Article', $writeMutation->getType()->name);
232
        $this->assertSame('writeArticle', $writeMutation->name);
233
    }
234
235
    /**
236
     * @it defines a subscription schema
237
     */
238
    public function testDefinesSubscriptionSchema()
239
    {
240
        $schema = new Schema([
241
            'query' => $this->blogQuery,
242
            'subscription' => $this->blogSubscription
243
        ]);
244
245
        $this->assertEquals($this->blogSubscription, $schema->getSubscriptionType());
246
247
        $sub = $this->blogSubscription->getField('articleSubscribe');
248
        $this->assertEquals($sub->getType(), $this->blogArticle);
249
        $this->assertEquals($sub->getType()->name, 'Article');
250
        $this->assertEquals($sub->name, 'articleSubscribe');
251
    }
252
253
    /**
254
     * @it defines an enum type with deprecated value
255
     */
256
    public function testDefinesEnumTypeWithDeprecatedValue()
257
    {
258
        $enumTypeWithDeprecatedValue = new EnumType([
259
            'name' => 'EnumWithDeprecatedValue',
260
            'values' => [
261
                'foo' => ['deprecationReason' => 'Just because']
262
            ]
263
        ]);
264
265
        $value = $enumTypeWithDeprecatedValue->getValues()[0];
266
267
        $this->assertArraySubset([
268
            'name' => 'foo',
269
            'description' => null,
270
            'deprecationReason' => 'Just because',
271
            'value' => 'foo',
272
            'astNode' => null
273
        ], (array) $value);
274
275
        $this->assertEquals(true, $value->isDeprecated());
276
    }
277
278
    /**
279
     * @it defines an enum type with a value of `null` and `undefined`
280
     */
281
    public function testDefinesAnEnumTypeWithAValueOfNullAndUndefined()
282
    {
283
        $EnumTypeWithNullishValue = new EnumType([
284
            'name' => 'EnumWithNullishValue',
285
            'values' => [
286
                'NULL' => ['value' => null],
287
                'UNDEFINED' => ['value' => null],
288
            ]
289
        ]);
290
291
        $expected = [
292
            [
293
                'name' => 'NULL',
294
                'description' => null,
295
                'deprecationReason' => null,
296
                'value' => null,
297
                'astNode' => null,
298
            ],
299
            [
300
                'name' => 'UNDEFINED',
301
                'description' => null,
302
                'deprecationReason' => null,
303
                'value' => null,
304
                'astNode' => null,
305
            ],
306
        ];
307
308
        $actual = $EnumTypeWithNullishValue->getValues();
309
310
        $this->assertEquals(count($expected), count($actual));
311
        $this->assertArraySubset($expected[0], (array)$actual[0]);
312
        $this->assertArraySubset($expected[1], (array)$actual[1]);
313
    }
314
315
    /**
316
     * @it defines an object type with deprecated field
317
     */
318
    public function testDefinesAnObjectTypeWithDeprecatedField()
319
    {
320
        $TypeWithDeprecatedField = new ObjectType([
321
          'name' => 'foo',
322
          'fields' => [
323
            'bar' => [
324
              'type' => Type::string(),
325
              'deprecationReason' => 'A terrible reason'
326
            ]
327
          ]
328
        ]);
329
330
        $field = $TypeWithDeprecatedField->getField('bar');
331
332
        $this->assertEquals(Type::string(), $field->getType());
333
        $this->assertEquals(true, $field->isDeprecated());
334
        $this->assertEquals('A terrible reason', $field->deprecationReason);
335
        $this->assertEquals('bar', $field->name);
336
        $this->assertEquals([], $field->args);
337
    }
338
339
    /**
340
     * @it includes nested input objects in the map
341
     */
342
    public function testIncludesNestedInputObjectInTheMap()
343
    {
344
        $nestedInputObject = new InputObjectType([
345
            'name' => 'NestedInputObject',
346
            'fields' => ['value' => ['type' => Type::string()]]
347
        ]);
348
        $someInputObject = new InputObjectType([
349
            'name' => 'SomeInputObject',
350
            'fields' => ['nested' => ['type' => $nestedInputObject]]
351
        ]);
352
        $someMutation = new ObjectType([
353
            'name' => 'SomeMutation',
354
            'fields' => [
355
                'mutateSomething' => [
356
                    'type' => $this->blogArticle,
357
                    'args' => ['input' => ['type' => $someInputObject]]
358
                ]
359
            ]
360
        ]);
361
362
        $schema = new Schema([
363
            'query' => $this->blogQuery,
364
            'mutation' => $someMutation
365
        ]);
366
        $this->assertSame($nestedInputObject, $schema->getType('NestedInputObject'));
367
    }
368
369
    /**
370
     * @it includes interface possible types in the type map
371
     */
372
    public function testIncludesInterfaceSubtypesInTheTypeMap()
373
    {
374
        $someInterface = new InterfaceType([
375
            'name' => 'SomeInterface',
376
            'fields' => [
377
                'f' => ['type' => Type::int()]
378
            ]
379
        ]);
380
381
        $someSubtype = new ObjectType([
382
            'name' => 'SomeSubtype',
383
            'fields' => [
384
                'f' => ['type' => Type::int()]
385
            ],
386
            'interfaces' => [$someInterface],
387
        ]);
388
389
        $schema = new Schema([
390
            'query' => new ObjectType([
391
                'name' => 'Query',
392
                'fields' => [
393
                    'iface' => ['type' => $someInterface]
394
                ]
395
            ]),
396
            'types' => [$someSubtype]
397
        ]);
398
        $this->assertSame($someSubtype, $schema->getType('SomeSubtype'));
399
    }
400
401
    /**
402
     * @it includes interfaces' thunk subtypes in the type map
403
     */
404
    public function testIncludesInterfacesThunkSubtypesInTheTypeMap()
405
    {
406
        $someInterface = null;
407
408
        $someSubtype = new ObjectType([
409
            'name' => 'SomeSubtype',
410
            'fields' => [
411
                'f' => ['type' => Type::int()]
412
            ],
413
            'interfaces' => function() use (&$someInterface) { return [$someInterface]; },
414
        ]);
415
416
        $someInterface = new InterfaceType([
417
            'name' => 'SomeInterface',
418
            'fields' => [
419
                'f' => ['type' => Type::int()]
420
            ]
421
        ]);
422
423
        $schema = new Schema([
424
            'query' => new ObjectType([
425
                'name' => 'Query',
426
                'fields' => [
427
                    'iface' => ['type' => $someInterface]
428
                ]
429
            ]),
430
            'types' => [$someSubtype]
431
        ]);
432
433
        $this->assertSame($someSubtype, $schema->getType('SomeSubtype'));
434
    }
435
436
    /**
437
     * @it stringifies simple types
438
     */
439
    public function testStringifiesSimpleTypes()
440
    {
441
        $this->assertSame('Int', (string) Type::int());
442
        $this->assertSame('Article', (string) $this->blogArticle);
443
444
        $this->assertSame('Interface', (string) $this->interfaceType);
445
        $this->assertSame('Union', (string) $this->unionType);
446
        $this->assertSame('Enum', (string) $this->enumType);
447
        $this->assertSame('InputObject', (string) $this->inputObjectType);
448
        $this->assertSame('Object', (string) $this->objectType);
449
450
        $this->assertSame('Int!', (string) new NonNull(Type::int()));
451
        $this->assertSame('[Int]', (string) new ListOfType(Type::int()));
452
        $this->assertSame('[Int]!', (string) new NonNull(new ListOfType(Type::int())));
453
        $this->assertSame('[Int!]', (string) new ListOfType(new NonNull(Type::int())));
454
        $this->assertSame('[[Int]]', (string) new ListOfType(new ListOfType(Type::int())));
455
    }
456
457
    /**
458
     * @it JSON stringifies simple types
459
     */
460
    public function testJSONStringifiesSimpleTypes()
461
    {
462
        $this->assertEquals('"Int"', json_encode(Type::int()));
463
        $this->assertEquals('"Article"', json_encode($this->blogArticle));
464
        $this->assertEquals('"Interface"', json_encode($this->interfaceType));
465
        $this->assertEquals('"Union"', json_encode($this->unionType));
466
        $this->assertEquals('"Enum"', json_encode($this->enumType));
467
        $this->assertEquals('"InputObject"', json_encode($this->inputObjectType));
468
        $this->assertEquals('"Int!"', json_encode(Type::nonNull(Type::int())));
469
        $this->assertEquals('"[Int]"', json_encode(Type::listOf(Type::int())));
470
        $this->assertEquals('"[Int]!"', json_encode(Type::nonNull(Type::listOf(Type::int()))));
471
        $this->assertEquals('"[Int!]"', json_encode(Type::listOf(Type::nonNull(Type::int()))));
472
        $this->assertEquals('"[[Int]]"', json_encode(Type::listOf(Type::listOf(Type::int()))));
473
    }
474
475
    /**
476
     * @it identifies input types
477
     */
478
    public function testIdentifiesInputTypes()
479
    {
480
        $expected = [
481
            [Type::int(), true],
482
            [$this->objectType, false],
483
            [$this->interfaceType, false],
484
            [$this->unionType, false],
485
            [$this->enumType, true],
486
            [$this->inputObjectType, true]
487
        ];
488
489
        foreach ($expected as $index => $entry) {
490
            $this->assertSame($entry[1], Type::isInputType($entry[0]), "Type {$entry[0]} was detected incorrectly");
491
        }
492
    }
493
494
    /**
495
     * @it identifies output types
496
     */
497
    public function testIdentifiesOutputTypes()
498
    {
499
        $expected = [
500
            [Type::int(), true],
501
            [$this->objectType, true],
502
            [$this->interfaceType, true],
503
            [$this->unionType, true],
504
            [$this->enumType, true],
505
            [$this->inputObjectType, false]
506
        ];
507
508
        foreach ($expected as $index => $entry) {
509
            $this->assertSame($entry[1], Type::isOutputType($entry[0]), "Type {$entry[0]} was detected incorrectly");
510
        }
511
    }
512
513
    /**
514
     * @it prohibits nesting NonNull inside NonNull
515
     */
516
    public function testProhibitsNestingNonNullInsideNonNull()
517
    {
518
        $this->expectException(InvariantViolation::class);
519
        $this->expectExceptionMessage(
520
            'Expected Int! to be a GraphQL nullable type.'
521
        );
522
        Type::nonNull(Type::nonNull(Type::int()));
523
    }
524
525
    /**
526
     * @it allows a thunk for Union member types
527
     */
528
    public function testAllowsThunkForUnionTypes()
529
    {
530
        $union = new UnionType([
531
            'name' => 'ThunkUnion',
532
            'types' => function() {return [$this->objectType]; }
533
        ]);
534
535
        $types = $union->getTypes();
536
        $this->assertEquals(1, count($types));
537
        $this->assertSame($this->objectType, $types[0]);
538
    }
539
540
    public function testAllowsRecursiveDefinitions()
541
    {
542
        // See https://github.com/webonyx/graphql-php/issues/16
543
        $node = new InterfaceType([
544
            'name' => 'Node',
545
            'fields' => [
546
                'id' => ['type' => Type::nonNull(Type::id())]
547
            ]
548
        ]);
549
550
        $blog = null;
551
        $called = false;
552
553
        $user = new ObjectType([
554
            'name' => 'User',
555
            'fields' => function() use (&$blog, &$called) {
556
                $this->assertNotNull($blog, 'Blog type is expected to be defined at this point, but it is null');
557
                $called = true;
558
559
                return [
560
                    'id' => ['type' => Type::nonNull(Type::id())],
561
                    'blogs' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($blog)))]
562
                ];
563
            },
564
            'interfaces' => function() use ($node) {
565
                return [$node];
566
            }
567
        ]);
568
569
        $blog = new ObjectType([
570
            'name' => 'Blog',
571
            'fields' => function() use ($user) {
572
                return [
573
                    'id' => ['type' => Type::nonNull(Type::id())],
574
                    'owner' => ['type' => Type::nonNull($user)]
575
                ];
576
            },
577
            'interfaces' => function() use ($node) {
578
                return [$node];
579
            }
580
        ]);
581
582
        $schema = new Schema([
583
            'query' => new ObjectType([
584
                'name' => 'Query',
585
                'fields' => [
586
                    'node' => ['type' => $node]
587
                ]
588
            ]),
589
            'types' => [$user, $blog]
590
        ]);
591
592
        $this->assertTrue($called);
593
        $schema->getType('Blog');
594
595
        $this->assertEquals([$node], $blog->getInterfaces());
596
        $this->assertEquals([$node], $user->getInterfaces());
597
598
        $this->assertNotNull($user->getField('blogs'));
599
        $this->assertSame($blog, $user->getField('blogs')->getType()->getWrappedType(true));
0 ignored issues
show
Bug introduced by
The method getWrappedType() does not exist on GraphQL\Type\Definition\Type. It seems like you code against a sub-type of GraphQL\Type\Definition\Type such as GraphQL\Type\Definition\ListOfType or GraphQL\Type\Definition\NonNull. ( Ignorable by Annotation )

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

599
        $this->assertSame($blog, $user->getField('blogs')->getType()->/** @scrutinizer ignore-call */ getWrappedType(true));
Loading history...
600
601
        $this->assertNotNull($blog->getField('owner'));
602
        $this->assertSame($user, $blog->getField('owner')->getType()->getWrappedType(true));
603
    }
604
605
    public function testInputObjectTypeAllowsRecursiveDefinitions()
606
    {
607
        $called = false;
608
        $inputObject = new InputObjectType([
609
            'name' => 'InputObject',
610
            'fields' => function() use (&$inputObject, &$called) {
611
                $called = true;
612
                return [
613
                    'value' => ['type' => Type::string()],
614
                    'nested' => ['type' => $inputObject ]
615
                ];
616
            }
617
        ]);
618
        $someMutation = new ObjectType([
619
            'name' => 'SomeMutation',
620
            'fields' => [
621
                'mutateSomething' => [
622
                    'type' => $this->blogArticle,
623
                    'args' => ['input' => ['type' => $inputObject]]
624
                ]
625
            ]
626
        ]);
627
628
        $schema = new Schema([
629
            'query' => $this->blogQuery,
630
            'mutation' => $someMutation
631
        ]);
632
633
        $this->assertSame($inputObject, $schema->getType('InputObject'));
634
        $this->assertTrue($called);
635
        $this->assertEquals(count($inputObject->getFields()), 2);
636
        $this->assertSame($inputObject->getField('nested')->getType(), $inputObject);
637
        $this->assertSame($someMutation->getField('mutateSomething')->getArg('input')->getType(), $inputObject);
638
    }
639
640
    public function testInterfaceTypeAllowsRecursiveDefinitions()
641
    {
642
        $called = false;
643
        $interface = new InterfaceType([
644
            'name' => 'SomeInterface',
645
            'fields' => function() use (&$interface, &$called) {
646
                $called = true;
647
                return [
648
                    'value' => ['type' => Type::string()],
649
                    'nested' => ['type' => $interface ]
650
                ];
651
            }
652
        ]);
653
654
        $query = new ObjectType([
655
            'name' => 'Query',
656
            'fields' => [
657
                'test' => ['type' => $interface]
658
            ]
659
        ]);
660
661
        $schema = new Schema([
662
            'query' => $query
663
        ]);
664
665
        $this->assertSame($interface, $schema->getType('SomeInterface'));
666
        $this->assertTrue($called);
667
        $this->assertEquals(count($interface->getFields()), 2);
668
        $this->assertSame($interface->getField('nested')->getType(), $interface);
669
        $this->assertSame($interface->getField('value')->getType(), Type::string());
670
    }
671
672
    public function testAllowsShorthandFieldDefinition()
673
    {
674
        $interface = new InterfaceType([
675
            'name' => 'SomeInterface',
676
            'fields' => function() use (&$interface) {
677
                return [
678
                    'value' => Type::string(),
679
                    'nested' => $interface,
680
                    'withArg' => [
681
                        'type' => Type::string(),
682
                        'args' => [
683
                            'arg1' => Type::int()
684
                        ]
685
                    ]
686
                ];
687
            }
688
        ]);
689
690
        $query = new ObjectType([
691
            'name' => 'Query',
692
            'fields' => [
693
                'test' => $interface
694
            ]
695
        ]);
696
697
        $schema = new Schema([
698
            'query' => $query
699
        ]);
700
701
        $valueField = $schema->getType('SomeInterface')->getField('value');
0 ignored issues
show
Bug introduced by
The method getField() does not exist on GraphQL\Type\Definition\Type. It seems like you code against a sub-type of GraphQL\Type\Definition\Type such as GraphQL\Type\Definition\InterfaceType or GraphQL\Type\Definition\ObjectType or GraphQL\Type\Definition\InputObjectType. ( Ignorable by Annotation )

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

701
        $valueField = $schema->getType('SomeInterface')->/** @scrutinizer ignore-call */ getField('value');
Loading history...
702
        $nestedField = $schema->getType('SomeInterface')->getField('nested');
703
704
        $this->assertEquals(Type::string(), $valueField->getType());
705
        $this->assertEquals($interface, $nestedField->getType());
706
707
        $withArg = $schema->getType('SomeInterface')->getField('withArg');
708
        $this->assertEquals(Type::string(), $withArg->getType());
709
710
        $this->assertEquals('arg1', $withArg->args[0]->name);
711
        $this->assertEquals(Type::int(), $withArg->args[0]->getType());
712
713
        $testField = $schema->getType('Query')->getField('test');
714
        $this->assertEquals($interface, $testField->getType());
715
        $this->assertEquals('test', $testField->name);
716
    }
717
718
    public function testInfersNameFromClassname()
719
    {
720
        $myObj = new MyCustomType();
721
        $this->assertEquals('MyCustom', $myObj->name);
722
723
        $otherCustom = new OtherCustom();
724
        $this->assertEquals('OtherCustom', $otherCustom->name);
725
    }
726
727
    public function testAllowsOverridingInternalTypes()
728
    {
729
        $idType = new CustomScalarType([
730
            'name' => 'ID',
731
            'serialize' => function() {},
732
            'parseValue' => function() {},
733
            'parseLiteral' => function() {}
734
        ]);
735
736
        $schema = new Schema([
737
            'query' => new ObjectType(['name' => 'Query', 'fields' => []]),
738
            'types' => [$idType]
739
        ]);
740
741
        $this->assertSame($idType, $schema->getType('ID'));
742
    }
743
744
    // Field config must be object
745
746
    /**
747
     * @it accepts an Object type with a field function
748
     */
749
    public function testAcceptsAnObjectTypeWithAFieldFunction()
750
    {
751
        $objType = new ObjectType([
752
            'name' => 'SomeObject',
753
            'fields' => function () {
754
                return [
755
                    'f' => ['type' => Type::string()],
756
                ];
757
            },
758
        ]);
759
        $objType->assertValid(true);
0 ignored issues
show
Unused Code introduced by
The call to GraphQL\Type\Definition\ObjectType::assertValid() has too many arguments starting with true. ( Ignorable by Annotation )

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

759
        $objType->/** @scrutinizer ignore-call */ 
760
                  assertValid(true);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
760
        $this->assertSame(Type::string(), $objType->getField('f')->getType());
761
    }
762
763
    /**
764
     * @it rejects an Object type field with undefined config
765
     */
766
    public function testRejectsAnObjectTypeFieldWithUndefinedConfig()
767
    {
768
        $objType = new ObjectType([
769
            'name' => 'SomeObject',
770
            'fields' => [
771
                'f' => null,
772
            ],
773
        ]);
774
        $this->expectException(InvariantViolation::class);
775
        $this->expectExceptionMessage(
776
            'SomeObject.f field config must be an array, but got: null'
777
        );
778
        $objType->getFields();
779
    }
780
781
    /**
782
     * @it rejects an Object type with incorrectly typed fields
783
     */
784
    public function testRejectsAnObjectTypeWithIncorrectlyTypedFields()
785
    {
786
        $objType = new ObjectType([
787
            'name' => 'SomeObject',
788
            'fields' => [['field' => Type::string()]],
789
        ]);
790
        $this->expectException(InvariantViolation::class);
791
        $this->expectExceptionMessage(
792
            'SomeObject fields must be an associative array with field names as keys or a ' .
793
            'function which returns such an array.'
794
        );
795
        $objType->getFields();
796
    }
797
798
    /**
799
     * @it rejects an Object type with a field function that returns incorrect type
800
     */
801
    public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectType()
802
    {
803
        $objType = new ObjectType([
804
            'name' => 'SomeObject',
805
            'fields' => function () {
806
                return [['field' => Type::string()]];
807
            },
808
        ]);
809
        $this->expectException(InvariantViolation::class);
810
        $this->expectExceptionMessage(
811
            'SomeObject fields must be an associative array with field names as keys or a ' .
812
            'function which returns such an array.'
813
        );
814
        $objType->getFields();
815
    }
816
817
    // Field arg config must be object
818
819
    /**
820
     * @it accepts an Object type with field args
821
     */
822
    public function testAcceptsAnObjectTypeWithFieldArgs()
823
    {
824
        $this->expectNotToPerformAssertions();
825
        $objType = new ObjectType([
826
            'name' => 'SomeObject',
827
            'fields' => [
828
                'goodField' => [
829
                    'type' => Type::string(),
830
                    'args' => [
831
                        'goodArg' => ['type' => Type::string()],
832
                    ],
833
                ],
834
            ],
835
        ]);
836
        // Should not throw:
837
        $objType->assertValid();
838
    }
839
840
    // rejects an Object type with incorrectly typed field args
841
842
    /**
843
     * @it does not allow isDeprecated without deprecationReason on field
844
     */
845
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField()
846
    {
847
        $OldObject = new ObjectType([
848
            'name' => 'OldObject',
849
            'fields' => [
850
                'field' => [
851
                    'type' => Type::string(),
852
                    'isDeprecated' => true,
853
                ],
854
            ],
855
        ]);
856
857
        $this->expectException(InvariantViolation::class);
858
        $this->expectExceptionMessage(
859
            'OldObject.field should provide "deprecationReason" instead of "isDeprecated".'
860
        );
861
        $OldObject->assertValid();
862
    }
863
864
    // Object interfaces must be array
865
866
    /**
867
     * @it accepts an Object type with array interfaces
868
     */
869
    public function testAcceptsAnObjectTypeWithArrayInterfaces()
870
    {
871
        $objType = new ObjectType([
872
            'name' => 'SomeObject',
873
            'interfaces' => [$this->interfaceType],
874
            'fields' => ['f' => ['type' => Type::string()]],
875
        ]);
876
        $this->assertSame($this->interfaceType, $objType->getInterfaces()[0]);
877
    }
878
879
    /**
880
     * @it accepts an Object type with interfaces as a function returning an array
881
     */
882
    public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray()
883
    {
884
        $objType = new ObjectType([
885
            'name' => 'SomeObject',
886
            'interfaces' => function () {
887
                return [$this->interfaceType];
888
            },
889
            'fields' => ['f' => ['type' => Type::string()]],
890
        ]);
891
        $this->assertSame($this->interfaceType, $objType->getInterfaces()[0]);
892
    }
893
894
    /**
895
     * @it rejects an Object type with incorrectly typed interfaces
896
     */
897
    public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces()
898
    {
899
        $objType = new ObjectType([
900
            'name' => 'SomeObject',
901
            'interfaces' => new \stdClass(),
902
            'fields' => ['f' => ['type' => Type::string()]],
903
        ]);
904
        $this->expectException(InvariantViolation::class);
905
        $this->expectExceptionMessage(
906
            'SomeObject interfaces must be an Array or a callable which returns an Array.'
907
        );
908
        $objType->getInterfaces();
909
    }
910
911
    /**
912
     * @it rejects an Object type with interfaces as a function returning an incorrect type
913
     */
914
    public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType()
915
    {
916
        $objType = new ObjectType([
917
            'name' => 'SomeObject',
918
            'interfaces' => function () {
919
                return new \stdClass();
920
            },
921
            'fields' => ['f' => ['type' => Type::string()]],
922
        ]);
923
        $this->expectException(InvariantViolation::class);
924
        $this->expectExceptionMessage(
925
            'SomeObject interfaces must be an Array or a callable which returns an Array.'
926
        );
927
        $objType->getInterfaces();
928
    }
929
930
    // Type System: Object fields must have valid resolve values
931
932
    private function schemaWithObjectWithFieldResolver($resolveValue)
933
    {
934
        $BadResolverType = new ObjectType([
935
            'name' => 'BadResolver',
936
            'fields' => [
937
                'badField' => [
938
                    'type' => Type::string(),
939
                    'resolve' => $resolveValue,
940
                ],
941
            ],
942
        ]);
943
944
        $schema = new Schema([
945
            'query' => new ObjectType([
946
                'name' => 'Query',
947
                'fields' => [
948
                    'f' => ['type' => $BadResolverType],
949
                ],
950
            ]),
951
        ]);
952
        $schema->assertValid();
953
        return $schema;
954
    }
955
956
    /**
957
     * @it accepts a lambda as an Object field resolver
958
     */
959
    public function testAcceptsALambdaAsAnObjectFieldResolver()
960
    {
961
        $this->expectNotToPerformAssertions();
962
        // should not throw:
963
        $this->schemaWithObjectWithFieldResolver(function () {});
964
    }
965
966
    /**
967
     * @it rejects an empty Object field resolver
968
     */
969
    public function testRejectsAnEmptyObjectFieldResolver()
970
    {
971
        $this->expectException(InvariantViolation::class);
972
        $this->expectExceptionMessage(
973
            'BadResolver.badField field resolver must be a function if provided, but got: []'
974
        );
975
        $this->schemaWithObjectWithFieldResolver([]);
976
    }
977
978
    /**
979
     * @it rejects a constant scalar value resolver
980
     */
981
    public function testRejectsAConstantScalarValueResolver()
982
    {
983
        $this->expectException(InvariantViolation::class);
984
        $this->expectExceptionMessage(
985
            'BadResolver.badField field resolver must be a function if provided, but got: 0'
986
        );
987
        $this->schemaWithObjectWithFieldResolver(0);
988
    }
989
990
991
    // Type System: Interface types must be resolvable
992
993
    private function schemaWithFieldType($type)
994
    {
995
        $schema = new Schema([
996
            'query' => new ObjectType([
997
                'name' => 'Query',
998
                'fields' => ['field' => ['type' => $type]],
999
            ]),
1000
            'types' => [$type],
1001
        ]);
1002
        $schema->assertValid();
1003
        return $schema;
1004
    }
1005
    /**
1006
     * @it accepts an Interface type defining resolveType
1007
     */
1008
    public function testAcceptsAnInterfaceTypeDefiningResolveType()
1009
    {
1010
        $this->expectNotToPerformAssertions();
1011
        $AnotherInterfaceType = new InterfaceType([
1012
            'name' => 'AnotherInterface',
1013
            'fields' => ['f' => ['type' => Type::string()]],
1014
        ]);
1015
1016
        // Should not throw:
1017
        $this->schemaWithFieldType(
1018
            new ObjectType([
1019
                'name' => 'SomeObject',
1020
                'interfaces' => [$AnotherInterfaceType],
1021
                'fields' => ['f' => ['type' => Type::string()]],
1022
            ])
1023
        );
1024
    }
1025
1026
    /**
1027
     * @it accepts an Interface with implementing type defining isTypeOf
1028
     */
1029
    public function testAcceptsAnInterfaceWithImplementingTypeDefiningIsTypeOf()
1030
    {
1031
        $this->expectNotToPerformAssertions();
1032
        $InterfaceTypeWithoutResolveType = new InterfaceType([
1033
            'name' => 'InterfaceTypeWithoutResolveType',
1034
            'fields' => ['f' => ['type' => Type::string()]],
1035
        ]);
1036
1037
        // Should not throw:
1038
        $this->schemaWithFieldType(
1039
            new ObjectType([
1040
                'name' => 'SomeObject',
1041
                'interfaces' => [$InterfaceTypeWithoutResolveType],
1042
                'fields' => ['f' => ['type' => Type::string()]],
1043
            ])
1044
        );
1045
    }
1046
1047
    /**
1048
     * @it accepts an Interface type defining resolveType with implementing type defining isTypeOf
1049
     */
1050
    public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf()
1051
    {
1052
        $this->expectNotToPerformAssertions();
1053
        $AnotherInterfaceType = new InterfaceType([
1054
            'name' => 'AnotherInterface',
1055
            'fields' => ['f' => ['type' => Type::string()]],
1056
        ]);
1057
1058
        // Should not throw:
1059
        $this->schemaWithFieldType(
1060
            new ObjectType([
1061
                'name' => 'SomeObject',
1062
                'interfaces' => [$AnotherInterfaceType],
1063
                'fields' => ['f' => ['type' => Type::string()]],
1064
            ])
1065
        );
1066
    }
1067
1068
    /**
1069
     * @it rejects an Interface type with an incorrect type for resolveType
1070
     */
1071
    public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType()
1072
    {
1073
        $this->expectException(InvariantViolation::class);
1074
        $this->expectExceptionMessage(
1075
            'AnotherInterface must provide "resolveType" as a function, but got: instance of stdClass'
1076
        );
1077
1078
        $type = new InterfaceType([
1079
            'name' => 'AnotherInterface',
1080
            'resolveType' => new \stdClass(),
1081
            'fields' => ['f' => ['type' => Type::string()]],
1082
        ]);
1083
        $type->assertValid();
1084
    }
1085
1086
    // Type System: Union types must be resolvable
1087
1088
    private function ObjectWithIsTypeOf()
0 ignored issues
show
Unused Code introduced by
The method ObjectWithIsTypeOf() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
1089
    {
1090
        return new ObjectType([
1091
            'name' => 'ObjectWithIsTypeOf',
1092
            'fields' => ['f' => ['type' => Type::string()]],
1093
        ]);
1094
    }
1095
1096
    /**
1097
     * @it accepts a Union type defining resolveType
1098
     */
1099
    public function testAcceptsAUnionTypeDefiningResolveType()
1100
    {
1101
        $this->expectNotToPerformAssertions();
1102
        // Should not throw:
1103
        $this->schemaWithFieldType(
1104
            new UnionType([
1105
                'name' => 'SomeUnion',
1106
                'types' => [$this->objectType],
1107
            ])
1108
        );
1109
    }
1110
1111
    /**
1112
     * @it accepts a Union of Object types defining isTypeOf
1113
     */
1114
    public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf()
1115
    {
1116
        $this->expectNotToPerformAssertions();
1117
        // Should not throw:
1118
        $this->schemaWithFieldType(
1119
            new UnionType([
1120
                'name' => 'SomeUnion',
1121
                'types' => [$this->objectWithIsTypeOf],
1122
            ])
1123
        );
1124
    }
1125
1126
    /**
1127
     * @it accepts a Union type defining resolveType of Object types defining isTypeOf
1128
     */
1129
    public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf()
1130
    {
1131
        $this->expectNotToPerformAssertions();
1132
        // Should not throw:
1133
        $this->schemaWithFieldType(
1134
            new UnionType([
1135
                'name' => 'SomeUnion',
1136
                'types' => [$this->objectWithIsTypeOf],
1137
            ])
1138
        );
1139
    }
1140
1141
    /**
1142
     * @it rejects an Union type with an incorrect type for resolveType
1143
     */
1144
    public function testRejectsAnUnionTypeWithAnIncorrectTypeForResolveType()
1145
    {
1146
        $this->expectException(InvariantViolation::class);
1147
        $this->expectExceptionMessage(
1148
            'SomeUnion must provide "resolveType" as a function, but got: instance of stdClass'
1149
        );
1150
        $this->schemaWithFieldType(
1151
            new UnionType([
1152
                'name' => 'SomeUnion',
1153
                'resolveType' => new \stdClass(),
1154
                'types' => [$this->objectWithIsTypeOf],
1155
            ])
1156
        );
1157
    }
1158
1159
    // Type System: Scalar types must be serializable
1160
1161
    /**
1162
     * @it accepts a Scalar type defining serialize
1163
     */
1164
    public function testAcceptsAScalarTypeDefiningSerialize()
1165
    {
1166
        $this->expectNotToPerformAssertions();
1167
        // Should not throw
1168
        $this->schemaWithFieldType(
1169
            new CustomScalarType([
1170
                'name' => 'SomeScalar',
1171
                'serialize' => function () {
1172
                    return null;
1173
                },
1174
            ])
1175
        );
1176
    }
1177
1178
    /**
1179
     * @it rejects a Scalar type not defining serialize
1180
     */
1181
    public function testRejectsAScalarTypeNotDefiningSerialize()
1182
    {
1183
        $this->expectException(InvariantViolation::class);
1184
        $this->expectExceptionMessage(
1185
            'SomeScalar must provide "serialize" function. If this custom Scalar ' .
1186
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
1187
            'functions are also provided.'
1188
        );
1189
        $this->schemaWithFieldType(
1190
            new CustomScalarType([
1191
                'name' => 'SomeScalar',
1192
            ])
1193
        );
1194
    }
1195
1196
    /**
1197
     * @it rejects a Scalar type defining serialize with an incorrect type
1198
     */
1199
    public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType()
1200
    {
1201
        $this->expectException(InvariantViolation::class);
1202
        $this->expectExceptionMessage(
1203
            'SomeScalar must provide "serialize" function. If this custom Scalar ' .
1204
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
1205
            'functions are also provided.'
1206
        );
1207
        $this->schemaWithFieldType(
1208
            new CustomScalarType([
1209
                'name' => 'SomeScalar',
1210
                'serialize' => new \stdClass(),
1211
            ])
1212
        );
1213
    }
1214
1215
    /**
1216
     * @it accepts a Scalar type defining parseValue and parseLiteral
1217
     */
1218
    public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral()
1219
    {
1220
        $this->expectNotToPerformAssertions();
1221
        // Should not throw:
1222
        $this->schemaWithFieldType(
1223
            new CustomScalarType([
1224
                'name' => 'SomeScalar',
1225
                'serialize' => function () {
1226
                },
1227
                'parseValue' => function () {
1228
                },
1229
                'parseLiteral' => function () {
1230
                },
1231
            ])
1232
        );
1233
    }
1234
1235
    /**
1236
     * @it rejects a Scalar type defining parseValue but not parseLiteral
1237
     */
1238
    public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral()
1239
    {
1240
        $this->expectException(InvariantViolation::class);
1241
        $this->expectExceptionMessage(
1242
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1243
        );
1244
        $this->schemaWithFieldType(
1245
            new CustomScalarType([
1246
                'name' => 'SomeScalar',
1247
                'serialize' => function () {
1248
                },
1249
                'parseValue' => function () {
1250
                },
1251
            ])
1252
        );
1253
    }
1254
1255
    /**
1256
     * @it rejects a Scalar type defining parseLiteral but not parseValue
1257
     */
1258
    public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue()
1259
    {
1260
        $this->expectException(InvariantViolation::class);
1261
        $this->expectExceptionMessage(
1262
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1263
        );
1264
        $this->schemaWithFieldType(
1265
            new CustomScalarType([
1266
                'name' => 'SomeScalar',
1267
                'serialize' => function () {
1268
                },
1269
                'parseLiteral' => function () {
1270
                },
1271
            ])
1272
        );
1273
    }
1274
1275
    /**
1276
     * @it rejects a Scalar type defining parseValue and parseLiteral with an incorrect type
1277
     */
1278
    public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType()
1279
    {
1280
        $this->expectException(InvariantViolation::class);
1281
        $this->expectExceptionMessage(
1282
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1283
        );
1284
        $this->schemaWithFieldType(
1285
            new CustomScalarType([
1286
                'name' => 'SomeScalar',
1287
                'serialize' => function () {
1288
                },
1289
                'parseValue' => new \stdClass(),
1290
                'parseLiteral' => new \stdClass(),
1291
            ])
1292
        );
1293
    }
1294
1295
    // Type System: Object types must be assertable
1296
1297
    /**
1298
     * @it accepts an Object type with an isTypeOf function
1299
     */
1300
    public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction()
1301
    {
1302
        $this->expectNotToPerformAssertions();
1303
        // Should not throw
1304
        $this->schemaWithFieldType(
1305
            new ObjectType([
1306
                'name' => 'AnotherObject',
1307
                'fields' => ['f' => ['type' => Type::string()]],
1308
            ])
1309
        );
1310
    }
1311
1312
    /**
1313
     * @it rejects an Object type with an incorrect type for isTypeOf
1314
     */
1315
    public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf()
1316
    {
1317
        $this->expectException(InvariantViolation::class);
1318
        $this->expectExceptionMessage(
1319
            'AnotherObject must provide "isTypeOf" as a function, but got: instance of stdClass'
1320
        );
1321
        $this->schemaWithFieldType(
1322
            new ObjectType([
1323
                'name' => 'AnotherObject',
1324
                'isTypeOf' => new \stdClass(),
1325
                'fields' => ['f' => ['type' => Type::string()]],
1326
            ])
1327
        );
1328
    }
1329
1330
    // Type System: Union types must be array
1331
1332
    /**
1333
     * @it accepts a Union type with array types
1334
     */
1335
    public function testAcceptsAUnionTypeWithArrayTypes()
1336
    {
1337
        $this->expectNotToPerformAssertions();
1338
        // Should not throw:
1339
        $this->schemaWithFieldType(
1340
            new UnionType([
1341
                'name' => 'SomeUnion',
1342
                'types' => [$this->objectType],
1343
            ])
1344
        );
1345
    }
1346
1347
    /**
1348
     * @it accepts a Union type with function returning an array of types
1349
     */
1350
    public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes()
1351
    {
1352
        $this->expectNotToPerformAssertions();
1353
        $this->schemaWithFieldType(
1354
            new UnionType([
1355
                'name' => 'SomeUnion',
1356
                'types' => function () {
1357
                    return [$this->objectType];
1358
                },
1359
            ])
1360
        );
1361
    }
1362
1363
    /**
1364
     * @it rejects a Union type without types
1365
     */
1366
    public function testRejectsAUnionTypeWithoutTypes()
1367
    {
1368
        $this->expectException(InvariantViolation::class);
1369
        $this->expectExceptionMessage(
1370
            'Must provide Array of types or a callable which returns such an array for Union SomeUnion'
1371
        );
1372
        $this->schemaWithFieldType(
1373
            new UnionType([
1374
                'name' => 'SomeUnion',
1375
            ])
1376
        );
1377
    }
1378
1379
    /**
1380
     * @it rejects a Union type with incorrectly typed types
1381
     */
1382
    public function testRejectsAUnionTypeWithIncorrectlyTypedTypes()
1383
    {
1384
        $this->expectException(InvariantViolation::class);
1385
        $this->expectExceptionMessage(
1386
            'Must provide Array of types or a callable which returns such an array for Union SomeUnion'
1387
        );
1388
        $this->schemaWithFieldType(
1389
            new UnionType([
1390
                'name' => 'SomeUnion',
1391
                'types' => (object)[ 'test' => $this->objectType, ],
1392
            ])
1393
        );
1394
    }
1395
1396
    // Type System: Input Objects must have fields
1397
1398
    /**
1399
     * @it accepts an Input Object type with fields
1400
     */
1401
    public function testAcceptsAnInputObjectTypeWithFields()
1402
    {
1403
        $inputObjType = new InputObjectType([
1404
            'name' => 'SomeInputObject',
1405
            'fields' => [
1406
                'f' => ['type' => Type::string()],
1407
            ],
1408
        ]);
1409
        $inputObjType->assertValid();
1410
        $this->assertSame(Type::string(), $inputObjType->getField('f')->getType());
1411
    }
1412
1413
    /**
1414
     * @it accepts an Input Object type with a field function
1415
     */
1416
    public function testAcceptsAnInputObjectTypeWithAFieldFunction()
1417
    {
1418
        $inputObjType = new InputObjectType([
1419
            'name' => 'SomeInputObject',
1420
            'fields' => function () {
1421
                return [
1422
                    'f' => ['type' => Type::string()],
1423
                ];
1424
            },
1425
        ]);
1426
        $inputObjType->assertValid();
1427
        $this->assertSame(Type::string(), $inputObjType->getField('f')->getType());
1428
    }
1429
1430
    /**
1431
     * @it rejects an Input Object type with incorrect fields
1432
     */
1433
    public function testRejectsAnInputObjectTypeWithIncorrectFields()
1434
    {
1435
        $inputObjType = new InputObjectType([
1436
            'name' => 'SomeInputObject',
1437
            'fields' => [],
1438
        ]);
1439
        $this->expectException(InvariantViolation::class);
1440
        $this->expectExceptionMessage(
1441
            'SomeInputObject fields must be an associative array with field names as keys or a callable '.
1442
            'which returns such an array.'
1443
        );
1444
        $inputObjType->assertValid();
1445
    }
1446
1447
    /**
1448
     * @it rejects an Input Object type with fields function that returns incorrect type
1449
     */
1450
    public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorrectType()
1451
    {
1452
        $inputObjType = new InputObjectType([
1453
            'name' => 'SomeInputObject',
1454
            'fields' => function () {
1455
                return [];
1456
            },
1457
        ]);
1458
        $this->expectException(InvariantViolation::class);
1459
        $this->expectExceptionMessage(
1460
            'SomeInputObject fields must be an associative array with field names as keys or a ' .
1461
            'callable which returns such an array.'
1462
        );
1463
        $inputObjType->assertValid();
1464
    }
1465
1466
    // Type System: Input Object fields must not have resolvers
1467
1468
    /**
1469
     * @it rejects an Input Object type with resolvers
1470
     */
1471
    public function testRejectsAnInputObjectTypeWithResolvers()
1472
    {
1473
        $inputObjType = new InputObjectType([
1474
            'name' => 'SomeInputObject',
1475
            'fields' => [
1476
                'f' => [
1477
                    'type' => Type::string(),
1478
                    'resolve' => function () {
1479
                        return 0;
1480
                    },
1481
                ],
1482
            ],
1483
        ]);
1484
        $this->expectException(InvariantViolation::class);
1485
        $this->expectExceptionMessage(
1486
            'SomeInputObject.f field type has a resolve property, ' .
1487
            'but Input Types cannot define resolvers.'
1488
        );
1489
        $inputObjType->assertValid();
1490
    }
1491
1492
    /**
1493
     * @it rejects an Input Object type with resolver constant
1494
     */
1495
    public function testRejectsAnInputObjectTypeWithResolverConstant()
1496
    {
1497
        $inputObjType = new InputObjectType([
1498
            'name' => 'SomeInputObject',
1499
            'fields' => [
1500
                'f' => [
1501
                    'type' => Type::string(),
1502
                    'resolve' => new \stdClass(),
1503
                ],
1504
            ],
1505
        ]);
1506
        $this->expectException(InvariantViolation::class);
1507
        $this->expectExceptionMessage(
1508
            'SomeInputObject.f field type has a resolve property, ' .
1509
            'but Input Types cannot define resolvers.'
1510
        );
1511
        $inputObjType->assertValid();
1512
    }
1513
1514
    // Type System: Enum types must be well defined
1515
1516
    /**
1517
     * @it accepts a well defined Enum type with empty value definition
1518
     */
1519
    public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition()
1520
    {
1521
        $enumType = new EnumType([
1522
            'name' => 'SomeEnum',
1523
            'values' => [
1524
                'FOO' => [],
1525
                'BAR' => [],
1526
            ],
1527
        ]);
1528
        $this->assertEquals('FOO', $enumType->getValue('FOO')->value);
1529
        $this->assertEquals('BAR', $enumType->getValue('BAR')->value);
1530
    }
1531
1532
    /**
1533
     * @it accepts a well defined Enum type with internal value definition
1534
     */
1535
    public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition()
1536
    {
1537
        $enumType = new EnumType([
1538
            'name' => 'SomeEnum',
1539
            'values' => [
1540
                'FOO' => ['value' => 10],
1541
                'BAR' => ['value' => 20],
1542
            ],
1543
        ]);
1544
        $this->assertEquals(10, $enumType->getValue('FOO')->value);
1545
        $this->assertEquals(20, $enumType->getValue('BAR')->value);
1546
    }
1547
1548
    /**
1549
     * @it rejects an Enum type with incorrectly typed values
1550
     */
1551
    public function testRejectsAnEnumTypeWithIncorrectlyTypedValues()
1552
    {
1553
        $enumType = new EnumType([
1554
            'name' => 'SomeEnum',
1555
            'values' => [['FOO' => 10]],
1556
        ]);
1557
        $this->expectException(InvariantViolation::class);
1558
        $this->expectExceptionMessage(
1559
            'SomeEnum values must be an array with value names as keys.'
1560
        );
1561
        $enumType->assertValid();
1562
    }
1563
1564
    /**
1565
     * @it does not allow isDeprecated without deprecationReason on enum
1566
     */
1567
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum()
1568
    {
1569
        $enumType = new EnumType([
1570
            'name' => 'SomeEnum',
1571
            'values' => [
1572
                'FOO' => [
1573
                    'isDeprecated' => true,
1574
                ],
1575
            ],
1576
        ]);
1577
        $this->expectException(InvariantViolation::class);
1578
        $this->expectExceptionMessage(
1579
            'SomeEnum.FOO should provide "deprecationReason" instead ' .
1580
            'of "isDeprecated".'
1581
        );
1582
        $enumType->assertValid();
1583
    }
1584
1585
    // Type System: List must accept only types
1586
1587
    public function testListMustAcceptOnlyTypes()
1588
    {
1589
        $types = [
1590
            Type::string(),
1591
            $this->scalarType,
1592
            $this->objectType,
1593
            $this->unionType,
1594
            $this->interfaceType,
1595
            $this->enumType,
1596
            $this->inputObjectType,
1597
            Type::listOf(Type::string()),
1598
            Type::nonNull(Type::string()),
1599
        ];
1600
1601
        $badTypes = [[], new \stdClass(), '', null];
1602
1603
        foreach ($types as $type) {
1604
            try {
1605
                Type::listOf($type);
1606
            } catch (\Throwable $e) {
1607
                $this->fail("List is expected to accept type: " . get_class($type) . ", but got error: ". $e->getMessage());
1608
            }
1609
        }
1610
        foreach ($badTypes as $badType) {
1611
            $typeStr = Utils::printSafe($badType);
1612
            try {
1613
                Type::listOf($badType);
1614
                $this->fail("List should not accept $typeStr");
1615
            } catch (InvariantViolation $e) {
1616
                $this->assertEquals("Expected $typeStr to be a GraphQL type.", $e->getMessage());
1617
            }
1618
        }
1619
    }
1620
1621
    // Type System: NonNull must only accept non-nullable types
1622
1623
    public function testNonNullMustOnlyAcceptNonNullableTypes()
1624
    {
1625
        $nullableTypes = [
1626
            Type::string(),
1627
            $this->scalarType,
1628
            $this->objectType,
1629
            $this->unionType,
1630
            $this->interfaceType,
1631
            $this->enumType,
1632
            $this->inputObjectType,
1633
            Type::listOf(Type::string()),
1634
            Type::listOf(Type::nonNull(Type::string())),
1635
        ];
1636
        $notNullableTypes = [
1637
            Type::nonNull(Type::string()),
1638
            [],
1639
            new \stdClass(),
1640
            '',
1641
            null,
1642
        ];
1643
        foreach ($nullableTypes as $type) {
1644
            try {
1645
                Type::nonNull($type);
1646
            } catch (\Throwable $e) {
1647
                $this->fail("NonNull is expected to accept type: " . get_class($type) . ", but got error: ". $e->getMessage());
1648
            }
1649
        }
1650
        foreach ($notNullableTypes as $badType) {
1651
            $typeStr = Utils::printSafe($badType);
1652
            try {
1653
                Type::nonNull($badType);
1654
                $this->fail("Nulls should not accept $typeStr");
1655
            } catch (InvariantViolation $e) {
1656
                $this->assertEquals("Expected $typeStr to be a GraphQL nullable type.", $e->getMessage());
1657
            }
1658
        }
1659
    }
1660
1661
    // Type System: A Schema must contain uniquely named types
1662
1663
    /**
1664
     * @it rejects a Schema which redefines a built-in type
1665
     */
1666
    public function testRejectsASchemaWhichRedefinesABuiltInType()
1667
    {
1668
        $FakeString = new CustomScalarType([
1669
            'name' => 'String',
1670
            'serialize' => function () {
1671
            },
1672
        ]);
1673
1674
        $QueryType = new ObjectType([
1675
            'name' => 'Query',
1676
            'fields' => [
1677
                'normal' => ['type' => Type::string()],
1678
                'fake' => ['type' => $FakeString],
1679
            ],
1680
        ]);
1681
1682
        $this->expectException(InvariantViolation::class);
1683
        $this->expectExceptionMessage(
1684
            'Schema must contain unique named types but contains multiple types named "String" '.
1685
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1686
        );
1687
        $schema = new Schema(['query' => $QueryType]);
1688
        $schema->assertValid();
1689
    }
1690
1691
    /**
1692
     * @it rejects a Schema which defines an object type twice
1693
     */
1694
    public function testRejectsASchemaWhichDefinesAnObjectTypeTwice()
1695
    {
1696
        $A = new ObjectType([
1697
            'name' => 'SameName',
1698
            'fields' => ['f' => ['type' => Type::string()]],
1699
        ]);
1700
1701
        $B = new ObjectType([
1702
            'name' => 'SameName',
1703
            'fields' => ['f' => ['type' => Type::string()]],
1704
        ]);
1705
1706
        $QueryType = new ObjectType([
1707
            'name' => 'Query',
1708
            'fields' => [
1709
                'a' => ['type' => $A],
1710
                'b' => ['type' => $B],
1711
            ],
1712
        ]);
1713
        $this->expectException(InvariantViolation::class);
1714
        $this->expectExceptionMessage(
1715
            'Schema must contain unique named types but contains multiple types named "SameName" ' .
1716
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1717
        );
1718
        $schema = new Schema([ 'query' => $QueryType ]);
1719
        $schema->assertValid();
1720
    }
1721
1722
    /**
1723
     * @it rejects a Schema which have same named objects implementing an interface
1724
     */
1725
    public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface()
1726
    {
1727
        $AnotherInterface = new InterfaceType([
1728
            'name' => 'AnotherInterface',
1729
            'fields' => ['f' => ['type' => Type::string()]],
1730
        ]);
1731
1732
        $FirstBadObject = new ObjectType([
1733
            'name' => 'BadObject',
1734
            'interfaces' => [$AnotherInterface],
1735
            'fields' => ['f' => ['type' => Type::string()]],
1736
        ]);
1737
1738
        $SecondBadObject = new ObjectType([
1739
            'name' => 'BadObject',
1740
            'interfaces' => [$AnotherInterface],
1741
            'fields' => ['f' => ['type' => Type::string()]],
1742
        ]);
1743
1744
        $QueryType = new ObjectType([
1745
            'name' => 'Query',
1746
            'fields' => [
1747
                'iface' => ['type' => $AnotherInterface],
1748
            ],
1749
        ]);
1750
1751
        $this->expectException(InvariantViolation::class);
1752
        $this->expectExceptionMessage(
1753
            'Schema must contain unique named types but contains multiple types named "BadObject" ' .
1754
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1755
        );
1756
        $schema = new Schema([
1757
            'query' => $QueryType,
1758
            'types' => [$FirstBadObject, $SecondBadObject],
1759
        ]);
1760
        $schema->assertValid();
1761
    }
1762
}
1763