Failed Conditions
Push — master ( bf4e7d...c70528 )
by Vladimir
09:31
created

testAcceptsAnInputObjectTypeWithFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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