Passed
Push — master ( cd3856...1a1bf1 )
by Vladimir
04:11
created

testCanBeUsedForLimitedExecution()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 12
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\GraphQL;
9
use GraphQL\Language\AST\DocumentNode;
10
use GraphQL\Language\AST\Node;
11
use GraphQL\Language\AST\NodeList;
12
use GraphQL\Language\DirectiveLocation;
13
use GraphQL\Language\Parser;
14
use GraphQL\Language\Printer;
15
use GraphQL\Type\Definition\CustomScalarType;
16
use GraphQL\Type\Definition\Directive;
17
use GraphQL\Type\Definition\EnumType;
18
use GraphQL\Type\Definition\FieldArgument;
19
use GraphQL\Type\Definition\InputObjectType;
20
use GraphQL\Type\Definition\InterfaceType;
21
use GraphQL\Type\Definition\NonNull;
22
use GraphQL\Type\Definition\ObjectType;
23
use GraphQL\Type\Definition\ScalarType;
24
use GraphQL\Type\Definition\Type;
25
use GraphQL\Type\Definition\UnionType;
26
use GraphQL\Type\Schema;
27
use GraphQL\Utils\SchemaExtender;
28
use GraphQL\Utils\SchemaPrinter;
29
use PHPUnit\Framework\TestCase;
30
use function array_filter;
31
use function array_map;
32
use function array_merge;
33
use function array_values;
34
use function count;
35
use function implode;
36
use function in_array;
37
use function iterator_to_array;
38
use function preg_match;
39
use function preg_replace;
40
use function trim;
41
42
class SchemaExtenderTest extends TestCase
43
{
44
    /** @var Schema */
45
    protected $testSchema;
46
47
    /** @var string[] */
48
    protected $testSchemaDefinitions;
49
50
    /** @var ObjectType */
51
    protected $FooType;
52
53
    /** @var Directive */
54
    protected $FooDirective;
55
56
    public function setUp()
57
    {
58
        parent::setUp();
59
60
        $SomeScalarType = new CustomScalarType([
61
            'name' => 'SomeScalar',
62
            'serialize' => static function ($x) {
63
                return $x;
64
            },
65
        ]);
66
67
        $SomeInterfaceType = new InterfaceType([
68
            'name' => 'SomeInterface',
69
            'fields' => static function () use (&$SomeInterfaceType) {
70
                return [
71
                    'name' => [ 'type' => Type::string()],
72
                    'some' => [ 'type' => $SomeInterfaceType],
73
                ];
74
            },
75
        ]);
76
77
        $FooType = new ObjectType([
78
            'name' => 'Foo',
79
            'interfaces' => [$SomeInterfaceType],
80
            'fields' => static function () use ($SomeInterfaceType, &$FooType) {
81
                return [
82
                    'name' => [ 'type' => Type::string() ],
83
                    'some' => [ 'type' => $SomeInterfaceType ],
84
                    'tree' => [ 'type' => Type::nonNull(Type::listOf($FooType))],
85
                ];
86
            },
87
        ]);
88
89
        $BarType = new ObjectType([
90
            'name' => 'Bar',
91
            'interfaces' => [$SomeInterfaceType],
92
            'fields' => static function () use ($SomeInterfaceType, $FooType) : array {
93
                return [
94
                    'name' => [ 'type' => Type::string() ],
95
                    'some' => [ 'type' => $SomeInterfaceType ],
96
                    'foo' => [ 'type' => $FooType ],
97
                ];
98
            },
99
        ]);
100
101
        $BizType = new ObjectType([
102
            'name' => 'Biz',
103
            'fields' => static function () : array {
104
                return [
105
                    'fizz' => [ 'type' => Type::string() ],
106
                ];
107
            },
108
        ]);
109
110
        $SomeUnionType = new UnionType([
111
            'name' => 'SomeUnion',
112
            'types' => [$FooType, $BizType],
113
        ]);
114
115
        $SomeEnumType = new EnumType([
116
            'name' => 'SomeEnum',
117
            'values' => [
118
                'ONE' => [ 'value' => 1 ],
119
                'TWO' => [ 'value' => 2 ],
120
            ],
121
        ]);
122
123
        $SomeInputType = new InputObjectType([
124
            'name' => 'SomeInput',
125
            'fields' => static function () : array {
126
                return [
127
                    'fooArg' => [ 'type' => Type::string() ],
128
                ];
129
            },
130
        ]);
131
132
        $FooDirective = new Directive([
133
            'name' => 'foo',
134
            'args' => [
135
                new FieldArgument([
136
                    'name' => 'input',
137
                    'type' => $SomeInputType,
138
                ]),
139
            ],
140
            'locations' => [
141
                DirectiveLocation::SCHEMA,
142
                DirectiveLocation::SCALAR,
143
                DirectiveLocation::OBJECT,
144
                DirectiveLocation::FIELD_DEFINITION,
145
                DirectiveLocation::ARGUMENT_DEFINITION,
146
                DirectiveLocation::IFACE,
147
                DirectiveLocation::UNION,
148
                DirectiveLocation::ENUM,
149
                DirectiveLocation::ENUM_VALUE,
150
                DirectiveLocation::INPUT_OBJECT,
151
                DirectiveLocation::INPUT_FIELD_DEFINITION,
152
            ],
153
        ]);
154
155
        $this->testSchema = new Schema([
156
            'query' => new ObjectType([
157
                'name' => 'Query',
158
                'fields' => static function () use ($FooType, $SomeScalarType, $SomeUnionType, $SomeEnumType, $SomeInterfaceType, $SomeInputType) : array {
159
                    return [
160
                        'foo' => [ 'type' => $FooType ],
161
                        'someScalar' => [ 'type' => $SomeScalarType ],
162
                        'someUnion' => [ 'type' => $SomeUnionType ],
163
                        'someEnum' => [ 'type' => $SomeEnumType ],
164
                        'someInterface' => [
165
                            'args' => [
166
                                'id' => [
167
                                    'type' => Type::nonNull(Type::id()),
168
                                ],
169
                            ],
170
                            'type' => $SomeInterfaceType,
171
                        ],
172
                        'someInput' => [
173
                            'args' => [ 'input' => [ 'type' => $SomeInputType ] ],
174
                            'type' => Type::string(),
175
                        ],
176
                    ];
177
                },
178
            ]),
179
            'types' => [$FooType, $BarType],
180
            'directives' => array_merge(GraphQL::getStandardDirectives(), [$FooDirective]),
181
        ]);
182
183
        $testSchemaAst = Parser::parse(SchemaPrinter::doPrint($this->testSchema));
184
185
        $this->testSchemaDefinitions = array_map(static function ($node) {
186
            return Printer::doPrint($node);
187
        }, iterator_to_array($testSchemaAst->definitions->getIterator()));
188
189
        $this->FooDirective = $FooDirective;
190
        $this->FooType      = $FooType;
191
    }
192
193
    protected function dedent(string $str) : string
194
    {
195
        $trimmedStr = trim($str, "\n");
196
        $trimmedStr = preg_replace('/[ \t]*$/', '', $trimmedStr);
197
198
        preg_match('/^[ \t]*/', $trimmedStr, $indentMatch);
199
        $indent = $indentMatch[0];
200
        return preg_replace('/^' . $indent . '/m', '', $trimmedStr);
201
    }
202
203
    /**
204
     * @param mixed[]|null $options
205
     */
206
    protected function extendTestSchema(string $sdl, ?array $options = null) : Schema
207
    {
208
        $originalPrint  = SchemaPrinter::doPrint($this->testSchema);
209
        $ast            = Parser::parse($sdl);
210
        $extendedSchema = SchemaExtender::extend($this->testSchema, $ast, $options);
211
        self::assertEquals(SchemaPrinter::doPrint($this->testSchema), $originalPrint);
212
        return $extendedSchema;
213
    }
214
215
    protected function printTestSchemaChanges(Schema $extendedSchema) : string
216
    {
217
        $ast              = Parser::parse(SchemaPrinter::doPrint($extendedSchema));
218
        $ast->definitions = array_values(array_filter(
219
            $ast->definitions instanceof NodeList ? iterator_to_array($ast->definitions->getIterator()) : $ast->definitions,
220
            function (Node $node) : bool {
221
                return ! in_array(Printer::doPrint($node), $this->testSchemaDefinitions, true);
222
            }
223
        ));
224
225
        return Printer::doPrint($ast);
226
    }
227
228
    /**
229
     * @see it('returns the original schema when there are no type definitions')
230
     */
231
    public function testReturnsTheOriginalSchemaWhenThereAreNoTypeDefinitions()
232
    {
233
        $extendedSchema = $this->extendTestSchema('{ field }');
234
        self::assertEquals($extendedSchema, $this->testSchema);
235
    }
236
237
    /**
238
     * @see it('extends without altering original schema')
239
     */
240
    public function testExtendsWithoutAlteringOriginalSchema()
241
    {
242
        $extendedSchema = $this->extendTestSchema('
243
            extend type Query {
244
                newField: String
245
            }');
246
        self::assertNotEquals($extendedSchema, $this->testSchema);
247
        self::assertContains('newField', SchemaPrinter::doPrint($extendedSchema));
248
        self::assertNotContains('newField', SchemaPrinter::doPrint($this->testSchema));
249
    }
250
251
    /**
252
     * @see it('can be used for limited execution')
253
     */
254
    public function testCanBeUsedForLimitedExecution()
255
    {
256
        $extendedSchema = $this->extendTestSchema('
257
          extend type Query {
258
            newField: String
259
          }
260
        ');
261
262
        $result = GraphQL::executeQuery($extendedSchema, '{ newField }', ['newField' => 123]);
263
264
        self::assertEquals($result->toArray(), [
265
            'data' => ['newField' => '123'],
266
        ]);
267
    }
268
269
    /**
270
     * @see it('can describe the extended fields')
271
     */
272
    public function testCanDescribeTheExtendedFields()
273
    {
274
        $extendedSchema = $this->extendTestSchema('
275
            extend type Query {
276
                "New field description."
277
                newField: String
278
            }
279
        ');
280
281
        self::assertEquals(
282
            $extendedSchema->getQueryType()->getField('newField')->description,
283
            'New field description.'
284
        );
285
    }
286
287
    /**
288
     * @see it('can describe the extended fields with legacy comments')
289
     */
290
    public function testCanDescribeTheExtendedFieldsWithLegacyComments()
291
    {
292
        $extendedSchema = $this->extendTestSchema('
293
            extend type Query {
294
                # New field description.
295
                newField: String
296
            }
297
        ', ['commentDescriptions' => true]);
298
299
        self::assertEquals(
300
            $extendedSchema->getQueryType()->getField('newField')->description,
301
            'New field description.'
302
        );
303
    }
304
305
    /**
306
     * @see it('describes extended fields with strings when present')
307
     */
308
    public function testDescribesExtendedFieldsWithStringsWhenPresent()
309
    {
310
        $extendedSchema = $this->extendTestSchema('
311
            extend type Query {
312
                # New field description.
313
                "Actually use this description."
314
                newField: String
315
            }
316
        ', ['commentDescriptions' => true ]);
317
318
        self::assertEquals(
319
            $extendedSchema->getQueryType()->getField('newField')->description,
320
            'Actually use this description.'
321
        );
322
    }
323
324
    /**
325
     * @see it('extends objects by adding new fields')
326
     */
327
    public function testExtendsObjectsByAddingNewFields()
328
    {
329
        $extendedSchema = $this->extendTestSchema(
330
            '
331
            extend type Foo {
332
                newField: String
333
            }
334
        '
335
        );
336
337
        self::assertEquals(
338
            $this->printTestSchemaChanges($extendedSchema),
339
            $this->dedent('
340
              type Foo implements SomeInterface {
341
                name: String
342
                some: SomeInterface
343
                tree: [Foo]!
344
                newField: String
345
              }
346
            ')
347
        );
348
349
        $fooType  = $extendedSchema->getType('Foo');
350
        $fooField = $extendedSchema->getQueryType()->getField('foo');
351
        self::assertEquals($fooField->getType(), $fooType);
352
    }
353
354
    /**
355
     * @see it('extends enums by adding new values')
356
     */
357
    public function testExtendsEnumsByAddingNewValues()
358
    {
359
        $extendedSchema = $this->extendTestSchema('
360
          extend enum SomeEnum {
361
            NEW_ENUM
362
          }
363
        ');
364
365
        self::assertEquals(
366
            $this->printTestSchemaChanges($extendedSchema),
367
            $this->dedent('
368
              enum SomeEnum {
369
                ONE
370
                TWO
371
                NEW_ENUM
372
              }
373
          ')
374
        );
375
376
        $someEnumType = $extendedSchema->getType('SomeEnum');
377
        $enumField    = $extendedSchema->getQueryType()->getField('someEnum');
378
        self::assertEquals($enumField->getType(), $someEnumType);
379
    }
380
381
    /**
382
     * @see it('extends unions by adding new types')
383
     */
384
    public function testExtendsUnionsByAddingNewTypes()
385
    {
386
        $extendedSchema = $this->extendTestSchema('
387
          extend union SomeUnion = Bar
388
        ');
389
        self::assertEquals(
390
            $this->printTestSchemaChanges($extendedSchema),
391
            $this->dedent('
392
              union SomeUnion = Foo | Biz | Bar
393
            ')
394
        );
395
396
        $someUnionType = $extendedSchema->getType('SomeUnion');
397
        $unionField    = $extendedSchema->getQueryType()->getField('someUnion');
398
        self::assertEquals($unionField->getType(), $someUnionType);
399
    }
400
401
    /**
402
     * @see it('allows extension of union by adding itself')
403
     */
404
    public function testAllowsExtensionOfUnionByAddingItself()
405
    {
406
        $extendedSchema = $this->extendTestSchema('
407
          extend union SomeUnion = SomeUnion
408
        ');
409
410
        $errors = $extendedSchema->validate();
411
        self::assertGreaterThan(0, count($errors));
412
413
        self::assertEquals(
414
            $this->printTestSchemaChanges($extendedSchema),
415
            $this->dedent('
416
                union SomeUnion = Foo | Biz | SomeUnion
417
            ')
418
        );
419
    }
420
421
    /**
422
     * @see it('extends inputs by adding new fields')
423
     */
424
    public function testExtendsInputsByAddingNewFields()
425
    {
426
        $extendedSchema = $this->extendTestSchema('
427
          extend input SomeInput {
428
            newField: String
429
          }
430
        ');
431
432
        self::assertEquals(
433
            $this->printTestSchemaChanges($extendedSchema),
434
            $this->dedent('
435
              input SomeInput {
436
                fooArg: String
437
                newField: String
438
              }
439
            ')
440
        );
441
442
        $someInputType = $extendedSchema->getType('SomeInput');
443
        $inputField    = $extendedSchema->getQueryType()->getField('someInput');
444
        self::assertEquals($inputField->args[0]->getType(), $someInputType);
445
446
        $fooDirective = $extendedSchema->getDirective('foo');
447
        self::assertEquals($fooDirective->args[0]->getType(), $someInputType);
448
    }
449
450
    /**
451
     * @see it('extends scalars by adding new directives')
452
     */
453
    public function testExtendsScalarsByAddingNewDirectives()
454
    {
455
        $extendedSchema = $this->extendTestSchema('
456
          extend scalar SomeScalar @foo
457
        ');
458
459
        $someScalar = $extendedSchema->getType('SomeScalar');
460
        self::assertCount(1, $someScalar->extensionASTNodes);
461
        self::assertEquals(
462
            Printer::doPrint($someScalar->extensionASTNodes[0]),
463
            'extend scalar SomeScalar @foo'
464
        );
465
    }
466
467
    /**
468
     * @see it('correctly assign AST nodes to new and extended types')
469
     */
470
    public function testCorrectlyAssignASTNodesToNewAndExtendedTypes()
471
    {
472
        $extendedSchema = $this->extendTestSchema('
473
              extend type Query {
474
                newField(testArg: TestInput): TestEnum
475
              }
476
              extend scalar SomeScalar @foo
477
              extend enum SomeEnum {
478
                NEW_VALUE
479
              }
480
              extend union SomeUnion = Bar
481
              extend input SomeInput {
482
                newField: String
483
              }
484
              extend interface SomeInterface {
485
                newField: String
486
              }
487
              enum TestEnum {
488
                TEST_VALUE
489
              }
490
              input TestInput {
491
                testInputField: TestEnum
492
              }
493
            ');
494
495
        $ast = Parser::parse('
496
            extend type Query {
497
                oneMoreNewField: TestUnion
498
            }
499
            extend scalar SomeScalar @test
500
            extend enum SomeEnum {
501
                ONE_MORE_NEW_VALUE
502
            }
503
            extend union SomeUnion = TestType
504
            extend input SomeInput {
505
                oneMoreNewField: String
506
            }
507
            extend interface SomeInterface {
508
                oneMoreNewField: String
509
            }
510
            union TestUnion = TestType
511
            interface TestInterface {
512
                interfaceField: String
513
            }
514
            type TestType implements TestInterface {
515
                interfaceField: String
516
            }
517
            directive @test(arg: Int) on FIELD | SCALAR
518
        ');
519
520
        $extendedTwiceSchema = SchemaExtender::extend($extendedSchema, $ast);
521
        $query               = $extendedTwiceSchema->getQueryType();
522
        /** @var ScalarType $someScalar */
523
        $someScalar = $extendedTwiceSchema->getType('SomeScalar');
524
        /** @var EnumType $someEnum */
525
        $someEnum = $extendedTwiceSchema->getType('SomeEnum');
526
        /** @var UnionType $someUnion */
527
        $someUnion = $extendedTwiceSchema->getType('SomeUnion');
528
        /** @var InputObjectType $someInput */
529
        $someInput = $extendedTwiceSchema->getType('SomeInput');
530
        /** @var InterfaceType $someInterface */
531
        $someInterface = $extendedTwiceSchema->getType('SomeInterface');
532
533
        /** @var InputObjectType $testInput */
534
        $testInput = $extendedTwiceSchema->getType('TestInput');
535
        /** @var EnumType $testEnum */
536
        $testEnum = $extendedTwiceSchema->getType('TestEnum');
537
        /** @var UnionType $testUnion */
538
        $testUnion = $extendedTwiceSchema->getType('TestUnion');
539
        /** @var InterfaceType $testInterface */
540
        $testInterface = $extendedTwiceSchema->getType('TestInterface');
541
        /** @var ObjectType $testType */
542
        $testType = $extendedTwiceSchema->getType('TestType');
543
        /** @var Directive $testDirective */
544
        $testDirective = $extendedTwiceSchema->getDirective('test');
545
546
        self::assertCount(2, $query->extensionASTNodes);
547
        self::assertCount(2, $someScalar->extensionASTNodes);
548
        self::assertCount(2, $someEnum->extensionASTNodes);
549
        self::assertCount(2, $someUnion->extensionASTNodes);
550
        self::assertCount(2, $someInput->extensionASTNodes);
551
        self::assertCount(2, $someInterface->extensionASTNodes);
552
553
        self::assertCount(0, $testType->extensionASTNodes ?? []);
554
        self::assertCount(0, $testEnum->extensionASTNodes ?? []);
555
        self::assertCount(0, $testUnion->extensionASTNodes ?? []);
556
        self::assertCount(0, $testInput->extensionASTNodes ?? []);
557
        self::assertCount(0, $testInterface->extensionASTNodes ?? []);
558
559
        $restoredExtensionAST = new DocumentNode([
560
            'definitions' => array_merge(
561
                $query->extensionASTNodes,
562
                $someScalar->extensionASTNodes,
563
                $someEnum->extensionASTNodes,
564
                $someUnion->extensionASTNodes,
565
                $someInput->extensionASTNodes,
566
                $someInterface->extensionASTNodes,
567
                [
568
                    $testInput->astNode,
569
                    $testEnum->astNode,
570
                    $testUnion->astNode,
571
                    $testInterface->astNode,
572
                    $testType->astNode,
573
                    $testDirective->astNode,
574
                ]
575
            ),
576
        ]);
577
578
        self::assertEquals(
579
            SchemaPrinter::doPrint(SchemaExtender::extend($this->testSchema, $restoredExtensionAST)),
580
            SchemaPrinter::doPrint($extendedTwiceSchema)
581
        );
582
583
        $newField = $query->getField('newField');
584
585
        self::assertEquals(Printer::doPrint($newField->astNode), 'newField(testArg: TestInput): TestEnum');
586
        self::assertEquals(Printer::doPrint($newField->args[0]->astNode), 'testArg: TestInput');
587
        self::assertEquals(Printer::doPrint($query->getField('oneMoreNewField')->astNode), 'oneMoreNewField: TestUnion');
588
        self::assertEquals(Printer::doPrint($someEnum->getValue('NEW_VALUE')->astNode), 'NEW_VALUE');
589
        self::assertEquals(Printer::doPrint($someEnum->getValue('ONE_MORE_NEW_VALUE')->astNode), 'ONE_MORE_NEW_VALUE');
590
        self::assertEquals(Printer::doPrint($someInput->getField('newField')->astNode), 'newField: String');
591
        self::assertEquals(Printer::doPrint($someInput->getField('oneMoreNewField')->astNode), 'oneMoreNewField: String');
592
        self::assertEquals(Printer::doPrint($someInterface->getField('newField')->astNode), 'newField: String');
593
        self::assertEquals(Printer::doPrint($someInterface->getField('oneMoreNewField')->astNode), 'oneMoreNewField: String');
594
        self::assertEquals(Printer::doPrint($testInput->getField('testInputField')->astNode), 'testInputField: TestEnum');
595
        self::assertEquals(Printer::doPrint($testEnum->getValue('TEST_VALUE')->astNode), 'TEST_VALUE');
596
        self::assertEquals(Printer::doPrint($testInterface->getField('interfaceField')->astNode), 'interfaceField: String');
597
        self::assertEquals(Printer::doPrint($testType->getField('interfaceField')->astNode), 'interfaceField: String');
598
        self::assertEquals(Printer::doPrint($testDirective->args[0]->astNode), 'arg: Int');
599
    }
600
601
    /**
602
     * @see it('builds types with deprecated fields/values')
603
     */
604
    public function testBuildsTypesWithDeprecatedFieldsOrValues()
605
    {
606
        $extendedSchema = $this->extendTestSchema('
607
            type TypeWithDeprecatedField {
608
                newDeprecatedField: String @deprecated(reason: "not used anymore")
609
            }
610
            enum EnumWithDeprecatedValue {
611
                DEPRECATED @deprecated(reason: "do not use")
612
            }
613
        ');
614
615
        /** @var ObjectType $typeWithDeprecatedField */
616
        $typeWithDeprecatedField = $extendedSchema->getType('TypeWithDeprecatedField');
617
        $deprecatedFieldDef      = $typeWithDeprecatedField->getField('newDeprecatedField');
618
619
        self::assertEquals(true, $deprecatedFieldDef->isDeprecated());
620
        self::assertEquals('not used anymore', $deprecatedFieldDef->deprecationReason);
621
622
        /** @var EnumType $enumWithDeprecatedValue */
623
        $enumWithDeprecatedValue = $extendedSchema->getType('EnumWithDeprecatedValue');
624
        $deprecatedEnumDef       = $enumWithDeprecatedValue->getValue('DEPRECATED');
625
626
        self::assertEquals(true, $deprecatedEnumDef->isDeprecated());
627
        self::assertEquals('do not use', $deprecatedEnumDef->deprecationReason);
628
    }
629
630
    /**
631
     * @see it('extends objects with deprecated fields')
632
     */
633
    public function testExtendsObjectsWithDeprecatedFields()
634
    {
635
        $extendedSchema = $this->extendTestSchema('
636
          extend type Foo {
637
            deprecatedField: String @deprecated(reason: "not used anymore")
638
          }
639
        ');
640
        /** @var ObjectType $fooType */
641
        $fooType            = $extendedSchema->getType('Foo');
642
        $deprecatedFieldDef = $fooType->getField('deprecatedField');
643
644
        self::assertTrue($deprecatedFieldDef->isDeprecated());
645
        self::assertEquals('not used anymore', $deprecatedFieldDef->deprecationReason);
646
    }
647
648
    /**
649
     * @see it('extends enums with deprecated values')
650
     */
651
    public function testExtendsEnumsWithDeprecatedValues()
652
    {
653
        $extendedSchema = $this->extendTestSchema('
654
          extend enum SomeEnum {
655
            DEPRECATED @deprecated(reason: "do not use")
656
          }
657
        ');
658
659
        /** @var EnumType $someEnumType */
660
        $someEnumType      = $extendedSchema->getType('SomeEnum');
661
        $deprecatedEnumDef = $someEnumType->getValue('DEPRECATED');
662
663
        self::assertTrue($deprecatedEnumDef->isDeprecated());
664
        self::assertEquals('do not use', $deprecatedEnumDef->deprecationReason);
665
    }
666
667
    /**
668
     * @see it('adds new unused object type')
669
     */
670
    public function testAddsNewUnusedObjectType()
671
    {
672
        $extendedSchema = $this->extendTestSchema('
673
          type Unused {
674
            someField: String
675
          }
676
        ');
677
        self::assertNotEquals($this->testSchema, $extendedSchema);
678
        self::assertEquals(
679
            $this->dedent('
680
                type Unused {
681
                  someField: String
682
                }
683
            '),
684
            $this->printTestSchemaChanges($extendedSchema)
685
        );
686
    }
687
688
    /**
689
     * @see it('adds new unused enum type')
690
     */
691
    public function testAddsNewUnusedEnumType()
692
    {
693
        $extendedSchema = $this->extendTestSchema('
694
          enum UnusedEnum {
695
            SOME
696
          }
697
        ');
698
        self::assertNotEquals($extendedSchema, $this->testSchema);
699
        self::assertEquals(
700
            $this->dedent('
701
                enum UnusedEnum {
702
                  SOME
703
                }
704
            '),
705
            $this->printTestSchemaChanges($extendedSchema)
706
        );
707
    }
708
709
    /**
710
     * @see it('adds new unused input object type')
711
     */
712
    public function testAddsNewUnusedInputObjectType()
713
    {
714
        $extendedSchema = $this->extendTestSchema('
715
          input UnusedInput {
716
            someInput: String
717
          }
718
        ');
719
720
        self::assertNotEquals($extendedSchema, $this->testSchema);
721
        self::assertEquals(
722
            $this->dedent('
723
              input UnusedInput {
724
                someInput: String
725
              }
726
            '),
727
            $this->printTestSchemaChanges($extendedSchema)
728
        );
729
    }
730
731
    /**
732
     * @see it('adds new union using new object type')
733
     */
734
    public function testAddsNewUnionUsingNewObjectType()
735
    {
736
        $extendedSchema = $this->extendTestSchema('
737
          type DummyUnionMember {
738
            someField: String
739
          }
740
741
          union UnusedUnion = DummyUnionMember
742
        ');
743
744
        self::assertNotEquals($extendedSchema, $this->testSchema);
745
        self::assertEquals(
746
            $this->dedent('
747
              type DummyUnionMember {
748
                someField: String
749
              }
750
751
              union UnusedUnion = DummyUnionMember
752
            '),
753
            $this->printTestSchemaChanges($extendedSchema)
754
        );
755
    }
756
757
    /**
758
     * @see it('extends objects by adding new fields with arguments')
759
     */
760
    public function testExtendsObjectsByAddingNewFieldsWithArguments()
761
    {
762
        $extendedSchema = $this->extendTestSchema('
763
          extend type Foo {
764
            newField(arg1: String, arg2: NewInputObj!): String
765
          }
766
767
          input NewInputObj {
768
            field1: Int
769
            field2: [Float]
770
            field3: String!
771
          }
772
        ');
773
774
        self::assertEquals(
775
            $this->dedent('
776
                type Foo implements SomeInterface {
777
                  name: String
778
                  some: SomeInterface
779
                  tree: [Foo]!
780
                  newField(arg1: String, arg2: NewInputObj!): String
781
                }
782
783
                input NewInputObj {
784
                  field1: Int
785
                  field2: [Float]
786
                  field3: String!
787
                }
788
            '),
789
            $this->printTestSchemaChanges($extendedSchema)
790
        );
791
    }
792
793
    /**
794
     * @see it('extends objects by adding new fields with existing types')
795
     */
796
    public function testExtendsObjectsByAddingNewFieldsWithExistingTypes()
797
    {
798
        $extendedSchema = $this->extendTestSchema('
799
          extend type Foo {
800
            newField(arg1: SomeEnum!): SomeEnum
801
          }
802
        ');
803
804
        self::assertEquals(
805
            $this->dedent('
806
                type Foo implements SomeInterface {
807
                  name: String
808
                  some: SomeInterface
809
                  tree: [Foo]!
810
                  newField(arg1: SomeEnum!): SomeEnum
811
                }
812
            '),
813
            $this->printTestSchemaChanges($extendedSchema)
814
        );
815
    }
816
817
    /**
818
     * @see it('extends objects by adding implemented interfaces')
819
     */
820
    public function testExtendsObjectsByAddingImplementedInterfaces()
821
    {
822
        $extendedSchema = $this->extendTestSchema('
823
          extend type Biz implements SomeInterface {
824
            name: String
825
            some: SomeInterface
826
          }
827
        ');
828
829
        self::assertEquals(
830
            $this->dedent('
831
                type Biz implements SomeInterface {
832
                  fizz: String
833
                  name: String
834
                  some: SomeInterface
835
                }
836
            '),
837
            $this->printTestSchemaChanges($extendedSchema)
838
        );
839
    }
840
841
    /**
842
     * @see it('extends objects by including new types')
843
     */
844
    public function testExtendsObjectsByIncludingNewTypes()
845
    {
846
        $extendedSchema = $this->extendTestSchema('
847
          extend type Foo {
848
            newObject: NewObject
849
            newInterface: NewInterface
850
            newUnion: NewUnion
851
            newScalar: NewScalar
852
            newEnum: NewEnum
853
            newTree: [Foo]!
854
          }
855
856
          type NewObject implements NewInterface {
857
            baz: String
858
          }
859
860
          type NewOtherObject {
861
            fizz: Int
862
          }
863
864
          interface NewInterface {
865
            baz: String
866
          }
867
868
          union NewUnion = NewObject | NewOtherObject
869
870
          scalar NewScalar
871
872
          enum NewEnum {
873
            OPTION_A
874
            OPTION_B
875
          }
876
        ');
877
878
        self::assertEquals(
879
            $this->dedent('
880
                type Foo implements SomeInterface {
881
                  name: String
882
                  some: SomeInterface
883
                  tree: [Foo]!
884
                  newObject: NewObject
885
                  newInterface: NewInterface
886
                  newUnion: NewUnion
887
                  newScalar: NewScalar
888
                  newEnum: NewEnum
889
                  newTree: [Foo]!
890
                }
891
892
                enum NewEnum {
893
                  OPTION_A
894
                  OPTION_B
895
                }
896
897
                interface NewInterface {
898
                  baz: String
899
                }
900
901
                type NewObject implements NewInterface {
902
                  baz: String
903
                }
904
905
                type NewOtherObject {
906
                  fizz: Int
907
                }
908
909
                scalar NewScalar
910
911
                union NewUnion = NewObject | NewOtherObject
912
            '),
913
            $this->printTestSchemaChanges($extendedSchema)
914
        );
915
    }
916
917
    /**
918
     * @see it('extends objects by adding implemented new interfaces')
919
     */
920
    public function testExtendsObjectsByAddingImplementedNewInterfaces()
921
    {
922
        $extendedSchema = $this->extendTestSchema('
923
          extend type Foo implements NewInterface {
924
            baz: String
925
          }
926
927
          interface NewInterface {
928
            baz: String
929
          }
930
        ');
931
932
        self::assertEquals(
933
            $this->dedent('
934
                type Foo implements SomeInterface & NewInterface {
935
                  name: String
936
                  some: SomeInterface
937
                  tree: [Foo]!
938
                  baz: String
939
                }
940
941
                interface NewInterface {
942
                  baz: String
943
                }
944
            '),
945
            $this->printTestSchemaChanges($extendedSchema)
946
        );
947
    }
948
949
    /**
950
     * @see it('extends different types multiple times')
951
     */
952
    public function testExtendsDifferentTypesMultipleTimes()
953
    {
954
        $extendedSchema = $this->extendTestSchema('
955
          extend type Biz implements NewInterface {
956
            buzz: String
957
          }
958
959
          extend type Biz implements SomeInterface {
960
            name: String
961
            some: SomeInterface
962
            newFieldA: Int
963
          }
964
965
          extend type Biz {
966
            newFieldA: Int
967
            newFieldB: Float
968
          }
969
970
          interface NewInterface {
971
            buzz: String
972
          }
973
974
          extend enum SomeEnum {
975
            THREE
976
          }
977
978
          extend enum SomeEnum {
979
            FOUR
980
          }
981
982
          extend union SomeUnion = Boo
983
984
          extend union SomeUnion = Joo
985
986
          type Boo {
987
            fieldA: String
988
          }
989
990
          type Joo {
991
            fieldB: String
992
          }
993
994
          extend input SomeInput {
995
            fieldA: String
996
          }
997
998
          extend input SomeInput {
999
            fieldB: String
1000
          }
1001
        ');
1002
1003
        self::assertEquals(
1004
            $this->dedent('
1005
                type Biz implements NewInterface & SomeInterface {
1006
                  fizz: String
1007
                  buzz: String
1008
                  name: String
1009
                  some: SomeInterface
1010
                  newFieldA: Int
1011
                  newFieldB: Float
1012
                }
1013
1014
                type Boo {
1015
                  fieldA: String
1016
                }
1017
1018
                type Joo {
1019
                  fieldB: String
1020
                }
1021
1022
                interface NewInterface {
1023
                  buzz: String
1024
                }
1025
1026
                enum SomeEnum {
1027
                  ONE
1028
                  TWO
1029
                  THREE
1030
                  FOUR
1031
                }
1032
1033
                input SomeInput {
1034
                  fooArg: String
1035
                  fieldA: String
1036
                  fieldB: String
1037
                }
1038
1039
                union SomeUnion = Foo | Biz | Boo | Joo
1040
            '),
1041
            $this->printTestSchemaChanges($extendedSchema)
1042
        );
1043
    }
1044
1045
    /**
1046
     * @see it('extends interfaces by adding new fields')
1047
     */
1048
    public function testExtendsInterfacesByAddingNewFields()
1049
    {
1050
        $extendedSchema = $this->extendTestSchema('
1051
          extend interface SomeInterface {
1052
            newField: String
1053
          }
1054
1055
          extend type Bar {
1056
            newField: String
1057
          }
1058
1059
          extend type Foo {
1060
            newField: String
1061
          }
1062
        ');
1063
1064
        self::assertEquals(
1065
            $this->dedent('
1066
                type Bar implements SomeInterface {
1067
                  name: String
1068
                  some: SomeInterface
1069
                  foo: Foo
1070
                  newField: String
1071
                }
1072
1073
                type Foo implements SomeInterface {
1074
                  name: String
1075
                  some: SomeInterface
1076
                  tree: [Foo]!
1077
                  newField: String
1078
                }
1079
1080
                interface SomeInterface {
1081
                  name: String
1082
                  some: SomeInterface
1083
                  newField: String
1084
                }
1085
            '),
1086
            $this->printTestSchemaChanges($extendedSchema)
1087
        );
1088
    }
1089
1090
    /**
1091
     * @see it('allows extension of interface with missing Object fields')
1092
     */
1093
    public function testAllowsExtensionOfInterfaceWithMissingObjectFields()
1094
    {
1095
        $extendedSchema = $this->extendTestSchema('
1096
          extend interface SomeInterface {
1097
            newField: String
1098
          }
1099
        ');
1100
1101
        $errors = $extendedSchema->validate();
1102
        self::assertGreaterThan(0, $errors);
1103
1104
        self::assertEquals(
1105
            $this->dedent('
1106
                interface SomeInterface {
1107
                  name: String
1108
                  some: SomeInterface
1109
                  newField: String
1110
                }
1111
            '),
1112
            $this->printTestSchemaChanges($extendedSchema)
1113
        );
1114
    }
1115
1116
    /**
1117
     * @see it('extends interfaces multiple times')
1118
     */
1119
    public function testExtendsInterfacesMultipleTimes()
1120
    {
1121
        $extendedSchema = $this->extendTestSchema('
1122
          extend interface SomeInterface {
1123
            newFieldA: Int
1124
          }
1125
          extend interface SomeInterface {
1126
            newFieldB(test: Boolean): String
1127
          }
1128
        ');
1129
1130
        self::assertEquals(
1131
            $this->dedent('
1132
                interface SomeInterface {
1133
                  name: String
1134
                  some: SomeInterface
1135
                  newFieldA: Int
1136
                  newFieldB(test: Boolean): String
1137
                }
1138
            '),
1139
            $this->printTestSchemaChanges($extendedSchema)
1140
        );
1141
    }
1142
1143
    /**
1144
     * @see it('may extend mutations and subscriptions')
1145
     */
1146
    public function testMayExtendMutationsAndSubscriptions()
1147
    {
1148
        $mutationSchema = new Schema([
1149
            'query' => new ObjectType([
1150
                'name' => 'Query',
1151
                'fields' => static function () {
1152
                    return [ 'queryField' => [ 'type' => Type::string() ] ];
1153
                },
1154
            ]),
1155
            'mutation' => new ObjectType([
1156
                'name' => 'Mutation',
1157
                'fields' => static function () {
1158
                    return [ 'mutationField' => ['type' => Type::string() ] ];
1159
                },
1160
            ]),
1161
            'subscription' => new ObjectType([
1162
                'name' => 'Subscription',
1163
                'fields' => static function () {
1164
                    return ['subscriptionField' => ['type' => Type::string()]];
1165
                },
1166
            ]),
1167
        ]);
1168
1169
        $ast = Parser::parse('
1170
            extend type Query {
1171
              newQueryField: Int
1172
            }
1173
1174
            extend type Mutation {
1175
              newMutationField: Int
1176
            }
1177
1178
            extend type Subscription {
1179
              newSubscriptionField: Int
1180
            }
1181
        ');
1182
1183
        $originalPrint  = SchemaPrinter::doPrint($mutationSchema);
1184
        $extendedSchema = SchemaExtender::extend($mutationSchema, $ast);
1185
        self::assertNotEquals($mutationSchema, $extendedSchema);
1186
        self::assertEquals(SchemaPrinter::doPrint($mutationSchema), $originalPrint);
1187
        self::assertEquals(SchemaPrinter::doPrint($extendedSchema), $this->dedent('
1188
            type Mutation {
1189
              mutationField: String
1190
              newMutationField: Int
1191
            }
1192
1193
            type Query {
1194
              queryField: String
1195
              newQueryField: Int
1196
            }
1197
1198
            type Subscription {
1199
              subscriptionField: String
1200
              newSubscriptionField: Int
1201
            }
1202
        '));
1203
    }
1204
1205
    /**
1206
     * @see it('may extend directives with new simple directive')
1207
     */
1208
    public function testMayExtendDirectivesWithNewSimpleDirective()
1209
    {
1210
        $extendedSchema = $this->extendTestSchema('
1211
          directive @neat on QUERY
1212
        ');
1213
1214
        $newDirective = $extendedSchema->getDirective('neat');
1215
        self::assertEquals($newDirective->name, 'neat');
1216
        self::assertContains('QUERY', $newDirective->locations);
1217
    }
1218
1219
    /**
1220
     * @see it('sets correct description when extending with a new directive')
1221
     */
1222
    public function testSetsCorrectDescriptionWhenExtendingWithANewDirective()
1223
    {
1224
        $extendedSchema = $this->extendTestSchema('
1225
          """
1226
          new directive
1227
          """
1228
          directive @new on QUERY
1229
        ');
1230
1231
        $newDirective = $extendedSchema->getDirective('new');
1232
        self::assertEquals('new directive', $newDirective->description);
1233
    }
1234
1235
    /**
1236
     * @see it('sets correct description using legacy comments')
1237
     */
1238
    public function testSetsCorrectDescriptionUsingLegacyComments()
1239
    {
1240
        $extendedSchema = $this->extendTestSchema(
1241
            '
1242
          # new directive
1243
          directive @new on QUERY
1244
        ',
1245
            [ 'commentDescriptions' => true ]
1246
        );
1247
1248
        $newDirective = $extendedSchema->getDirective('new');
1249
        self::assertEquals('new directive', $newDirective->description);
1250
    }
1251
1252
1253
    /**
1254
     * @see it('may extend directives with new complex directive')
1255
     */
1256
    public function testMayExtendDirectivesWithNewComplexDirective()
1257
    {
1258
        $extendedSchema = $this->extendTestSchema('
1259
          directive @profile(enable: Boolean! tag: String) on QUERY | FIELD
1260
        ');
1261
1262
        $extendedDirective = $extendedSchema->getDirective('profile');
1263
        self::assertContains('QUERY', $extendedDirective->locations);
1264
        self::assertContains('FIELD', $extendedDirective->locations);
1265
1266
        $args = $extendedDirective->args;
1267
        self::assertCount(2, $args);
1268
1269
        $arg0 = $args[0];
1270
        $arg1 = $args[1];
1271
        /** @var NonNull $arg0Type */
1272
        $arg0Type = $arg0->getType();
1273
1274
        self::assertEquals('enable', $arg0->name);
1275
        self::assertTrue($arg0Type instanceof NonNull);
1276
        self::assertTrue($arg0Type->getWrappedType() instanceof ScalarType);
1277
1278
        self::assertEquals('tag', $arg1->name);
1279
        self::assertTrue($arg1->getType() instanceof ScalarType);
1280
    }
1281
1282
    /**
1283
     * @see it('Rejects invalid SDL')
1284
     */
1285
    public function testRejectsInvalidSDL()
1286
    {
1287
        $sdl = '
1288
            extend schema @unknown
1289
        ';
1290
1291
        try {
1292
            $this->extendTestSchema($sdl);
1293
            self::fail();
1294
        } catch (Error $error) {
1295
            self::assertEquals('Unknown directive "unknown".', $error->getMessage());
1296
        }
1297
    }
1298
1299
    /**
1300
     * @see it('Allows to disable SDL validation')
1301
     */
1302
    public function testAllowsToDisableSDLValidation()
1303
    {
1304
        $sdl = '
1305
          extend schema @unknown
1306
        ';
1307
1308
        $this->extendTestSchema($sdl, [ 'assumeValid' => true ]);
1309
        $this->extendTestSchema($sdl, [ 'assumeValidSDL' => true ]);
1310
    }
1311
1312
    /**
1313
     * @see it('does not allow replacing a default directive')
1314
     */
1315
    public function testDoesNotAllowReplacingADefaultDirective()
1316
    {
1317
        $sdl = '
1318
          directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD
1319
        ';
1320
1321
        try {
1322
            $this->extendTestSchema($sdl);
1323
            self::fail();
1324
        } catch (Error $error) {
1325
            self::assertEquals('Directive "include" already exists in the schema. It cannot be redefined.', $error->getMessage());
1326
        }
1327
    }
1328
1329
    /**
1330
     * @see it('does not allow replacing a custom directive')
1331
     */
1332
    public function testDoesNotAllowReplacingACustomDirective()
1333
    {
1334
        $extendedSchema = $this->extendTestSchema('
1335
          directive @meow(if: Boolean!) on FIELD | FRAGMENT_SPREAD
1336
        ');
1337
1338
        $replacementAST = Parser::parse('
1339
            directive @meow(if: Boolean!) on FIELD | QUERY
1340
        ');
1341
1342
        try {
1343
            SchemaExtender::extend($extendedSchema, $replacementAST);
1344
            self::fail();
1345
        } catch (Error $error) {
1346
            self::assertEquals('Directive "meow" already exists in the schema. It cannot be redefined.', $error->getMessage());
1347
        }
1348
    }
1349
1350
    /**
1351
     * @see it('does not allow replacing an existing type')
1352
     */
1353
    public function testDoesNotAllowReplacingAnExistingType()
1354
    {
1355
        $existingTypeError = static function ($type) {
1356
            return 'Type "' . $type . '" already exists in the schema. It cannot also be defined in this type definition.';
1357
        };
1358
1359
        $typeSDL = '
1360
            type Bar
1361
        ';
1362
1363
        try {
1364
            $this->extendTestSchema($typeSDL);
1365
            self::fail();
1366
        } catch (Error $error) {
1367
            self::assertEquals($existingTypeError('Bar'), $error->getMessage());
1368
        }
1369
1370
        $scalarSDL = '
1371
          scalar SomeScalar
1372
        ';
1373
1374
        try {
1375
            $this->extendTestSchema($scalarSDL);
1376
            self::fail();
1377
        } catch (Error $error) {
1378
            self::assertEquals($existingTypeError('SomeScalar'), $error->getMessage());
1379
        }
1380
1381
        $interfaceSDL = '
1382
          interface SomeInterface
1383
        ';
1384
1385
        try {
1386
            $this->extendTestSchema($interfaceSDL);
1387
            self::fail();
1388
        } catch (Error $error) {
1389
            self::assertEquals($existingTypeError('SomeInterface'), $error->getMessage());
1390
        }
1391
1392
        $enumSDL = '
1393
          enum SomeEnum
1394
        ';
1395
1396
        try {
1397
            $this->extendTestSchema($enumSDL);
1398
            self::fail();
1399
        } catch (Error $error) {
1400
            self::assertEquals($existingTypeError('SomeEnum'), $error->getMessage());
1401
        }
1402
1403
        $unionSDL = '
1404
          union SomeUnion
1405
        ';
1406
1407
        try {
1408
            $this->extendTestSchema($unionSDL);
1409
            self::fail();
1410
        } catch (Error $error) {
1411
            self::assertEquals($existingTypeError('SomeUnion'), $error->getMessage());
1412
        }
1413
1414
        $inputSDL = '
1415
          input SomeInput
1416
        ';
1417
1418
        try {
1419
            $this->extendTestSchema($inputSDL);
1420
            self::fail();
1421
        } catch (Error $error) {
1422
            self::assertEquals($existingTypeError('SomeInput'), $error->getMessage());
1423
        }
1424
    }
1425
1426
    /**
1427
     * @see it('does not allow replacing an existing field')
1428
     */
1429
    public function testDoesNotAllowReplacingAnExistingField()
1430
    {
1431
        $existingFieldError = static function (string $type, string $field) {
1432
            return 'Field "' . $type . '.' . $field . '" already exists in the schema. It cannot also be defined in this type extension.';
1433
        };
1434
1435
        $typeSDL = '
1436
          extend type Bar {
1437
            foo: Foo
1438
          }
1439
        ';
1440
1441
        try {
1442
            $this->extendTestSchema($typeSDL);
1443
            self::fail();
1444
        } catch (Error $error) {
1445
            self::assertEquals($existingFieldError('Bar', 'foo'), $error->getMessage());
1446
        }
1447
1448
        $interfaceSDL = '
1449
          extend interface SomeInterface {
1450
            some: Foo
1451
          }
1452
        ';
1453
1454
        try {
1455
            $this->extendTestSchema($interfaceSDL);
1456
            self::fail();
1457
        } catch (Error  $error) {
1458
            self::assertEquals($existingFieldError('SomeInterface', 'some'), $error->getMessage());
1459
        }
1460
1461
        $inputSDL = '
1462
          extend input SomeInput {
1463
            fooArg: String
1464
          }
1465
        ';
1466
1467
        try {
1468
            $this->extendTestSchema($inputSDL);
1469
            self::fail();
1470
        } catch (Error $error) {
1471
            self::assertEquals($existingFieldError('SomeInput', 'fooArg'), $error->getMessage());
1472
        }
1473
    }
1474
1475
    /**
1476
     * @see it('does not allow replacing an existing enum value')
1477
     */
1478
    public function testDoesNotAllowReplacingAnExistingEnumValue()
1479
    {
1480
        $sdl = '
1481
          extend enum SomeEnum {
1482
            ONE
1483
          }
1484
        ';
1485
1486
        try {
1487
            $this->extendTestSchema($sdl);
1488
            self::fail();
1489
        } catch (Error $error) {
1490
            self::assertEquals('Enum value "SomeEnum.ONE" already exists in the schema. It cannot also be defined in this type extension.', $error->getMessage());
1491
        }
1492
    }
1493
1494
    /**
1495
     * @see it('does not allow referencing an unknown type')
1496
     */
1497
    public function testDoesNotAllowReferencingAnUnknownType()
1498
    {
1499
        $unknownTypeError = 'Unknown type: "Quix". Ensure that this type exists either in the original schema, or is added in a type definition.';
1500
1501
        $typeSDL = '
1502
          extend type Bar {
1503
            quix: Quix
1504
          }
1505
        ';
1506
1507
        try {
1508
            $this->extendTestSchema($typeSDL);
1509
            self::fail();
1510
        } catch (Error $error) {
1511
            self::assertEquals($unknownTypeError, $error->getMessage());
1512
        }
1513
1514
        $interfaceSDL = '
1515
          extend interface SomeInterface {
1516
            quix: Quix
1517
          }
1518
        ';
1519
1520
        try {
1521
            $this->extendTestSchema($interfaceSDL);
1522
            self::fail();
1523
        } catch (Error $error) {
1524
            self::assertEquals($unknownTypeError, $error->getMessage());
1525
        }
1526
1527
        $unionSDL = '
1528
          extend union SomeUnion = Quix
1529
        ';
1530
1531
        try {
1532
            $this->extendTestSchema($unionSDL);
1533
            self::fail();
1534
        } catch (Error $error) {
1535
            self::assertEquals($unknownTypeError, $error->getMessage());
1536
        }
1537
1538
        $inputSDL = '
1539
          extend input SomeInput {
1540
            quix: Quix
1541
          }
1542
        ';
1543
1544
        try {
1545
            $this->extendTestSchema($inputSDL);
1546
            self::fail();
1547
        } catch (Error $error) {
1548
            self::assertEquals($unknownTypeError, $error->getMessage());
1549
        }
1550
    }
1551
1552
    /**
1553
     * @see it('does not allow extending an unknown type')
1554
     */
1555
    public function testDoesNotAllowExtendingAnUnknownType()
1556
    {
1557
        $sdls = [
1558
            'extend scalar UnknownType @foo',
1559
            'extend type UnknownType @foo',
1560
            'extend interface UnknownType @foo',
1561
            'extend enum UnknownType @foo',
1562
            'extend union UnknownType @foo',
1563
            'extend input UnknownType @foo',
1564
        ];
1565
1566
        foreach ($sdls as $sdl) {
1567
            try {
1568
                $this->extendTestSchema($sdl);
1569
                self::fail();
1570
            } catch (Error $error) {
1571
                self::assertEquals('Cannot extend type "UnknownType" because it does not exist in the existing schema.', $error->getMessage());
1572
            }
1573
        }
1574
    }
1575
1576
    /**
1577
     * @see it('maintains configuration of the original schema object')
1578
     */
1579
    public function testMaintainsConfigurationOfTheOriginalSchemaObject()
1580
    {
1581
        self::markTestSkipped('allowedLegacyNames currently not supported');
1582
1583
        $testSchemaWithLegacyNames = new Schema(
1584
            [
1585
                'query' => new ObjectType([
1586
                    'name' => 'Query',
1587
                    'fields' => static function () {
1588
                        return ['id' => ['type' => Type::id()]];
1589
                    },
1590
                ]),
1591
            ]/*,
1592
            [ 'allowedLegacyNames' => ['__badName'] ]
1593
            */
1594
        );
1595
1596
        $ast    = Parser::parse('
1597
            extend type Query {
1598
                __badName: String
1599
            }
1600
        ');
1601
        $schema = SchemaExtender::extend($testSchemaWithLegacyNames, $ast);
1602
        self::assertEquals(['__badName'], $schema->__allowedLegacyNames);
1603
    }
1604
1605
    /**
1606
     * @see it('adds to the configuration of the original schema object')
1607
     */
1608
    public function testAddsToTheConfigurationOfTheOriginalSchemaObject()
1609
    {
1610
        self::markTestSkipped('allowedLegacyNames currently not supported');
1611
1612
        $testSchemaWithLegacyNames = new Schema(
1613
            [
1614
                'query' => new ObjectType([
1615
                    'name' => 'Query',
1616
                    'fields' => static function () {
1617
                        return ['__badName' => ['type' => Type::string()]];
1618
                    },
1619
                ]),
1620
            ]/*,
1621
            ['allowedLegacyNames' => ['__badName']]
1622
            */
1623
        );
1624
1625
        $ast = Parser::parse('
1626
          extend type Query {
1627
            __anotherBadName: String
1628
          }
1629
        ');
1630
1631
        $schema = SchemaExtender::extend($testSchemaWithLegacyNames, $ast, [
1632
            'allowedLegacyNames' => ['__anotherBadName'],
1633
        ]);
1634
1635
        self::assertEquals(['__badName', '__anotherBadName'], $schema->__allowedLegacyNames);
1636
    }
1637
1638
    /**
1639
     * @see it('does not allow extending a mismatch type')
1640
     */
1641
    public function testDoesNotAllowExtendingAMismatchType()
1642
    {
1643
        $typeSDL = '
1644
          extend type SomeInterface @foo
1645
        ';
1646
1647
        try {
1648
            $this->extendTestSchema($typeSDL);
1649
            self::fail();
1650
        } catch (Error $error) {
1651
            self::assertEquals('Cannot extend non-object type "SomeInterface".', $error->getMessage());
1652
        }
1653
1654
        $interfaceSDL = '
1655
          extend interface Foo @foo
1656
        ';
1657
1658
        try {
1659
            $this->extendTestSchema($interfaceSDL);
1660
            self::fail();
1661
        } catch (Error $error) {
1662
            self::assertEquals('Cannot extend non-interface type "Foo".', $error->getMessage());
1663
        }
1664
1665
        $enumSDL = '
1666
          extend enum Foo @foo
1667
        ';
1668
1669
        try {
1670
            $this->extendTestSchema($enumSDL);
1671
            self::fail();
1672
        } catch (Error $error) {
1673
            self::assertEquals('Cannot extend non-enum type "Foo".', $error->getMessage());
1674
        }
1675
1676
        $unionSDL = '
1677
          extend union Foo @foo
1678
        ';
1679
1680
        try {
1681
            $this->extendTestSchema($unionSDL);
1682
            self::fail();
1683
        } catch (Error $error) {
1684
            self::assertEquals('Cannot extend non-union type "Foo".', $error->getMessage());
1685
        }
1686
1687
        $inputSDL = '
1688
          extend input Foo @foo
1689
        ';
1690
1691
        try {
1692
            $this->extendTestSchema($inputSDL);
1693
            self::fail();
1694
        } catch (Error $error) {
1695
            self::assertEquals('Cannot extend non-input object type "Foo".', $error->getMessage());
1696
        }
1697
    }
1698
1699
    /**
1700
     * @see it('does not automatically include common root type names')
1701
     */
1702
    public function testDoesNotAutomaticallyIncludeCommonRootTypeNames()
1703
    {
1704
        $schema = $this->extendTestSchema('
1705
            type Mutation {
1706
              doSomething: String
1707
            }
1708
        ');
1709
1710
        self::assertNull($schema->getMutationType());
1711
    }
1712
1713
    /**
1714
     * @see it('adds schema definition missing in the original schema')
1715
     */
1716
    public function testAddsSchemaDefinitionMissingInTheOriginalSchema()
1717
    {
1718
        $schema = new Schema([
1719
            'directives' => [$this->FooDirective],
1720
            'types' => [$this->FooType],
1721
        ]);
1722
1723
        self::assertNull($schema->getQueryType());
1724
1725
        $ast = Parser::parse('
1726
            schema @foo {
1727
              query: Foo
1728
            }
1729
        ');
1730
1731
        $schema    = SchemaExtender::extend($schema, $ast);
1732
        $queryType = $schema->getQueryType();
1733
1734
        self::assertEquals($queryType->name, 'Foo');
1735
    }
1736
1737
1738
    /**
1739
     * @see it('adds new root types via schema extension')
1740
     */
1741
    public function testAddsNewRootTypesViaSchemaExtension()
1742
    {
1743
        $schema = $this->extendTestSchema('
1744
            extend schema {
1745
              mutation: Mutation
1746
            }
1747
            type Mutation {
1748
              doSomething: String
1749
            }
1750
        ');
1751
1752
        $mutationType = $schema->getMutationType();
1753
        self::assertEquals($mutationType->name, 'Mutation');
1754
    }
1755
1756
    /**
1757
     * @see it('adds multiple new root types via schema extension')
1758
     */
1759
    public function testAddsMultipleNewRootTypesViaSchemaExtension()
1760
    {
1761
        $schema           = $this->extendTestSchema('
1762
            extend schema {
1763
              mutation: Mutation
1764
              subscription: Subscription
1765
            }
1766
            type Mutation {
1767
              doSomething: String
1768
            }
1769
            type Subscription {
1770
              hearSomething: String
1771
            }
1772
        ');
1773
        $mutationType     = $schema->getMutationType();
1774
        $subscriptionType = $schema->getSubscriptionType();
1775
1776
        self::assertEquals('Mutation', $mutationType->name);
1777
        self::assertEquals('Subscription', $subscriptionType->name);
1778
    }
1779
1780
    /**
1781
     * @see it('applies multiple schema extensions')
1782
     */
1783
    public function testAppliesMultipleSchemaExtensions()
1784
    {
1785
        $schema = $this->extendTestSchema('
1786
            extend schema {
1787
              mutation: Mutation
1788
            }
1789
            extend schema {
1790
              subscription: Subscription
1791
            }
1792
            type Mutation {
1793
              doSomething: String
1794
            }
1795
            type Subscription {
1796
              hearSomething: String
1797
            }
1798
        ');
1799
1800
        $mutationType     = $schema->getMutationType();
1801
        $subscriptionType = $schema->getSubscriptionType();
1802
1803
        self::assertEquals('Mutation', $mutationType->name);
1804
        self::assertEquals('Subscription', $subscriptionType->name);
1805
    }
1806
1807
    /**
1808
     * @see it('schema extension AST are available from schema object')
1809
     */
1810
    public function testSchemaExtensionASTAreAvailableFromSchemaObject()
1811
    {
1812
        $schema = $this->extendTestSchema('
1813
            extend schema {
1814
              mutation: Mutation
1815
            }
1816
            extend schema {
1817
              subscription: Subscription
1818
            }
1819
            type Mutation {
1820
              doSomething: String
1821
            }
1822
            type Subscription {
1823
              hearSomething: String
1824
            }
1825
        ');
1826
1827
        $ast    = Parser::parse('
1828
            extend schema @foo
1829
        ');
1830
        $schema = SchemaExtender::extend($schema, $ast);
1831
1832
        $nodes = $schema->extensionASTNodes;
1833
        self::assertEquals(
1834
            $this->dedent('
1835
                extend schema {
1836
                  mutation: Mutation
1837
                }
1838
1839
                extend schema {
1840
                  subscription: Subscription
1841
                }
1842
1843
                extend schema @foo
1844
            '),
1845
            implode(
1846
                "\n",
1847
                array_map(static function ($node) {
1848
                    return Printer::doPrint($node) . "\n";
1849
                }, $nodes)
1850
            )
1851
        );
1852
    }
1853
1854
    /**
1855
     * @see it('does not allow redefining an existing root type')
1856
     */
1857
    public function testDoesNotAllowRedefiningAnExistingRootType()
1858
    {
1859
        $sdl = '
1860
            extend schema {
1861
                query: SomeType
1862
            }
1863
1864
            type SomeType {
1865
                seeSomething: String
1866
            }
1867
        ';
1868
1869
        try {
1870
            $this->extendTestSchema($sdl);
1871
            self::fail();
1872
        } catch (Error $error) {
1873
            self::assertEquals('Must provide only one query type in schema.', $error->getMessage());
1874
        }
1875
    }
1876
1877
1878
    /**
1879
     * @see it('does not allow defining a root operation type twice')
1880
     */
1881
    public function testDoesNotAllowDefiningARootOperationTypeTwice()
1882
    {
1883
        $sdl = '
1884
            extend schema {
1885
              mutation: Mutation
1886
            }
1887
            extend schema {
1888
              mutation: Mutation
1889
            }
1890
            type Mutation {
1891
              doSomething: String
1892
            }
1893
        ';
1894
1895
        try {
1896
            $this->extendTestSchema($sdl);
1897
            self::fail();
1898
        } catch (Error $error) {
1899
            self::assertEquals('Must provide only one mutation type in schema.', $error->getMessage());
1900
        }
1901
    }
1902
1903
    /**
1904
     * @see it('does not allow defining a root operation type with different types')
1905
     */
1906
    public function testDoesNotAllowDefiningARootOperationTypeWithDifferentTypes()
1907
    {
1908
        $sdl = '
1909
            extend schema {
1910
              mutation: Mutation
1911
            }
1912
            extend schema {
1913
              mutation: SomethingElse
1914
            }
1915
            type Mutation {
1916
              doSomething: String
1917
            }
1918
            type SomethingElse {
1919
              doSomethingElse: String
1920
            }
1921
        ';
1922
1923
        try {
1924
            $this->extendTestSchema($sdl);
1925
            self::fail();
1926
        } catch (Error $error) {
1927
            self::assertEquals('Must provide only one mutation type in schema.', $error->getMessage());
1928
        }
1929
    }
1930
1931
    /**
1932
     * @see https://github.com/webonyx/graphql-php/pull/381
1933
     */
1934
    public function testOriginalResolversArePreserved()
1935
    {
1936
        $queryType = new ObjectType([
1937
            'name' => 'Query',
1938
            'fields' => [
1939
                'hello' => [
1940
                    'type' => Type::string(),
1941
                    'resolve' => static function () {
1942
                        return 'Hello World!';
1943
                    },
1944
                ],
1945
            ],
1946
        ]);
1947
1948
        $schema = new Schema(['query' => $queryType]);
1949
1950
        $documentNode = Parser::parse('
1951
extend type Query {
1952
	misc: String
1953
}
1954
');
1955
1956
        $extendedSchema = SchemaExtender::extend($schema, $documentNode);
1957
        $helloResolveFn = $extendedSchema->getQueryType()->getField('hello')->resolveFn;
1958
1959
        self::assertInternalType('callable', $helloResolveFn);
1960
1961
        $query  = '{ hello }';
1962
        $result = GraphQL::executeQuery($extendedSchema, $query);
1963
        self::assertSame(['data' => ['hello' => 'Hello World!']], $result->toArray());
1964
    }
1965
}
1966