Passed
Pull Request — master (#586)
by Šimon
12:52
created

SchemaExtenderTest::testDoesNotAllowExtendingAMismatchType()   B

Complexity

Conditions 6
Paths 243

Size

Total Lines 55
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 30
c 2
b 0
f 0
dl 0
loc 55
rs 7.7211
cc 6
nc 243
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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