Passed
Push — master ( d8d032...bdbb30 )
by Vladimir
05:55
created

ValidationTest::assertMatchesValidationMessage()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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