Passed
Push — master ( 7cc72b...baad32 )
by Vladimir
10:39
created

ValidationTest::testRejectsAnEmptyFieldArgType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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