Failed Conditions
Push — master ( 975c9f...a4f39b )
by Vladimir
09:30
created

testRejectsObjectsImplementingTheExtendedInterfaceDueToMismatchingInterfaceType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 44
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 8
c 1
b 0
f 1
dl 0
loc 44
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Type;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Error\InvariantViolation;
9
use GraphQL\Error\Warning;
10
use GraphQL\Language\Parser;
11
use GraphQL\Language\SourceLocation;
12
use GraphQL\Type\Definition\CustomScalarType;
13
use GraphQL\Type\Definition\EnumType;
14
use GraphQL\Type\Definition\InputObjectType;
15
use GraphQL\Type\Definition\InterfaceType;
16
use GraphQL\Type\Definition\ObjectType;
17
use GraphQL\Type\Definition\ScalarType;
18
use GraphQL\Type\Definition\Type;
19
use GraphQL\Type\Definition\UnionType;
20
use GraphQL\Type\Schema;
21
use GraphQL\Utils\BuildSchema;
22
use GraphQL\Utils\SchemaExtender;
23
use GraphQL\Utils\Utils;
24
use PHPUnit\Framework\TestCase;
25
use function array_map;
26
use function array_merge;
27
use function implode;
28
use function print_r;
29
use function sprintf;
30
31
class ValidationTest extends TestCase
32
{
33
    /** @var ScalarType */
34
    public $SomeScalarType;
35
36
    /** @var ObjectType */
37
    public $SomeObjectType;
38
39
    /** @var UnionType */
40
    public $SomeUnionType;
41
42
    /** @var InterfaceType */
43
    public $SomeInterfaceType;
44
45
    /** @var EnumType */
46
    public $SomeEnumType;
47
48
    /** @var InputObjectType */
49
    public $SomeInputObjectType;
50
51
    /** @var mixed[] */
52
    public $outputTypes;
53
54
    /** @var mixed[] */
55
    public $notOutputTypes;
56
57
    /** @var mixed[] */
58
    public $inputTypes;
59
60
    /** @var mixed[] */
61
    public $notInputTypes;
62
63
    /** @var float */
64
    public $Number;
65
66
    public function setUp()
67
    {
68
        $this->Number = 1;
69
70
        $this->SomeScalarType = new CustomScalarType([
71
            'name'         => 'SomeScalar',
72
            'serialize'    => static function () {
73
            },
74
            'parseValue'   => static function () {
75
            },
76
            'parseLiteral' => static function () {
77
            },
78
        ]);
79
80
        $this->SomeInterfaceType = new InterfaceType([
81
            'name'   => 'SomeInterface',
82
            'fields' => function () {
83
                return ['f' => ['type' => $this->SomeObjectType]];
84
            },
85
        ]);
86
87
        $this->SomeObjectType = new ObjectType([
88
            'name'       => 'SomeObject',
89
            'fields'     => function () {
90
                return ['f' => ['type' => $this->SomeObjectType]];
91
            },
92
            'interfaces' => function () {
93
                return [$this->SomeInterfaceType];
94
            },
95
        ]);
96
97
        $this->SomeUnionType = new UnionType([
98
            'name'  => 'SomeUnion',
99
            'types' => [$this->SomeObjectType],
100
        ]);
101
102
        $this->SomeEnumType = new EnumType([
103
            'name'   => 'SomeEnum',
104
            'values' => [
105
                'ONLY' => [],
106
            ],
107
        ]);
108
109
        $this->SomeInputObjectType = new InputObjectType([
110
            'name'   => 'SomeInputObject',
111
            'fields' => [
112
                'val' => ['type' => Type::string(), 'defaultValue' => 'hello'],
113
            ],
114
        ]);
115
116
        $this->outputTypes = $this->withModifiers([
117
            Type::string(),
118
            $this->SomeScalarType,
119
            $this->SomeEnumType,
120
            $this->SomeObjectType,
121
            $this->SomeUnionType,
122
            $this->SomeInterfaceType,
123
        ]);
124
125
        $this->notOutputTypes = $this->withModifiers([
126
            $this->SomeInputObjectType,
127
        ]);
128
129
        $this->inputTypes = $this->withModifiers([
130
            Type::string(),
131
            $this->SomeScalarType,
132
            $this->SomeEnumType,
133
            $this->SomeInputObjectType,
134
        ]);
135
136
        $this->notInputTypes = $this->withModifiers([
137
            $this->SomeObjectType,
138
            $this->SomeUnionType,
139
            $this->SomeInterfaceType,
140
        ]);
141
142
        Warning::suppress(Warning::WARNING_NOT_A_TYPE);
143
    }
144
145
    private function withModifiers($types)
146
    {
147
        return array_merge(
148
            $types,
149
            Utils::map(
150
                $types,
151
                static function ($type) {
152
                    return Type::listOf($type);
153
                }
154
            ),
155
            Utils::map(
156
                $types,
157
                static function ($type) {
158
                    return Type::nonNull($type);
159
                }
160
            ),
161
            Utils::map(
162
                $types,
163
                static function ($type) {
164
                    return Type::nonNull(Type::listOf($type));
165
                }
166
            )
167
        );
168
    }
169
170
    public function tearDown()
171
    {
172
        parent::tearDown();
173
        Warning::enable(Warning::WARNING_NOT_A_TYPE);
174
    }
175
176
    public function testRejectsTypesWithoutNames() : void
177
    {
178
        $this->assertEachCallableThrows(
179
            [
180
                static function () {
181
                    return new ObjectType([]);
182
                },
183
                static function () {
184
                    return new EnumType([]);
185
                },
186
                static function () {
187
                    return new InputObjectType([]);
188
                },
189
                static function () {
190
                    return new UnionType([]);
191
                },
192
                static function () {
193
                    return new InterfaceType([]);
194
                },
195
            ],
196
            'Must provide name.'
197
        );
198
    }
199
200
    /**
201
     * DESCRIBE: Type System: A Schema must have Object root types
202
     */
203
    private function assertEachCallableThrows($closures, $expectedError)
204
    {
205
        foreach ($closures as $index => $factory) {
206
            try {
207
                $factory();
208
                self::fail('Expected exception not thrown for entry ' . $index);
209
            } catch (InvariantViolation $e) {
210
                self::assertEquals($expectedError, $e->getMessage(), 'Error in callable #' . $index);
211
            }
212
        }
213
    }
214
215
    /**
216
     * @see it('accepts a Schema whose query type is an object type')
217
     */
218
    public function testAcceptsASchemaWhoseQueryTypeIsAnObjectType() : void
219
    {
220
        $schema = BuildSchema::build('
221
      type Query {
222
        test: String
223
      }
224
        ');
225
        self::assertEquals([], $schema->validate());
226
227
        $schemaWithDef = BuildSchema::build('
228
      schema {
229
        query: QueryRoot
230
      }
231
      type QueryRoot {
232
        test: String
233
      }
234
    ');
235
        self::assertEquals([], $schemaWithDef->validate());
236
    }
237
238
    /**
239
     * @see it('accepts a Schema whose query and mutation types are object types')
240
     */
241
    public function testAcceptsASchemaWhoseQueryAndMutationTypesAreObjectTypes() : void
242
    {
243
        $schema = BuildSchema::build('
244
      type Query {
245
        test: String
246
      }
247
248
      type Mutation {
249
        test: String
250
      }
251
        ');
252
        self::assertEquals([], $schema->validate());
253
254
        $schema = BuildSchema::build('
255
      schema {
256
        query: QueryRoot
257
        mutation: MutationRoot
258
      }
259
260
      type QueryRoot {
261
        test: String
262
      }
263
264
      type MutationRoot {
265
        test: String
266
      }
267
        ');
268
        self::assertEquals([], $schema->validate());
269
    }
270
271
    /**
272
     * @see it('accepts a Schema whose query and subscription types are object types')
273
     */
274
    public function testAcceptsASchemaWhoseQueryAndSubscriptionTypesAreObjectTypes() : void
275
    {
276
        $schema = BuildSchema::build('
277
      type Query {
278
        test: String
279
      }
280
281
      type Subscription {
282
        test: String
283
      }
284
        ');
285
        self::assertEquals([], $schema->validate());
286
287
        $schema = BuildSchema::build('
288
      schema {
289
        query: QueryRoot
290
        subscription: SubscriptionRoot
291
      }
292
293
      type QueryRoot {
294
        test: String
295
      }
296
297
      type SubscriptionRoot {
298
        test: String
299
      }
300
        ');
301
        self::assertEquals([], $schema->validate());
302
    }
303
304
    /**
305
     * @see it('rejects a Schema without a query type')
306
     */
307
    public function testRejectsASchemaWithoutAQueryType() : void
308
    {
309
        $schema = BuildSchema::build('
310
      type Mutation {
311
        test: String
312
      }
313
        ');
314
315
        $this->assertMatchesValidationMessage(
316
            $schema->validate(),
317
            [['message' => 'Query root type must be provided.']]
318
        );
319
320
        $schemaWithDef = BuildSchema::build('
321
      schema {
322
        mutation: MutationRoot
323
      }
324
325
      type MutationRoot {
326
        test: String
327
      }
328
        ');
329
330
        $this->assertMatchesValidationMessage(
331
            $schemaWithDef->validate(),
332
            [[
333
                'message'   => 'Query root type must be provided.',
334
                'locations' => [['line' => 2, 'column' => 7]],
335
            ],
336
            ]
337
        );
338
    }
339
340
    private function formatLocations(Error $error)
341
    {
342
        return Utils::map($error->getLocations(), static function (SourceLocation $loc) {
343
            return ['line' => $loc->line, 'column' => $loc->column];
344
        });
345
    }
346
347
    /**
348
     * @param Error[] $errors
349
     * @param bool    $withLocation
350
     *
351
     * @return mixed[]
352
     */
353
    private function formatErrors(array $errors, $withLocation = true)
354
    {
355
        return Utils::map($errors, function (Error $error) use ($withLocation) {
356
            if (! $withLocation) {
357
                return [ 'message' => $error->getMessage() ];
358
            }
359
360
            return [
361
                'message' => $error->getMessage(),
362
                'locations' => $this->formatLocations($error),
363
            ];
364
        });
365
    }
366
367
    private function assertMatchesValidationMessage($errors, $expected)
368
    {
369
        $expectedWithLocations = [];
370
        foreach ($expected as $index => $err) {
371
            if (! isset($err['locations']) && isset($errors[$index])) {
372
                $expectedWithLocations[$index] = $err + ['locations' => $this->formatLocations($errors[$index])];
373
            } else {
374
                $expectedWithLocations[$index] = $err;
375
            }
376
        }
377
378
        self::assertEquals($expectedWithLocations, $this->formatErrors($errors));
379
    }
380
381
    /**
382
     * @see it('rejects a Schema whose query root type is not an Object type')
383
     */
384
    public function testRejectsASchemaWhoseQueryTypeIsNotAnObjectType() : void
385
    {
386
        $schema = BuildSchema::build('
387
      input Query {
388
        test: String
389
      }
390
        ');
391
392
        $this->assertMatchesValidationMessage(
393
            $schema->validate(),
394
            [[
395
                'message'   => 'Query root type must be Object type, it cannot be Query.',
396
                'locations' => [['line' => 2, 'column' => 7]],
397
            ],
398
            ]
399
        );
400
401
        $schemaWithDef = BuildSchema::build('
402
      schema {
403
        query: SomeInputObject
404
      }
405
406
      input SomeInputObject {
407
        test: String
408
      }
409
        ');
410
411
        $this->assertMatchesValidationMessage(
412
            $schemaWithDef->validate(),
413
            [[
414
                'message'   => 'Query root type must be Object type, it cannot be SomeInputObject.',
415
                'locations' => [['line' => 3, 'column' => 16]],
416
            ],
417
            ]
418
        );
419
    }
420
421
    /**
422
     * @see it('rejects a Schema whose mutation type is an input type')
423
     */
424
    public function testRejectsASchemaWhoseMutationTypeIsAnInputType() : void
425
    {
426
        $schema = BuildSchema::build('
427
      type Query {
428
        field: String
429
      }
430
431
      input Mutation {
432
        test: String
433
      }
434
        ');
435
436
        $this->assertMatchesValidationMessage(
437
            $schema->validate(),
438
            [[
439
                'message'   => 'Mutation root type must be Object type if provided, it cannot be Mutation.',
440
                'locations' => [['line' => 6, 'column' => 7]],
441
            ],
442
            ]
443
        );
444
445
        $schemaWithDef = BuildSchema::build('
446
      schema {
447
        query: Query
448
        mutation: SomeInputObject
449
      }
450
451
      type Query {
452
        field: String
453
      }
454
455
      input SomeInputObject {
456
        test: String
457
      }
458
        ');
459
460
        $this->assertMatchesValidationMessage(
461
            $schemaWithDef->validate(),
462
            [[
463
                'message'   => 'Mutation root type must be Object type if provided, it cannot be SomeInputObject.',
464
                'locations' => [['line' => 4, 'column' => 19]],
465
            ],
466
            ]
467
        );
468
    }
469
470
    // DESCRIBE: Type System: Objects must have fields
471
472
    /**
473
     * @see it('rejects a Schema whose subscription type is an input type')
474
     */
475
    public function testRejectsASchemaWhoseSubscriptionTypeIsAnInputType() : void
476
    {
477
        $schema = BuildSchema::build('
478
      type Query {
479
        field: String
480
      }
481
482
      input Subscription {
483
        test: String
484
      }
485
        ');
486
487
        $this->assertMatchesValidationMessage(
488
            $schema->validate(),
489
            [[
490
                'message'   => 'Subscription root type must be Object type if provided, it cannot be Subscription.',
491
                'locations' => [['line' => 6, 'column' => 7]],
492
            ],
493
            ]
494
        );
495
496
        $schemaWithDef = BuildSchema::build('
497
      schema {
498
        query: Query
499
        subscription: SomeInputObject
500
      }
501
502
      type Query {
503
        field: String
504
      }
505
506
      input SomeInputObject {
507
        test: String
508
      }
509
        ');
510
511
        $this->assertMatchesValidationMessage(
512
            $schemaWithDef->validate(),
513
            [[
514
                'message'   => 'Subscription root type must be Object type if provided, it cannot be SomeInputObject.',
515
                'locations' => [['line' => 4, 'column' => 23]],
516
            ],
517
            ]
518
        );
519
    }
520
521
    /**
522
     * @see it('rejects a schema extended with invalid root types')
523
     */
524
    public function testRejectsASchemaExtendedWithInvalidRootTypes()
525
    {
526
        $schema = BuildSchema::build('
527
            input SomeInputObject {
528
                test: String
529
            }
530
        ');
531
532
        $schema = SchemaExtender::extend(
533
            $schema,
534
            Parser::parse('
535
                extend schema {
536
                  query: SomeInputObject
537
                }
538
            ')
539
        );
540
541
        $schema = SchemaExtender::extend(
542
            $schema,
543
            Parser::parse('
544
                extend schema {
545
                  mutation: SomeInputObject
546
                }
547
            ')
548
        );
549
550
        $schema = SchemaExtender::extend(
551
            $schema,
552
            Parser::parse('
553
                extend schema {
554
                  subscription: SomeInputObject
555
                }
556
            ')
557
        );
558
559
        $expected = [
560
            [
561
                'message' => 'Query root type must be Object type, it cannot be SomeInputObject.',
562
                'locations' => [[ 'line' => 2, 'column' => 13 ]],
563
            ],
564
            [
565
                'message' => 'Mutation root type must be Object type if provided, it cannot be SomeInputObject.',
566
                'locations' => [[ 'line' => 2, 'column' => 13 ]],
567
            ],
568
            [
569
                'message' => 'Subscription root type must be Object type if provided, it cannot be SomeInputObject.',
570
                'locations' => [[ 'line' => 2, 'column' => 13 ]],
571
            ],
572
        ];
573
574
        $this->assertMatchesValidationMessage($schema->validate(), $expected);
575
    }
576
577
    /**
578
     * @see it('rejects a Schema whose directives are incorrectly typed')
579
     */
580
    public function testRejectsASchemaWhoseDirectivesAreIncorrectlyTyped() : void
581
    {
582
        $schema = new Schema([
583
            'query'      => $this->SomeObjectType,
584
            'directives' => ['somedirective'],
585
        ]);
586
587
        $this->assertMatchesValidationMessage(
588
            $schema->validate(),
589
            [['message' => 'Expected directive but got: somedirective.']]
590
        );
591
    }
592
593
    /**
594
     * @see it('accepts an Object type with fields object')
595
     */
596
    public function testAcceptsAnObjectTypeWithFieldsObject() : void
597
    {
598
        $schema = BuildSchema::build('
599
      type Query {
600
        field: SomeObject
601
      }
602
603
      type SomeObject {
604
        field: String
605
      }
606
        ');
607
608
        self::assertEquals([], $schema->validate());
609
    }
610
611
    /**
612
     * @see it('rejects an Object type with missing fields')
613
     */
614
    public function testRejectsAnObjectTypeWithMissingFields() : void
615
    {
616
        $schema = BuildSchema::build('
617
      type Query {
618
        test: IncompleteObject
619
      }
620
621
      type IncompleteObject
622
        ');
623
624
        $this->assertMatchesValidationMessage(
625
            $schema->validate(),
626
            [[
627
                'message'   => 'Type IncompleteObject must define one or more fields.',
628
                'locations' => [['line' => 6, 'column' => 7]],
629
            ],
630
            ]
631
        );
632
633
        $manualSchema = $this->schemaWithFieldType(
634
            new ObjectType([
635
                'name'   => 'IncompleteObject',
636
                'fields' => [],
637
            ])
638
        );
639
640
        $this->assertMatchesValidationMessage(
641
            $manualSchema->validate(),
642
            [['message' => 'Type IncompleteObject must define one or more fields.']]
643
        );
644
645
        $manualSchema2 = $this->schemaWithFieldType(
646
            new ObjectType([
647
                'name'   => 'IncompleteObject',
648
                'fields' => static function () {
649
                    return [];
650
                },
651
            ])
652
        );
653
654
        $this->assertMatchesValidationMessage(
655
            $manualSchema2->validate(),
656
            [['message' => 'Type IncompleteObject must define one or more fields.']]
657
        );
658
    }
659
660
    /**
661
     * DESCRIBE: Type System: Fields args must be properly named
662
     */
663
    private function schemaWithFieldType($type) : Schema
664
    {
665
        return new Schema([
666
            'query' => new ObjectType([
667
                'name'   => 'Query',
668
                'fields' => ['f' => ['type' => $type]],
669
            ]),
670
            'types' => [$type],
671
        ]);
672
    }
673
674
    /**
675
     * @see it('rejects an Object type with incorrectly named fields')
676
     */
677
    public function testRejectsAnObjectTypeWithIncorrectlyNamedFields() : void
678
    {
679
        $schema = $this->schemaWithFieldType(
680
            new ObjectType([
681
                'name'   => 'SomeObject',
682
                'fields' => [
683
                    'bad-name-with-dashes' => ['type' => Type::string()],
684
                ],
685
            ])
686
        );
687
688
        $this->assertMatchesValidationMessage(
689
            $schema->validate(),
690
            [[
691
                'message' => 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but ' .
692
                    '"bad-name-with-dashes" does not.',
693
            ],
694
            ]
695
        );
696
    }
697
698
    /**
699
     * DESCRIBE: Type System: Union types must be valid
700
     */
701
    public function testAcceptsShorthandNotationForFields() : void
702
    {
703
        $this->expectNotToPerformAssertions();
704
        $schema = $this->schemaWithFieldType(
705
            new ObjectType([
706
                'name'   => 'SomeObject',
707
                'fields' => [
708
                    'field' => Type::string(),
709
                ],
710
            ])
711
        );
712
        $schema->assertValid();
713
    }
714
715
    /**
716
     * @see it('accepts field args with valid names')
717
     */
718
    public function testAcceptsFieldArgsWithValidNames() : void
719
    {
720
        $schema = $this->schemaWithFieldType(new ObjectType([
721
            'name'   => 'SomeObject',
722
            'fields' => [
723
                'goodField' => [
724
                    'type' => Type::string(),
725
                    'args' => [
726
                        'goodArg' => ['type' => Type::string()],
727
                    ],
728
                ],
729
            ],
730
        ]));
731
        self::assertEquals([], $schema->validate());
732
    }
733
734
    /**
735
     * @see it('rejects field arg with invalid names')
736
     */
737
    public function testRejectsFieldArgWithInvalidNames() : void
738
    {
739
        $QueryType = new ObjectType([
740
            'name'   => 'SomeObject',
741
            'fields' => [
742
                'badField' => [
743
                    'type' => Type::string(),
744
                    'args' => [
745
                        'bad-name-with-dashes' => ['type' => Type::string()],
746
                    ],
747
                ],
748
            ],
749
        ]);
750
        $schema    = new Schema(['query' => $QueryType]);
751
752
        $this->assertMatchesValidationMessage(
753
            $schema->validate(),
754
            [['message' => 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.']]
755
        );
756
    }
757
758
    /**
759
     * @see it('accepts a Union type with member types')
760
     */
761
    public function testAcceptsAUnionTypeWithArrayTypes() : void
762
    {
763
        $schema = BuildSchema::build('
764
      type Query {
765
        test: GoodUnion
766
      }
767
768
      type TypeA {
769
        field: String
770
      }
771
772
      type TypeB {
773
        field: String
774
      }
775
776
      union GoodUnion =
777
        | TypeA
778
        | TypeB
779
        ');
780
781
        self::assertEquals([], $schema->validate());
782
    }
783
784
    // DESCRIBE: Type System: Input Objects must have fields
785
786
    /**
787
     * @see it('rejects a Union type with empty types')
788
     */
789
    public function testRejectsAUnionTypeWithEmptyTypes() : void
790
    {
791
        $schema = BuildSchema::build('
792
            type Query {
793
                test: BadUnion
794
            }
795
            
796
            union BadUnion
797
        ');
798
799
        $schema = SchemaExtender::extend(
800
            $schema,
801
            Parser::parse('
802
                directive @test on UNION
803
        
804
                extend union BadUnion @test
805
            ')
806
        );
807
808
        $this->assertMatchesValidationMessage(
809
            $schema->validate(),
810
            [[
811
                'message'   => 'Union type BadUnion must define one or more member types.',
812
                'locations' => [['line' => 6, 'column' => 13], ['line' => 4, 'column' => 11]],
813
            ],
814
            ]
815
        );
816
    }
817
818
    /**
819
     * @see it('rejects a Union type with duplicated member type')
820
     */
821
    public function testRejectsAUnionTypeWithDuplicatedMemberType() : void
822
    {
823
        $schema = BuildSchema::build('
824
      type Query {
825
        test: BadUnion
826
      }
827
828
      type TypeA {
829
        field: String
830
      }
831
832
      type TypeB {
833
        field: String
834
      }
835
836
      union BadUnion =
837
        | TypeA
838
        | TypeB
839
        | TypeA
840
        ');
841
        $this->assertMatchesValidationMessage(
842
            $schema->validate(),
843
            [[
844
                'message'   => 'Union type BadUnion can only include type TypeA once.',
845
                'locations' => [['line' => 15, 'column' => 11], ['line' => 17, 'column' => 11]],
846
            ],
847
            ]
848
        );
849
850
        $extendedSchema = SchemaExtender::extend(
851
            $schema,
852
            Parser::parse('extend union BadUnion = TypeB')
853
        );
854
855
        $this->assertMatchesValidationMessage(
856
            $extendedSchema->validate(),
857
            [
858
                [
859
                    'message'   => 'Union type BadUnion can only include type TypeA once.',
860
                    'locations' => [['line' => 15, 'column' => 11], ['line' => 17, 'column' => 11]],
861
                ],
862
                [
863
                    'message' => 'Union type BadUnion can only include type TypeB once.',
864
                    'locations' => [[ 'line' => 16, 'column' => 11 ], [ 'line' => 3, 'column' => 5 ]],
865
                ],
866
            ]
867
        );
868
    }
869
870
    /**
871
     * @see it('rejects a Union type with non-Object members types')
872
     */
873
    public function testRejectsAUnionTypeWithNonObjectMembersType() : void
874
    {
875
        $schema = BuildSchema::build('
876
      type Query {
877
        test: BadUnion
878
      }
879
880
      type TypeA {
881
        field: String
882
      }
883
884
      type TypeB {
885
        field: String
886
      }
887
888
      union BadUnion =
889
        | TypeA
890
        | String
891
        | TypeB
892
        ');
893
894
        $schema = SchemaExtender::extend($schema, Parser::parse('extend union BadUnion = Int'));
895
896
        $this->assertMatchesValidationMessage(
897
            $schema->validate(),
898
            [
899
                [
900
                    'message'   => 'Union type BadUnion can only include Object types, it cannot include String.',
901
                    'locations' => [['line' => 16, 'column' => 11]],
902
                ],
903
                [
904
                    'message' => 'Union type BadUnion can only include Object types, it cannot include Int.',
905
                    'locations' => [[ 'line' => 1, 'column' => 25 ]],
906
                ],
907
            ]
908
        );
909
910
        $badUnionMemberTypes = [
911
            Type::string(),
912
            Type::nonNull($this->SomeObjectType),
913
            Type::listOf($this->SomeObjectType),
914
            $this->SomeInterfaceType,
915
            $this->SomeUnionType,
916
            $this->SomeEnumType,
917
            $this->SomeInputObjectType,
918
        ];
919
920
        foreach ($badUnionMemberTypes as $memberType) {
921
            $badSchema = $this->schemaWithFieldType(
922
                new UnionType(['name' => 'BadUnion', 'types' => [$memberType]])
923
            );
924
            $this->assertMatchesValidationMessage(
925
                $badSchema->validate(),
926
                [[
927
                    'message' => 'Union type BadUnion can only include Object types, ' .
928
                        'it cannot include ' . Utils::printSafe($memberType) . '.',
929
                ],
930
                ]
931
            );
932
        }
933
    }
934
935
    // DESCRIBE: Type System: Enum types must be well defined
936
937
    /**
938
     * @see it('accepts an Input Object type with fields')
939
     */
940
    public function testAcceptsAnInputObjectTypeWithFields() : void
941
    {
942
        $schema = BuildSchema::build('
943
      type Query {
944
        field(arg: SomeInputObject): String
945
      }
946
947
      input SomeInputObject {
948
        field: String
949
      }
950
        ');
951
        self::assertEquals([], $schema->validate());
952
    }
953
954
    /**
955
     * @see it('rejects an Input Object type with missing fields')
956
     */
957
    public function testRejectsAnInputObjectTypeWithMissingFields() : void
958
    {
959
        $schema = BuildSchema::build('
960
      type Query {
961
        field(arg: SomeInputObject): String
962
      }
963
964
      input SomeInputObject
965
        ');
966
967
        $schema = SchemaExtender::extend(
968
            $schema,
969
            Parser::parse('
970
        directive @test on ENUM
971
972
        extend input SomeInputObject @test
973
            '),
974
            ['assumeValid' => true]
975
        );
976
977
        $this->assertMatchesValidationMessage(
978
            $schema->validate(),
979
            [[
980
                'message'   => 'Input Object type SomeInputObject must define one or more fields.',
981
                'locations' => [['line' => 6, 'column' => 7], ['line' => 3, 'column' => 23]],
982
            ],
983
            ]
984
        );
985
    }
986
987
    /**
988
     * @see it('accepts an Input Object with breakable circular reference')
989
     */
990
    public function testAcceptsAnInputObjectWithBreakableCircularReference() : void
991
    {
992
        $schema = BuildSchema::build('
993
      input AnotherInputObject {
994
        parent: SomeInputObject
995
      }
996
      
997
      type Query {
998
        field(arg: SomeInputObject): String
999
      }
1000
      
1001
      input SomeInputObject {
1002
        self: SomeInputObject
1003
        arrayOfSelf: [SomeInputObject]
1004
        nonNullArrayOfSelf: [SomeInputObject]!
1005
        nonNullArrayOfNonNullSelf: [SomeInputObject!]!
1006
        intermediateSelf: AnotherInputObject
1007
      }
1008
        ');
1009
        self::assertEquals([], $schema->validate());
1010
    }
1011
1012
    /**
1013
     * @see it('rejects an Input Object with non-breakable circular reference')
1014
     */
1015
    public function testRejectsAnInputObjectWithNonBreakableCircularReference() : void
1016
    {
1017
        $schema = BuildSchema::build('
1018
      type Query {
1019
        field(arg: SomeInputObject): String
1020
      }
1021
      
1022
      input SomeInputObject {
1023
        nonNullSelf: SomeInputObject!
1024
      }
1025
        ');
1026
        $this->assertMatchesValidationMessage(
1027
            $schema->validate(),
1028
            [
1029
                [
1030
                    'message'   => 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "nonNullSelf".',
1031
                    'locations' => [['line' => 7, 'column' => 9]],
1032
                ],
1033
            ]
1034
        );
1035
    }
1036
1037
    /**
1038
     * @see it('rejects Input Objects with non-breakable circular reference spread across them')
1039
     */
1040
    public function testRejectsInputObjectsWithNonBreakableCircularReferenceSpreadAcrossThem() : void
1041
    {
1042
        $schema = BuildSchema::build('
1043
      type Query {
1044
        field(arg: SomeInputObject): String
1045
      }
1046
      
1047
      input SomeInputObject {
1048
        startLoop: AnotherInputObject!
1049
      }
1050
      
1051
      input AnotherInputObject {
1052
        nextInLoop: YetAnotherInputObject!
1053
      }
1054
      
1055
      input YetAnotherInputObject {
1056
        closeLoop: SomeInputObject!
1057
      }
1058
        ');
1059
        $this->assertMatchesValidationMessage(
1060
            $schema->validate(),
1061
            [
1062
                [
1063
                    'message'   => 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.nextInLoop.closeLoop".',
1064
                    'locations' => [
1065
                        ['line' => 7, 'column' => 9],
1066
                        ['line' => 11, 'column' => 9],
1067
                        ['line' => 15, 'column' => 9],
1068
                    ],
1069
                ],
1070
            ]
1071
        );
1072
    }
1073
1074
    /**
1075
     * @see it('rejects Input Objects with multiple non-breakable circular reference')
1076
     */
1077
    public function testRejectsInputObjectsWithMultipleNonBreakableCircularReferences() : void
1078
    {
1079
        $schema = BuildSchema::build('
1080
      type Query {
1081
        field(arg: SomeInputObject): String
1082
      }
1083
      
1084
      input SomeInputObject {
1085
        startLoop: AnotherInputObject!
1086
      }
1087
      
1088
      input AnotherInputObject {
1089
        closeLoop: SomeInputObject!
1090
        startSecondLoop: YetAnotherInputObject!
1091
      }
1092
      
1093
      input YetAnotherInputObject {
1094
        closeSecondLoop: AnotherInputObject!
1095
        nonNullSelf: YetAnotherInputObject!
1096
      }
1097
        ');
1098
        $this->assertMatchesValidationMessage(
1099
            $schema->validate(),
1100
            [
1101
                [
1102
                    'message'   => 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.closeLoop".',
1103
                    'locations' => [
1104
                        ['line' => 7, 'column' => 9],
1105
                        ['line' => 11, 'column' => 9],
1106
                    ],
1107
                ],
1108
                [
1109
                    'message'   => 'Cannot reference Input Object "AnotherInputObject" within itself through a series of non-null fields: "startSecondLoop.closeSecondLoop".',
1110
                    'locations' => [
1111
                        ['line' => 12, 'column' => 9],
1112
                        ['line' => 16, 'column' => 9],
1113
                    ],
1114
                ],
1115
                [
1116
                    'message'   => 'Cannot reference Input Object "YetAnotherInputObject" within itself through a series of non-null fields: "nonNullSelf".',
1117
                    'locations' => [
1118
                        ['line' => 17, 'column' => 9],
1119
                    ],
1120
                ],
1121
            ]
1122
        );
1123
    }
1124
1125
    /**
1126
     * @see it('rejects an Input Object type with incorrectly typed fields')
1127
     */
1128
    public function testRejectsAnInputObjectTypeWithIncorrectlyTypedFields() : void
1129
    {
1130
        $schema = BuildSchema::build('
1131
      type Query {
1132
        field(arg: SomeInputObject): String
1133
      }
1134
      
1135
      type SomeObject {
1136
        field: String
1137
      }
1138
1139
      union SomeUnion = SomeObject
1140
      
1141
      input SomeInputObject {
1142
        badObject: SomeObject
1143
        badUnion: SomeUnion
1144
        goodInputObject: SomeInputObject
1145
      }
1146
        ');
1147
        $this->assertMatchesValidationMessage(
1148
            $schema->validate(),
1149
            [
1150
                [
1151
                    'message'   => 'The type of SomeInputObject.badObject must be Input Type but got: SomeObject.',
1152
                    'locations' => [['line' => 13, 'column' => 20]],
1153
                ],
1154
                [
1155
                    'message'   => 'The type of SomeInputObject.badUnion must be Input Type but got: SomeUnion.',
1156
                    'locations' => [['line' => 14, 'column' => 19]],
1157
                ],
1158
            ]
1159
        );
1160
    }
1161
1162
    /**
1163
     * @see it('rejects an Enum type without values')
1164
     */
1165
    public function testRejectsAnEnumTypeWithoutValues() : void
1166
    {
1167
        $schema = BuildSchema::build('
1168
      type Query {
1169
        field: SomeEnum
1170
      }
1171
      
1172
      enum SomeEnum
1173
        ');
1174
1175
        $schema = SchemaExtender::extend(
1176
            $schema,
1177
            Parser::parse('
1178
        directive @test on ENUM
1179
1180
        extend enum SomeEnum @test
1181
            ')
1182
        );
1183
1184
        $this->assertMatchesValidationMessage(
1185
            $schema->validate(),
1186
            [[
1187
                'message'   => 'Enum type SomeEnum must define one or more values.',
1188
                'locations' => [['line' => 6, 'column' => 7], ['line' => 3, 'column' => 23]],
1189
            ],
1190
            ]
1191
        );
1192
    }
1193
1194
    /**
1195
     * @see it('rejects an Enum type with duplicate values')
1196
     */
1197
    public function testRejectsAnEnumTypeWithDuplicateValues() : void
1198
    {
1199
        $schema = BuildSchema::build('
1200
      type Query {
1201
        field: SomeEnum
1202
      }
1203
      
1204
      enum SomeEnum {
1205
        SOME_VALUE
1206
        SOME_VALUE
1207
      }
1208
        ');
1209
        $this->assertMatchesValidationMessage(
1210
            $schema->validate(),
1211
            [[
1212
                'message'   => 'Enum type SomeEnum can include value SOME_VALUE only once.',
1213
                'locations' => [['line' => 7, 'column' => 9], ['line' => 8, 'column' => 9]],
1214
            ],
1215
            ]
1216
        );
1217
    }
1218
1219
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() : void
1220
    {
1221
        $enum = new EnumType([
1222
            'name'   => 'SomeEnum',
1223
            'values' => [
1224
                'value' => ['isDeprecated' => true],
1225
            ],
1226
        ]);
1227
        $this->expectException(InvariantViolation::class);
1228
        $this->expectExceptionMessage('SomeEnum.value should provide "deprecationReason" instead of "isDeprecated".');
1229
        $enum->assertValid();
1230
    }
1231
1232
    /**
1233
     * DESCRIBE: Type System: Object fields must have output types
1234
     *
1235
     * @return string[][]
1236
     */
1237
    public function invalidEnumValueName() : array
1238
    {
1239
        return [
1240
            ['#value', 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.'],
1241
            ['1value', 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "1value" does not.'],
1242
            ['KEBAB-CASE', 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "KEBAB-CASE" does not.'],
1243
            ['false', 'Enum type SomeEnum cannot include value: false.'],
1244
            ['true', 'Enum type SomeEnum cannot include value: true.'],
1245
            ['null', 'Enum type SomeEnum cannot include value: null.'],
1246
        ];
1247
    }
1248
1249
    /**
1250
     * @see          it('rejects an Enum type with incorrectly named values')
1251
     *
1252
     * @dataProvider invalidEnumValueName
1253
     */
1254
    public function testRejectsAnEnumTypeWithIncorrectlyNamedValues($name, $expectedMessage) : void
1255
    {
1256
        $schema = $this->schemaWithEnum($name);
1257
1258
        $this->assertMatchesValidationMessage(
1259
            $schema->validate(),
1260
            [['message' => $expectedMessage],
1261
            ]
1262
        );
1263
    }
1264
1265
    private function schemaWithEnum($name)
1266
    {
1267
        return $this->schemaWithFieldType(
1268
            new EnumType([
1269
                'name'   => 'SomeEnum',
1270
                'values' => [
1271
                    $name => [],
1272
                ],
1273
            ])
1274
        );
1275
    }
1276
1277
    /**
1278
     * @see it('accepts an output type as an Object field type')
1279
     */
1280
    public function testAcceptsAnOutputTypeAsNnObjectFieldType() : void
1281
    {
1282
        foreach ($this->outputTypes as $type) {
1283
            $schema = $this->schemaWithObjectFieldOfType($type);
1284
            self::assertEquals([], $schema->validate());
1285
        }
1286
    }
1287
1288
    /**
1289
     * DESCRIBE: Type System: Objects can only implement unique interfaces
1290
     */
1291
    private function schemaWithObjectFieldOfType($fieldType) : Schema
1292
    {
1293
        $BadObjectType = new ObjectType([
1294
            'name'   => 'BadObject',
1295
            'fields' => [
1296
                'badField' => ['type' => $fieldType],
1297
            ],
1298
        ]);
1299
1300
        return new Schema([
1301
            'query' => new ObjectType([
1302
                'name'   => 'Query',
1303
                'fields' => [
1304
                    'f' => ['type' => $BadObjectType],
1305
                ],
1306
            ]),
1307
            'types' => [$this->SomeObjectType],
1308
        ]);
1309
    }
1310
1311
    /**
1312
     * @see it('rejects an empty Object field type')
1313
     */
1314
    public function testRejectsAnEmptyObjectFieldType() : void
1315
    {
1316
        $schema = $this->schemaWithObjectFieldOfType(null);
1317
1318
        $this->assertMatchesValidationMessage(
1319
            $schema->validate(),
1320
            [['message' => 'The type of BadObject.badField must be Output Type but got: null.'],
1321
            ]
1322
        );
1323
    }
1324
1325
    /**
1326
     * @see it('rejects a non-output type as an Object field type')
1327
     */
1328
    public function testRejectsANonOutputTypeAsAnObjectFieldType() : void
1329
    {
1330
        foreach ($this->notOutputTypes as $type) {
1331
            $schema = $this->schemaWithObjectFieldOfType($type);
1332
1333
            $this->assertMatchesValidationMessage(
1334
                $schema->validate(),
1335
                [[
1336
                    'message' => 'The type of BadObject.badField must be Output Type but got: ' . Utils::printSafe($type) . '.',
1337
                ],
1338
                ]
1339
            );
1340
        }
1341
    }
1342
1343
    /**
1344
     * @see it('rejects a non-type value as an Object field type')
1345
     */
1346
    public function testRejectsANonTypeValueAsAnObjectFieldType()
1347
    {
1348
        $schema = $this->schemaWithObjectFieldOfType($this->Number);
1349
        $this->assertMatchesValidationMessage(
1350
            $schema->validate(),
1351
            [
1352
                ['message' => 'The type of BadObject.badField must be Output Type but got: 1.'],
1353
                ['message' => 'Expected GraphQL named type but got: 1.'],
1354
            ]
1355
        );
1356
    }
1357
1358
    /**
1359
     * @see it('rejects with relevant locations for a non-output type as an Object field type')
1360
     */
1361
    public function testRejectsWithReleventLocationsForANonOutputTypeAsAnObjectFieldType() : void
1362
    {
1363
        $schema = BuildSchema::build('
1364
      type Query {
1365
        field: [SomeInputObject]
1366
      }
1367
      
1368
      input SomeInputObject {
1369
        field: String
1370
      }
1371
        ');
1372
        $this->assertMatchesValidationMessage(
1373
            $schema->validate(),
1374
            [[
1375
                'message'   => 'The type of Query.field must be Output Type but got: [SomeInputObject].',
1376
                'locations' => [['line' => 3, 'column' => 16]],
1377
            ],
1378
            ]
1379
        );
1380
    }
1381
1382
    // DESCRIBE: Type System: Interface fields must have output types
1383
1384
    /**
1385
     * @see it('rejects an Object implementing a non-type values')
1386
     */
1387
    public function testRejectsAnObjectImplementingANonTypeValues() : void
1388
    {
1389
        $schema   = new Schema([
1390
            'query' => new ObjectType([
1391
                'name'       => 'BadObject',
1392
                'interfaces' => [null],
1393
                'fields'     => ['a' => Type::string()],
1394
            ]),
1395
        ]);
1396
        $expected = ['message' => 'Type BadObject must only implement Interface types, it cannot implement null.'];
1397
1398
        $this->assertMatchesValidationMessage(
1399
            $schema->validate(),
1400
            [$expected]
1401
        );
1402
    }
1403
1404
    /**
1405
     * @see it('rejects an Object implementing a non-Interface type')
1406
     */
1407
    public function testRejectsAnObjectImplementingANonInterfaceType() : void
1408
    {
1409
        $schema = BuildSchema::build('
1410
      type Query {
1411
        field: BadObject
1412
      }
1413
      
1414
      input SomeInputObject {
1415
        field: String
1416
      }
1417
      
1418
      type BadObject implements SomeInputObject {
1419
        field: String
1420
      }
1421
        ');
1422
        $this->assertMatchesValidationMessage(
1423
            $schema->validate(),
1424
            [[
1425
                'message'   => 'Type BadObject must only implement Interface types, it cannot implement SomeInputObject.',
1426
                'locations' => [['line' => 10, 'column' => 33]],
1427
            ],
1428
            ]
1429
        );
1430
    }
1431
1432
    /**
1433
     * @see it('rejects an Object implementing the same interface twice')
1434
     */
1435
    public function testRejectsAnObjectImplementingTheSameInterfaceTwice() : void
1436
    {
1437
        $schema = BuildSchema::build('
1438
      type Query {
1439
        field: AnotherObject
1440
      }
1441
      
1442
      interface AnotherInterface {
1443
        field: String
1444
      }
1445
      
1446
      type AnotherObject implements AnotherInterface & AnotherInterface {
1447
        field: String
1448
      }
1449
        ');
1450
        $this->assertMatchesValidationMessage(
1451
            $schema->validate(),
1452
            [[
1453
                'message'   => 'Type AnotherObject can only implement AnotherInterface once.',
1454
                'locations' => [['line' => 10, 'column' => 37], ['line' => 10, 'column' => 56]],
1455
            ],
1456
            ]
1457
        );
1458
    }
1459
1460
    /**
1461
     * @see it('rejects an Object implementing the same interface twice due to extension')
1462
     */
1463
    public function testRejectsAnObjectImplementingTheSameInterfaceTwiceDueToExtension() : void
1464
    {
1465
        $this->expectNotToPerformAssertions();
1466
        self::markTestIncomplete('extend does not work this way (yet).');
1467
        $schema = BuildSchema::build('
1468
      type Query {
1469
        field: AnotherObject
1470
      }
1471
      
1472
      interface AnotherInterface {
1473
        field: String
1474
      }
1475
      
1476
      type AnotherObject implements AnotherInterface {
1477
        field: String
1478
      }
1479
      
1480
      extend type AnotherObject implements AnotherInterface
1481
        ');
1482
        $this->assertMatchesValidationMessage(
1483
            $schema->validate(),
1484
            [[
1485
                'message'   => 'Type AnotherObject can only implement AnotherInterface once.',
1486
                'locations' => [['line' => 10, 'column' => 37], ['line' => 14, 'column' => 38]],
1487
            ],
1488
            ]
1489
        );
1490
    }
1491
1492
    // DESCRIBE: Type System: Interface extensions should be valid
1493
1494
    /**
1495
     * @see it('rejects an Object implementing the extended interface due to missing field')
1496
     */
1497
    public function testRejectsAnObjectImplementingTheExtendedInterfaceDueToMissingField()
1498
    {
1499
        $schema         = BuildSchema::build('
1500
          type Query {
1501
            test: AnotherObject
1502
          }
1503
    
1504
          interface AnotherInterface {
1505
            field: String
1506
          }
1507
    
1508
          type AnotherObject implements AnotherInterface {
1509
            field: String
1510
          }');
1511
        $extendedSchema = SchemaExtender::extend(
1512
            $schema,
1513
            Parser::parse('
1514
                extend interface AnotherInterface {
1515
                  newField: String
1516
                }
1517
        
1518
                extend type AnotherObject {
1519
                  differentNewField: String
1520
                }
1521
            ')
1522
        );
1523
        $this->assertMatchesValidationMessage(
1524
            $extendedSchema->validate(),
1525
            [[
1526
                'message'   => 'Interface field AnotherInterface.newField expected but AnotherObject does not provide it.',
1527
                'locations' => [
1528
                    ['line' => 3, 'column' => 19],
1529
                    ['line' => 7, 'column' => 7],
1530
                    ['line' => 6, 'column' => 17],
1531
                ],
1532
            ],
1533
            ]
1534
        );
1535
    }
1536
1537
    /**
1538
     * @see it('rejects an Object implementing the extended interface due to missing field args')
1539
     */
1540
    public function testRejectsAnObjectImplementingTheExtendedInterfaceDueToMissingFieldArgs()
1541
    {
1542
        $schema         = BuildSchema::build('
1543
          type Query {
1544
            test: AnotherObject
1545
          }
1546
    
1547
          interface AnotherInterface {
1548
            field: String
1549
          }
1550
    
1551
          type AnotherObject implements AnotherInterface {
1552
            field: String
1553
          }');
1554
        $extendedSchema = SchemaExtender::extend(
1555
            $schema,
1556
            Parser::parse('
1557
                extend interface AnotherInterface {
1558
                  newField(test: Boolean): String
1559
                }
1560
        
1561
                extend type AnotherObject {
1562
                  newField: String
1563
                }
1564
            ')
1565
        );
1566
        $this->assertMatchesValidationMessage(
1567
            $extendedSchema->validate(),
1568
            [[
1569
                'message'   => 'Interface field argument AnotherInterface.newField(test:) expected but AnotherObject.newField does not provide it.',
1570
                'locations' => [
1571
                    ['line' => 3, 'column' => 28],
1572
                    ['line' => 7, 'column' => 19],
1573
                ],
1574
            ],
1575
            ]
1576
        );
1577
    }
1578
1579
    /**
1580
     * @see it('rejects Objects implementing the extended interface due to mismatching interface type')
1581
     */
1582
    public function testRejectsObjectsImplementingTheExtendedInterfaceDueToMismatchingInterfaceType()
1583
    {
1584
        $schema         = BuildSchema::build('
1585
          type Query {
1586
            test: AnotherObject
1587
          }
1588
    
1589
          interface AnotherInterface {
1590
            field: String
1591
          }
1592
    
1593
          type AnotherObject implements AnotherInterface {
1594
            field: String
1595
          }');
1596
        $extendedSchema = SchemaExtender::extend(
1597
            $schema,
1598
            Parser::parse('
1599
                extend interface AnotherInterface {
1600
                  newInterfaceField: NewInterface
1601
                }
1602
        
1603
                interface NewInterface {
1604
                  newField: String
1605
                }
1606
        
1607
                interface MismatchingInterface {
1608
                  newField: String
1609
                }
1610
        
1611
                extend type AnotherObject {
1612
                  newInterfaceField: MismatchingInterface
1613
                }
1614
        
1615
                # Required to prevent unused interface errors
1616
                type DummyObject implements NewInterface & MismatchingInterface {
1617
                  newField: String
1618
                }
1619
            ')
1620
        );
1621
        $this->assertMatchesValidationMessage(
1622
            $extendedSchema->validate(),
1623
            [[
1624
                'message'   => 'Interface field AnotherInterface.newInterfaceField expects type NewInterface but AnotherObject.newInterfaceField is type MismatchingInterface.',
1625
                'locations' => [['line' => 3, 'column' => 38], ['line' => 15, 'column' => 38]],
1626
            ],
1627
            ]
1628
        );
1629
    }
1630
1631
    // DESCRIBE: Type System: Field arguments must have input types
1632
1633
    /**
1634
     * @see it('accepts an output type as an Interface field type')
1635
     */
1636
    public function testAcceptsAnOutputTypeAsAnInterfaceFieldType() : void
1637
    {
1638
        foreach ($this->outputTypes as $type) {
1639
            $schema = $this->schemaWithInterfaceFieldOfType($type);
1640
            self::assertEquals([], $schema->validate());
1641
        }
1642
    }
1643
1644
    private function schemaWithInterfaceFieldOfType($fieldType)
1645
    {
1646
        $BadInterfaceType = new InterfaceType([
1647
            'name'   => 'BadInterface',
1648
            'fields' => [
1649
                'badField' => ['type' => $fieldType],
1650
            ],
1651
        ]);
1652
1653
        $BadImplementingType = new ObjectType([
1654
            'name' => 'BadImplementing',
1655
            'interfaces' => [ $BadInterfaceType ],
1656
            'fields' => [
1657
                'badField' => [ 'type' => $fieldType ],
1658
            ],
1659
        ]);
1660
1661
        return new Schema([
1662
            'query' => new ObjectType([
1663
                'name'   => 'Query',
1664
                'fields' => [
1665
                    'f' => ['type' => $BadInterfaceType],
1666
                ],
1667
            ]),
1668
            'types' => [ $BadImplementingType ],
1669
        ]);
1670
    }
1671
1672
    /**
1673
     * @see it('rejects an empty Interface field type')
1674
     */
1675
    public function testRejectsAnEmptyInterfaceFieldType() : void
1676
    {
1677
        $schema = $this->schemaWithInterfaceFieldOfType(null);
1678
        $this->assertMatchesValidationMessage(
1679
            $schema->validate(),
1680
            [
1681
                ['message' => 'The type of BadInterface.badField must be Output Type but got: null.'],
1682
                ['message' => 'The type of BadImplementing.badField must be Output Type but got: null.'],
1683
            ]
1684
        );
1685
    }
1686
1687
    /**
1688
     * @see it('rejects a non-output type as an Interface field type')
1689
     */
1690
    public function testRejectsANonOutputTypeAsAnInterfaceFieldType() : void
1691
    {
1692
        foreach ($this->notOutputTypes as $type) {
1693
            $schema = $this->schemaWithInterfaceFieldOfType($type);
1694
1695
            $this->assertMatchesValidationMessage(
1696
                $schema->validate(),
1697
                [
1698
                    ['message' => 'The type of BadInterface.badField must be Output Type but got: ' . Utils::printSafe($type) . '.'],
1699
                    ['message' => 'The type of BadImplementing.badField must be Output Type but got: ' . Utils::printSafe($type) . '.'],
1700
                ]
1701
            );
1702
        }
1703
    }
1704
1705
    /**
1706
     * @see it('rejects a non-type value as an Interface field type')
1707
     */
1708
    public function testRejectsANonTypeValueAsAnInterfaceFieldType()
1709
    {
1710
        $schema = $this->schemaWithInterfaceFieldOfType('string');
1711
        $this->assertMatchesValidationMessage(
1712
            $schema->validate(),
1713
            [
1714
                ['message' => 'The type of BadInterface.badField must be Output Type but got: string.'],
1715
                ['message' => 'Expected GraphQL named type but got: string.'],
1716
                ['message' => 'The type of BadImplementing.badField must be Output Type but got: string.'],
1717
            ]
1718
        );
1719
    }
1720
1721
    // DESCRIBE: Type System: Input Object fields must have input types
1722
1723
    /**
1724
     * @see it('rejects a non-output type as an Interface field type with locations')
1725
     */
1726
    public function testRejectsANonOutputTypeAsAnInterfaceFieldTypeWithLocations() : void
1727
    {
1728
        $schema = BuildSchema::build('
1729
      type Query {
1730
        field: SomeInterface
1731
      }
1732
      
1733
      interface SomeInterface {
1734
        field: SomeInputObject
1735
      }
1736
      
1737
      input SomeInputObject {
1738
        foo: String
1739
      }
1740
1741
      type SomeObject implements SomeInterface {
1742
        field: SomeInputObject
1743
      }
1744
        ');
1745
        $this->assertMatchesValidationMessage(
1746
            $schema->validate(),
1747
            [
1748
                [
1749
                    'message'   => 'The type of SomeInterface.field must be Output Type but got: SomeInputObject.',
1750
                    'locations' => [['line' => 7, 'column' => 16]],
1751
                ],
1752
                [
1753
                    'message' => 'The type of SomeObject.field must be Output Type but got: SomeInputObject.',
1754
                    'locations' => [[ 'line' => 15, 'column' => 16 ]],
1755
                ],
1756
            ]
1757
        );
1758
    }
1759
1760
    /**
1761
     * @see it('accepts an interface not implemented by at least one object')
1762
     */
1763
    public function testRejectsAnInterfaceNotImplementedByAtLeastOneObject()
1764
    {
1765
        $schema = BuildSchema::build('
1766
      type Query {
1767
        test: SomeInterface
1768
      }
1769
1770
      interface SomeInterface {
1771
        foo: String
1772
      }
1773
        ');
1774
        $this->assertMatchesValidationMessage(
1775
            $schema->validate(),
1776
            []
1777
        );
1778
    }
1779
1780
    /**
1781
     * @see it('accepts an input type as a field arg type')
1782
     */
1783
    public function testAcceptsAnInputTypeAsAFieldArgType() : void
1784
    {
1785
        foreach ($this->inputTypes as $type) {
1786
            $schema = $this->schemaWithArgOfType($type);
1787
            self::assertEquals([], $schema->validate());
1788
        }
1789
    }
1790
1791
    private function schemaWithArgOfType($argType)
1792
    {
1793
        $BadObjectType = new ObjectType([
1794
            'name'   => 'BadObject',
1795
            'fields' => [
1796
                'badField' => [
1797
                    'type' => Type::string(),
1798
                    'args' => [
1799
                        'badArg' => ['type' => $argType],
1800
                    ],
1801
                ],
1802
            ],
1803
        ]);
1804
1805
        return new Schema([
1806
            'query' => new ObjectType([
1807
                'name'   => 'Query',
1808
                'fields' => [
1809
                    'f' => ['type' => $BadObjectType],
1810
                ],
1811
            ]),
1812
            'types' => [$this->SomeObjectType],
1813
        ]);
1814
    }
1815
1816
    /**
1817
     * @see it('rejects an empty field arg type')
1818
     */
1819
    public function testRejectsAnEmptyFieldArgType() : void
1820
    {
1821
        $schema = $this->schemaWithArgOfType(null);
1822
        $this->assertMatchesValidationMessage(
1823
            $schema->validate(),
1824
            [['message' => 'The type of BadObject.badField(badArg:) must be Input Type but got: null.'],
1825
            ]
1826
        );
1827
    }
1828
1829
    // DESCRIBE: Objects must adhere to Interface they implement
1830
1831
    /**
1832
     * @see it('rejects a non-input type as a field arg type')
1833
     */
1834
    public function testRejectsANonInputTypeAsAFieldArgType() : void
1835
    {
1836
        foreach ($this->notInputTypes as $type) {
1837
            $schema = $this->schemaWithArgOfType($type);
1838
            $this->assertMatchesValidationMessage(
1839
                $schema->validate(),
1840
                [
1841
                    ['message' => 'The type of BadObject.badField(badArg:) must be Input Type but got: ' . Utils::printSafe($type) . '.'],
1842
                ]
1843
            );
1844
        }
1845
    }
1846
1847
    /**
1848
     * @see it('rejects a non-type value as a field arg type')
1849
     */
1850
    public function testRejectsANonTypeValueAsAFieldArgType()
1851
    {
1852
        $schema = $this->schemaWithArgOfType('string');
1853
        $this->assertMatchesValidationMessage(
1854
            $schema->validate(),
1855
            [
1856
                ['message' => 'The type of BadObject.badField(badArg:) must be Input Type but got: string.'],
1857
                ['message' => 'Expected GraphQL named type but got: string.'],
1858
            ]
1859
        );
1860
    }
1861
1862
    /**
1863
     * @see it('rejects a non-input type as a field arg with locations')
1864
     */
1865
    public function testANonInputTypeAsAFieldArgWithLocations() : void
1866
    {
1867
        $schema = BuildSchema::build('
1868
      type Query {
1869
        test(arg: SomeObject): String
1870
      }
1871
      
1872
      type SomeObject {
1873
        foo: String
1874
      }
1875
        ');
1876
        $this->assertMatchesValidationMessage(
1877
            $schema->validate(),
1878
            [[
1879
                'message'   => 'The type of Query.test(arg:) must be Input Type but got: SomeObject.',
1880
                'locations' => [['line' => 3, 'column' => 19]],
1881
            ],
1882
            ]
1883
        );
1884
    }
1885
1886
    /**
1887
     * @see it('accepts an input type as an input field type')
1888
     */
1889
    public function testAcceptsAnInputTypeAsAnInputFieldType() : void
1890
    {
1891
        foreach ($this->inputTypes as $type) {
1892
            $schema = $this->schemaWithInputFieldOfType($type);
1893
            self::assertEquals([], $schema->validate());
1894
        }
1895
    }
1896
1897
    private function schemaWithInputFieldOfType($inputFieldType)
1898
    {
1899
        $BadInputObjectType = new InputObjectType([
1900
            'name'   => 'BadInputObject',
1901
            'fields' => [
1902
                'badField' => ['type' => $inputFieldType],
1903
            ],
1904
        ]);
1905
1906
        return new Schema([
1907
            'query' => new ObjectType([
1908
                'name'   => 'Query',
1909
                'fields' => [
1910
                    'f' => [
1911
                        'type' => Type::string(),
1912
                        'args' => [
1913
                            'badArg' => ['type' => $BadInputObjectType],
1914
                        ],
1915
                    ],
1916
                ],
1917
            ]),
1918
            'types' => [ $this->SomeObjectType ],
1919
        ]);
1920
    }
1921
1922
    /**
1923
     * @see it('rejects an empty input field type')
1924
     */
1925
    public function testRejectsAnEmptyInputFieldType() : void
1926
    {
1927
        $schema = $this->schemaWithInputFieldOfType(null);
1928
        $this->assertMatchesValidationMessage(
1929
            $schema->validate(),
1930
            [['message' => 'The type of BadInputObject.badField must be Input Type but got: null.'],
1931
            ]
1932
        );
1933
    }
1934
1935
    /**
1936
     * @see it('rejects a non-input type as an input field type')
1937
     */
1938
    public function testRejectsANonInputTypeAsAnInputFieldType() : void
1939
    {
1940
        foreach ($this->notInputTypes as $type) {
1941
            $schema = $this->schemaWithInputFieldOfType($type);
1942
            $this->assertMatchesValidationMessage(
1943
                $schema->validate(),
1944
                [[
1945
                    'message' => 'The type of BadInputObject.badField must be Input Type but got: ' . Utils::printSafe($type) . '.',
1946
                ],
1947
                ]
1948
            );
1949
        }
1950
    }
1951
1952
    /**
1953
     * @see it('rejects a non-type value as an input field type')
1954
     */
1955
    public function testRejectsAAonTypeValueAsAnInputFieldType()
1956
    {
1957
        $schema = $this->schemaWithInputFieldOfType('string');
1958
        $this->assertMatchesValidationMessage(
1959
            $schema->validate(),
1960
            [
1961
                ['message' => 'The type of BadInputObject.badField must be Input Type but got: string.'],
1962
                ['message' => 'Expected GraphQL named type but got: string.'],
1963
            ]
1964
        );
1965
    }
1966
1967
    /**
1968
     * @see it('rejects a non-input type as an input object field with locations')
1969
     */
1970
    public function testRejectsANonInputTypeAsAnInputObjectFieldWithLocations() : void
1971
    {
1972
        $schema = BuildSchema::build('
1973
      type Query {
1974
        test(arg: SomeInputObject): String
1975
      }
1976
      
1977
      input SomeInputObject {
1978
        foo: SomeObject
1979
      }
1980
      
1981
      type SomeObject {
1982
        bar: String
1983
      }
1984
        ');
1985
        $this->assertMatchesValidationMessage(
1986
            $schema->validate(),
1987
            [[
1988
                'message'   => 'The type of SomeInputObject.foo must be Input Type but got: SomeObject.',
1989
                'locations' => [['line' => 7, 'column' => 14]],
1990
            ],
1991
            ]
1992
        );
1993
    }
1994
1995
    /**
1996
     * @see it('accepts an Object which implements an Interface')
1997
     */
1998
    public function testAcceptsAnObjectWhichImplementsAnInterface() : void
1999
    {
2000
        $schema = BuildSchema::build('
2001
      type Query {
2002
        test: AnotherObject
2003
      }
2004
      
2005
      interface AnotherInterface {
2006
        field(input: String): String
2007
      }
2008
      
2009
      type AnotherObject implements AnotherInterface {
2010
        field(input: String): String
2011
      }
2012
        ');
2013
2014
        self::assertEquals(
2015
            [],
2016
            $schema->validate()
2017
        );
2018
    }
2019
2020
    /**
2021
     * @see it('accepts an Object which implements an Interface along with more fields')
2022
     */
2023
    public function testAcceptsAnObjectWhichImplementsAnInterfaceAlongWithMoreFields() : void
2024
    {
2025
        $schema = BuildSchema::build('
2026
      type Query {
2027
        test: AnotherObject
2028
      }
2029
2030
      interface AnotherInterface {
2031
        field(input: String): String
2032
      }
2033
2034
      type AnotherObject implements AnotherInterface {
2035
        field(input: String): String
2036
        anotherField: String
2037
      }
2038
        ');
2039
2040
        self::assertEquals(
2041
            [],
2042
            $schema->validate()
2043
        );
2044
    }
2045
2046
    /**
2047
     * @see it('accepts an Object which implements an Interface field along with additional optional arguments')
2048
     */
2049
    public function testAcceptsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments() : void
2050
    {
2051
        $schema = BuildSchema::build('
2052
      type Query {
2053
        test: AnotherObject
2054
      }
2055
2056
      interface AnotherInterface {
2057
        field(input: String): String
2058
      }
2059
2060
      type AnotherObject implements AnotherInterface {
2061
        field(input: String, anotherInput: String): String
2062
      }
2063
        ');
2064
2065
        self::assertEquals(
2066
            [],
2067
            $schema->validate()
2068
        );
2069
    }
2070
2071
    /**
2072
     * @see it('rejects an Object missing an Interface field')
2073
     */
2074
    public function testRejectsAnObjectMissingAnInterfaceField() : void
2075
    {
2076
        $schema = BuildSchema::build('
2077
      type Query {
2078
        test: AnotherObject
2079
      }
2080
2081
      interface AnotherInterface {
2082
        field(input: String): String
2083
      }
2084
2085
      type AnotherObject implements AnotherInterface {
2086
        anotherField: String
2087
      }
2088
        ');
2089
2090
        $this->assertMatchesValidationMessage(
2091
            $schema->validate(),
2092
            [[
2093
                'message'   => 'Interface field AnotherInterface.field expected but ' .
2094
                    'AnotherObject does not provide it.',
2095
                'locations' => [['line' => 7, 'column' => 9], ['line' => 10, 'column' => 7]],
2096
            ],
2097
            ]
2098
        );
2099
    }
2100
2101
    /**
2102
     * @see it('rejects an Object with an incorrectly typed Interface field')
2103
     */
2104
    public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceField() : void
2105
    {
2106
        $schema = BuildSchema::build('
2107
      type Query {
2108
        test: AnotherObject
2109
      }
2110
2111
      interface AnotherInterface {
2112
        field(input: String): String
2113
      }
2114
2115
      type AnotherObject implements AnotherInterface {
2116
        field(input: String): Int
2117
      }
2118
        ');
2119
2120
        $this->assertMatchesValidationMessage(
2121
            $schema->validate(),
2122
            [[
2123
                'message'   => 'Interface field AnotherInterface.field expects type String but ' .
2124
                    'AnotherObject.field is type Int.',
2125
                'locations' => [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 31]],
2126
            ],
2127
            ]
2128
        );
2129
    }
2130
2131
    /**
2132
     * @see it('rejects an Object with a differently typed Interface field')
2133
     */
2134
    public function testRejectsAnObjectWithADifferentlyTypedInterfaceField() : void
2135
    {
2136
        $schema = BuildSchema::build('
2137
      type Query {
2138
        test: AnotherObject
2139
      }
2140
2141
      type A { foo: String }
2142
      type B { foo: String }
2143
2144
      interface AnotherInterface {
2145
        field: A
2146
      }
2147
2148
      type AnotherObject implements AnotherInterface {
2149
        field: B
2150
      }
2151
        ');
2152
2153
        $this->assertMatchesValidationMessage(
2154
            $schema->validate(),
2155
            [[
2156
                'message'   => 'Interface field AnotherInterface.field expects type A but ' .
2157
                    'AnotherObject.field is type B.',
2158
                'locations' => [['line' => 10, 'column' => 16], ['line' => 14, 'column' => 16]],
2159
            ],
2160
            ]
2161
        );
2162
    }
2163
2164
    /**
2165
     * @see it('accepts an Object with a subtyped Interface field (interface)')
2166
     */
2167
    public function testAcceptsAnObjectWithASubtypedInterfaceFieldForInterface() : void
2168
    {
2169
        $schema = BuildSchema::build('
2170
      type Query {
2171
        test: AnotherObject
2172
      }
2173
2174
      interface AnotherInterface {
2175
        field: AnotherInterface
2176
      }
2177
2178
      type AnotherObject implements AnotherInterface {
2179
        field: AnotherObject
2180
      }
2181
        ');
2182
2183
        self::assertEquals([], $schema->validate());
2184
    }
2185
2186
    /**
2187
     * @see it('accepts an Object with a subtyped Interface field (union)')
2188
     */
2189
    public function testAcceptsAnObjectWithASubtypedInterfaceFieldForUnion() : void
2190
    {
2191
        $schema = BuildSchema::build('
2192
      type Query {
2193
        test: AnotherObject
2194
      }
2195
2196
      type SomeObject {
2197
        field: String
2198
      }
2199
2200
      union SomeUnionType = SomeObject
2201
2202
      interface AnotherInterface {
2203
        field: SomeUnionType
2204
      }
2205
2206
      type AnotherObject implements AnotherInterface {
2207
        field: SomeObject
2208
      }
2209
        ');
2210
2211
        self::assertEquals([], $schema->validate());
2212
    }
2213
2214
    /**
2215
     * @see it('rejects an Object missing an Interface argument')
2216
     */
2217
    public function testRejectsAnObjectMissingAnInterfaceArgument() : void
2218
    {
2219
        $schema = BuildSchema::build('
2220
      type Query {
2221
        test: AnotherObject
2222
      }
2223
2224
      interface AnotherInterface {
2225
        field(input: String): String
2226
      }
2227
2228
      type AnotherObject implements AnotherInterface {
2229
        field: String
2230
      }
2231
        ');
2232
2233
        $this->assertMatchesValidationMessage(
2234
            $schema->validate(),
2235
            [[
2236
                'message'   => 'Interface field argument AnotherInterface.field(input:) expected ' .
2237
                    'but AnotherObject.field does not provide it.',
2238
                'locations' => [['line' => 7, 'column' => 15], ['line' => 11, 'column' => 9]],
2239
            ],
2240
            ]
2241
        );
2242
    }
2243
2244
    /**
2245
     * @see it('rejects an Object with an incorrectly typed Interface argument')
2246
     */
2247
    public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceArgument() : void
2248
    {
2249
        $schema = BuildSchema::build('
2250
      type Query {
2251
        test: AnotherObject
2252
      }
2253
2254
      interface AnotherInterface {
2255
        field(input: String): String
2256
      }
2257
2258
      type AnotherObject implements AnotherInterface {
2259
        field(input: Int): String
2260
      }
2261
        ');
2262
2263
        $this->assertMatchesValidationMessage(
2264
            $schema->validate(),
2265
            [[
2266
                'message'   => 'Interface field argument AnotherInterface.field(input:) expects ' .
2267
                    'type String but AnotherObject.field(input:) is type Int.',
2268
                'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]],
2269
            ],
2270
            ]
2271
        );
2272
    }
2273
2274
    /**
2275
     * @see it('rejects an Object with both an incorrectly typed field and argument')
2276
     */
2277
    public function testRejectsAnObjectWithBothAnIncorrectlyTypedFieldAndArgument() : void
2278
    {
2279
        $schema = BuildSchema::build('
2280
      type Query {
2281
        test: AnotherObject
2282
      }
2283
2284
      interface AnotherInterface {
2285
        field(input: String): String
2286
      }
2287
2288
      type AnotherObject implements AnotherInterface {
2289
        field(input: Int): Int
2290
      }
2291
        ');
2292
2293
        $this->assertMatchesValidationMessage(
2294
            $schema->validate(),
2295
            [
2296
                [
2297
                    'message'   => 'Interface field AnotherInterface.field expects type String but ' .
2298
                        'AnotherObject.field is type Int.',
2299
                    'locations' => [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 28]],
2300
                ],
2301
                [
2302
                    'message'   => 'Interface field argument AnotherInterface.field(input:) expects ' .
2303
                        'type String but AnotherObject.field(input:) is type Int.',
2304
                    'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]],
2305
                ],
2306
            ]
2307
        );
2308
    }
2309
2310
    /**
2311
     * @see it('rejects an Object which implements an Interface field along with additional required arguments')
2312
     */
2313
    public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() : void
2314
    {
2315
        $schema = BuildSchema::build('
2316
      type Query {
2317
        test: AnotherObject
2318
      }
2319
2320
      interface AnotherInterface {
2321
        field(input: String): String
2322
      }
2323
2324
      type AnotherObject implements AnotherInterface {
2325
        field(input: String, anotherInput: String!): String
2326
      }
2327
        ');
2328
2329
        $this->assertMatchesValidationMessage(
2330
            $schema->validate(),
2331
            [[
2332
                'message'   => 'Object field argument AnotherObject.field(anotherInput:) is of ' .
2333
                    'required type String! but is not also provided by the Interface ' .
2334
                    'field AnotherInterface.field.',
2335
                'locations' => [['line' => 11, 'column' => 44], ['line' => 7, 'column' => 9]],
2336
            ],
2337
            ]
2338
        );
2339
    }
2340
2341
    /**
2342
     * @see it('accepts an Object with an equivalently wrapped Interface field type')
2343
     */
2344
    public function testAcceptsAnObjectWithAnEquivalentlyWrappedInterfaceFieldType() : void
2345
    {
2346
        $schema = BuildSchema::build('
2347
      type Query {
2348
        test: AnotherObject
2349
      }
2350
2351
      interface AnotherInterface {
2352
        field: [String]!
2353
      }
2354
2355
      type AnotherObject implements AnotherInterface {
2356
        field: [String]!
2357
      }
2358
        ');
2359
2360
        self::assertEquals([], $schema->validate());
2361
    }
2362
2363
    /**
2364
     * @see it('rejects an Object with a non-list Interface field list type')
2365
     */
2366
    public function testRejectsAnObjectWithANonListInterfaceFieldListType() : void
2367
    {
2368
        $schema = BuildSchema::build('
2369
      type Query {
2370
        test: AnotherObject
2371
      }
2372
2373
      interface AnotherInterface {
2374
        field: [String]
2375
      }
2376
2377
      type AnotherObject implements AnotherInterface {
2378
        field: String
2379
      }
2380
        ');
2381
2382
        $this->assertMatchesValidationMessage(
2383
            $schema->validate(),
2384
            [[
2385
                'message'   => 'Interface field AnotherInterface.field expects type [String] ' .
2386
                    'but AnotherObject.field is type String.',
2387
                'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]],
2388
            ],
2389
            ]
2390
        );
2391
    }
2392
2393
    /**
2394
     * @see it('rejects an Object with a list Interface field non-list type')
2395
     */
2396
    public function testRejectsAnObjectWithAListInterfaceFieldNonListType() : void
2397
    {
2398
        $schema = BuildSchema::build('
2399
      type Query {
2400
        test: AnotherObject
2401
      }
2402
2403
      interface AnotherInterface {
2404
        field: String
2405
      }
2406
2407
      type AnotherObject implements AnotherInterface {
2408
        field: [String]
2409
      }
2410
        ');
2411
2412
        $this->assertMatchesValidationMessage(
2413
            $schema->validate(),
2414
            [[
2415
                'message'   => 'Interface field AnotherInterface.field expects type String but ' .
2416
                    'AnotherObject.field is type [String].',
2417
                'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]],
2418
            ],
2419
            ]
2420
        );
2421
    }
2422
2423
    /**
2424
     * @see it('accepts an Object with a subset non-null Interface field type')
2425
     */
2426
    public function testAcceptsAnObjectWithASubsetNonNullInterfaceFieldType() : void
2427
    {
2428
        $schema = BuildSchema::build('
2429
      type Query {
2430
        test: AnotherObject
2431
      }
2432
2433
      interface AnotherInterface {
2434
        field: String
2435
      }
2436
2437
      type AnotherObject implements AnotherInterface {
2438
        field: String!
2439
      }
2440
        ');
2441
2442
        self::assertEquals([], $schema->validate());
2443
    }
2444
2445
    /**
2446
     * @see it('rejects an Object with a superset nullable Interface field type')
2447
     */
2448
    public function testRejectsAnObjectWithASupersetNullableInterfaceFieldType() : void
2449
    {
2450
        $schema = BuildSchema::build('
2451
      type Query {
2452
        test: AnotherObject
2453
      }
2454
2455
      interface AnotherInterface {
2456
        field: String!
2457
      }
2458
2459
      type AnotherObject implements AnotherInterface {
2460
        field: String
2461
      }
2462
        ');
2463
2464
        $this->assertMatchesValidationMessage(
2465
            $schema->validate(),
2466
            [[
2467
                'message'   => 'Interface field AnotherInterface.field expects type String! ' .
2468
                    'but AnotherObject.field is type String.',
2469
                'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]],
2470
            ],
2471
            ]
2472
        );
2473
    }
2474
2475
    public function testRejectsDifferentInstancesOfTheSameType() : void
2476
    {
2477
        // Invalid: always creates new instance vs returning one from registry
2478
        $typeLoader = static function ($name) {
2479
            switch ($name) {
2480
                case 'Query':
2481
                    return new ObjectType([
2482
                        'name'   => 'Query',
2483
                        'fields' => [
2484
                            'test' => Type::string(),
2485
                        ],
2486
                    ]);
2487
                default:
2488
                    return null;
2489
            }
2490
        };
2491
2492
        $schema = new Schema([
2493
            'query'      => $typeLoader('Query'),
2494
            'typeLoader' => $typeLoader,
2495
        ]);
2496
        $this->expectException(InvariantViolation::class);
2497
        $this->expectExceptionMessage(
2498
            'Type loader returns different instance for Query than field/argument definitions. ' .
2499
            'Make sure you always return the same instance for the same type name.'
2500
        );
2501
        $schema->assertValid();
2502
    }
2503
}
2504