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

testNonNullMustOnlyAcceptNonNullableTypes()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 34
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
c 1
b 0
f 0
dl 0
loc 34
rs 9.1768
cc 5
nc 12
nop 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A DefinitionTest::testRejectsASchemaWhichDefinesAnObjectTypeTwice() 0 26 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Type;
6
7
use GraphQL\Error\InvariantViolation;
8
use GraphQL\Tests\Type\TestClasses\MyCustomType;
9
use GraphQL\Tests\Type\TestClasses\OtherCustom;
10
use GraphQL\Type\Definition\BooleanType;
11
use GraphQL\Type\Definition\CustomScalarType;
12
use GraphQL\Type\Definition\EnumType;
13
use GraphQL\Type\Definition\FloatType;
14
use GraphQL\Type\Definition\IDType;
15
use GraphQL\Type\Definition\InputObjectType;
16
use GraphQL\Type\Definition\InterfaceType;
17
use GraphQL\Type\Definition\IntType;
18
use GraphQL\Type\Definition\ListOfType;
19
use GraphQL\Type\Definition\NonNull;
20
use GraphQL\Type\Definition\ObjectType;
21
use GraphQL\Type\Definition\Type;
22
use GraphQL\Type\Definition\UnionType;
23
use GraphQL\Type\Schema;
24
use GraphQL\Utils\Utils;
25
use PHPUnit\Framework\TestCase;
26
use stdClass;
27
use Throwable;
28
use function count;
29
use function get_class;
30
use function json_encode;
31
use function sprintf;
32
33
class DefinitionTest extends TestCase
34
{
35
    /** @var ObjectType */
36
    public $blogImage;
37
38
    /** @var ObjectType */
39
    public $blogArticle;
40
41
    /** @var ObjectType */
42
    public $blogAuthor;
43
44
    /** @var ObjectType */
45
    public $blogMutation;
46
47
    /** @var ObjectType */
48
    public $blogQuery;
49
50
    /** @var ObjectType */
51
    public $blogSubscription;
52
53
    /** @var ObjectType */
54
    public $objectType;
55
56
    /** @var ObjectType */
57
    public $objectWithIsTypeOf;
58
59
    /** @var InterfaceType */
60
    public $interfaceType;
61
62
    /** @var UnionType */
63
    public $unionType;
64
65
    /** @var EnumType */
66
    public $enumType;
67
68
    /** @var InputObjectType */
69
    public $inputObjectType;
70
71
    /** @var CustomScalarType */
72
    public $scalarType;
73
74
    public function setUp()
75
    {
76
        $this->objectType      = new ObjectType(['name' => 'Object', 'fields' => ['tmp' => Type::string()]]);
77
        $this->interfaceType   = new InterfaceType(['name' => 'Interface']);
78
        $this->unionType       = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]);
79
        $this->enumType        = new EnumType(['name' => 'Enum']);
80
        $this->inputObjectType = new InputObjectType(['name' => 'InputObject']);
81
82
        $this->objectWithIsTypeOf = new ObjectType([
83
            'name'   => 'ObjectWithIsTypeOf',
84
            'fields' => ['f' => ['type' => Type::string()]],
85
        ]);
86
87
        $this->scalarType = new CustomScalarType([
88
            'name'         => 'Scalar',
89
            'serialize'    => static function () {
90
            },
91
            'parseValue'   => static function () {
92
            },
93
            'parseLiteral' => static function () {
94
            },
95
        ]);
96
97
        $this->blogImage = new ObjectType([
98
            'name'   => 'Image',
99
            'fields' => [
100
                'url'    => ['type' => Type::string()],
101
                'width'  => ['type' => Type::int()],
102
                'height' => ['type' => Type::int()],
103
            ],
104
        ]);
105
106
        $this->blogAuthor = new ObjectType([
107
            'name'   => 'Author',
108
            'fields' => function () {
109
                return [
110
                    'id'            => ['type' => Type::string()],
111
                    'name'          => ['type' => Type::string()],
112
                    'pic'           => [
113
                        'type' => $this->blogImage,
114
                        'args' => [
115
                            'width'  => ['type' => Type::int()],
116
                            'height' => ['type' => Type::int()],
117
                        ],
118
                    ],
119
                    'recentArticle' => $this->blogArticle,
120
                ];
121
            },
122
        ]);
123
124
        $this->blogArticle = new ObjectType([
125
            'name'   => 'Article',
126
            'fields' => [
127
                'id'          => ['type' => Type::string()],
128
                'isPublished' => ['type' => Type::boolean()],
129
                'author'      => ['type' => $this->blogAuthor],
130
                'title'       => ['type' => Type::string()],
131
                'body'        => ['type' => Type::string()],
132
            ],
133
        ]);
134
135
        $this->blogQuery = new ObjectType([
136
            'name'   => 'Query',
137
            'fields' => [
138
                'article' => [
139
                    'type' => $this->blogArticle,
140
                    'args' => [
141
                        'id' => ['type' => Type::string()],
142
                    ],
143
                ],
144
                'feed'    => ['type' => new ListOfType($this->blogArticle)],
145
            ],
146
        ]);
147
148
        $this->blogMutation = new ObjectType([
149
            'name'   => 'Mutation',
150
            'fields' => [
151
                'writeArticle' => ['type' => $this->blogArticle],
152
            ],
153
        ]);
154
155
        $this->blogSubscription = new ObjectType([
156
            'name'   => 'Subscription',
157
            'fields' => [
158
                'articleSubscribe' => [
159
                    'args' => ['id' => ['type' => Type::string()]],
160
                    'type' => $this->blogArticle,
161
                ],
162
            ],
163
        ]);
164
    }
165
166
    // Type System: Example
167
168
    /**
169
     * @see it('defines a query only schema')
170
     */
171
    public function testDefinesAQueryOnlySchema() : void
172
    {
173
        $blogSchema = new Schema([
174
            'query' => $this->blogQuery,
175
        ]);
176
177
        self::assertSame($blogSchema->getQueryType(), $this->blogQuery);
178
179
        $articleField = $this->blogQuery->getField('article');
180
        self::assertSame($articleField->getType(), $this->blogArticle);
181
        self::assertSame($articleField->getType()->name, 'Article');
182
        self::assertSame($articleField->name, 'article');
183
184
        /** @var ObjectType $articleFieldType */
185
        $articleFieldType = $articleField->getType();
186
        $titleField       = $articleFieldType->getField('title');
187
188
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $titleField);
189
        self::assertSame('title', $titleField->name);
190
        self::assertSame(Type::string(), $titleField->getType());
191
192
        $authorField = $articleFieldType->getField('author');
193
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $authorField);
194
195
        /** @var ObjectType $authorFieldType */
196
        $authorFieldType = $authorField->getType();
197
        self::assertSame($this->blogAuthor, $authorFieldType);
198
199
        $recentArticleField = $authorFieldType->getField('recentArticle');
200
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $recentArticleField);
201
        self::assertSame($this->blogArticle, $recentArticleField->getType());
202
203
        $feedField = $this->blogQuery->getField('feed');
204
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $feedField);
205
206
        /** @var ListOfType $feedFieldType */
207
        $feedFieldType = $feedField->getType();
208
        self::assertInstanceOf('GraphQL\Type\Definition\ListOfType', $feedFieldType);
209
        self::assertSame($this->blogArticle, $feedFieldType->getWrappedType());
210
    }
211
212
    /**
213
     * @see it('defines a mutation schema')
214
     */
215
    public function testDefinesAMutationSchema() : void
216
    {
217
        $schema = new Schema([
218
            'query'    => $this->blogQuery,
219
            'mutation' => $this->blogMutation,
220
        ]);
221
222
        self::assertSame($this->blogMutation, $schema->getMutationType());
223
        $writeMutation = $this->blogMutation->getField('writeArticle');
224
225
        self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $writeMutation);
226
        self::assertSame($this->blogArticle, $writeMutation->getType());
227
        self::assertSame('Article', $writeMutation->getType()->name);
228
        self::assertSame('writeArticle', $writeMutation->name);
229
    }
230
231
    /**
232
     * @see it('defines a subscription schema')
233
     */
234
    public function testDefinesSubscriptionSchema() : void
235
    {
236
        $schema = new Schema([
237
            'query'        => $this->blogQuery,
238
            'subscription' => $this->blogSubscription,
239
        ]);
240
241
        self::assertEquals($this->blogSubscription, $schema->getSubscriptionType());
242
243
        $sub = $this->blogSubscription->getField('articleSubscribe');
244
        self::assertEquals($sub->getType(), $this->blogArticle);
245
        self::assertEquals($sub->getType()->name, 'Article');
246
        self::assertEquals($sub->name, 'articleSubscribe');
247
    }
248
249
    /**
250
     * @see it('defines an enum type with deprecated value')
251
     */
252
    public function testDefinesEnumTypeWithDeprecatedValue() : void
253
    {
254
        $enumTypeWithDeprecatedValue = new EnumType([
255
            'name'   => 'EnumWithDeprecatedValue',
256
            'values' => [
257
                'foo' => ['deprecationReason' => 'Just because'],
258
            ],
259
        ]);
260
261
        $value = $enumTypeWithDeprecatedValue->getValues()[0];
262
263
        self::assertArraySubset(
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertArraySubset() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3494 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

263
        /** @scrutinizer ignore-deprecated */ self::assertArraySubset(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
264
            [
265
                'name'              => 'foo',
266
                'description'       => null,
267
                'deprecationReason' => 'Just because',
268
                'value'             => 'foo',
269
                'astNode'           => null,
270
            ],
271
            (array) $value
272
        );
273
274
        self::assertEquals(true, $value->isDeprecated());
275
    }
276
277
    /**
278
     * @see it('defines an enum type with a value of `null` and `undefined`')
279
     */
280
    public function testDefinesAnEnumTypeWithAValueOfNullAndUndefined() : void
281
    {
282
        $EnumTypeWithNullishValue = new EnumType([
283
            'name'   => 'EnumWithNullishValue',
284
            'values' => [
285
                'NULL'      => ['value' => null],
286
                'UNDEFINED' => ['value' => null],
287
            ],
288
        ]);
289
290
        $expected = [
291
            [
292
                'name'              => 'NULL',
293
                'description'       => null,
294
                'deprecationReason' => null,
295
                'value'             => null,
296
                'astNode'           => null,
297
            ],
298
            [
299
                'name'              => 'UNDEFINED',
300
                'description'       => null,
301
                'deprecationReason' => null,
302
                'value'             => null,
303
                'astNode'           => null,
304
            ],
305
        ];
306
307
        $actual = $EnumTypeWithNullishValue->getValues();
308
309
        self::assertEquals(count($expected), count($actual));
310
        self::assertArraySubset($expected[0], (array) $actual[0]);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertArraySubset() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3494 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

310
        /** @scrutinizer ignore-deprecated */ self::assertArraySubset($expected[0], (array) $actual[0]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
311
        self::assertArraySubset($expected[1], (array) $actual[1]);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertArraySubset() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3494 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

311
        /** @scrutinizer ignore-deprecated */ self::assertArraySubset($expected[1], (array) $actual[1]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
312
    }
313
314
    /**
315
     * @see it('defines an object type with deprecated field')
316
     */
317
    public function testDefinesAnObjectTypeWithDeprecatedField() : void
318
    {
319
        $TypeWithDeprecatedField = new ObjectType([
320
            'name'   => 'foo',
321
            'fields' => [
322
                'bar' => [
323
                    'type'              => Type::string(),
324
                    'deprecationReason' => 'A terrible reason',
325
                ],
326
            ],
327
        ]);
328
329
        $field = $TypeWithDeprecatedField->getField('bar');
330
331
        self::assertEquals(Type::string(), $field->getType());
332
        self::assertEquals(true, $field->isDeprecated());
333
        self::assertEquals('A terrible reason', $field->deprecationReason);
334
        self::assertEquals('bar', $field->name);
335
        self::assertEquals([], $field->args);
336
    }
337
338
    /**
339
     * @see it('includes nested input objects in the map')
340
     */
341
    public function testIncludesNestedInputObjectInTheMap() : void
342
    {
343
        $nestedInputObject = new InputObjectType([
344
            'name'   => 'NestedInputObject',
345
            'fields' => ['value' => ['type' => Type::string()]],
346
        ]);
347
        $someInputObject   = new InputObjectType([
348
            'name'   => 'SomeInputObject',
349
            'fields' => ['nested' => ['type' => $nestedInputObject]],
350
        ]);
351
        $someMutation      = new ObjectType([
352
            'name'   => 'SomeMutation',
353
            'fields' => [
354
                'mutateSomething' => [
355
                    'type' => $this->blogArticle,
356
                    'args' => ['input' => ['type' => $someInputObject]],
357
                ],
358
            ],
359
        ]);
360
361
        $schema = new Schema([
362
            'query'    => $this->blogQuery,
363
            'mutation' => $someMutation,
364
        ]);
365
        self::assertSame($nestedInputObject, $schema->getType('NestedInputObject'));
366
    }
367
368
    /**
369
     * @see it('includes interface possible types in the type map')
370
     */
371
    public function testIncludesInterfaceSubtypesInTheTypeMap() : void
372
    {
373
        $someInterface = new InterfaceType([
374
            'name'   => 'SomeInterface',
375
            'fields' => [
376
                'f' => ['type' => Type::int()],
377
            ],
378
        ]);
379
380
        $someSubtype = new ObjectType([
381
            'name'       => 'SomeSubtype',
382
            'fields'     => [
383
                'f' => ['type' => Type::int()],
384
            ],
385
            'interfaces' => [$someInterface],
386
        ]);
387
388
        $schema = new Schema([
389
            'query' => new ObjectType([
390
                'name'   => 'Query',
391
                'fields' => [
392
                    'iface' => ['type' => $someInterface],
393
                ],
394
            ]),
395
            'types' => [$someSubtype],
396
        ]);
397
        self::assertSame($someSubtype, $schema->getType('SomeSubtype'));
398
    }
399
400
    /**
401
     * @see it('includes interfaces' thunk subtypes in the type map')
402
     */
403
    public function testIncludesInterfacesThunkSubtypesInTheTypeMap() : void
404
    {
405
        $someInterface = null;
406
407
        $someSubtype = new ObjectType([
408
            'name'       => 'SomeSubtype',
409
            'fields'     => [
410
                'f' => ['type' => Type::int()],
411
            ],
412
            'interfaces' => static function () use (&$someInterface) {
413
                return [$someInterface];
414
            },
415
        ]);
416
417
        $someInterface = new InterfaceType([
418
            'name'   => 'SomeInterface',
419
            'fields' => [
420
                'f' => ['type' => Type::int()],
421
            ],
422
        ]);
423
424
        $schema = new Schema([
425
            'query' => new ObjectType([
426
                'name'   => 'Query',
427
                'fields' => [
428
                    'iface' => ['type' => $someInterface],
429
                ],
430
            ]),
431
            'types' => [$someSubtype],
432
        ]);
433
434
        self::assertSame($someSubtype, $schema->getType('SomeSubtype'));
435
    }
436
437
    /**
438
     * @see it('stringifies simple types')
439
     */
440
    public function testStringifiesSimpleTypes() : void
441
    {
442
        self::assertSame('Int', (string) Type::int());
443
        self::assertSame('Article', (string) $this->blogArticle);
444
445
        self::assertSame('Interface', (string) $this->interfaceType);
446
        self::assertSame('Union', (string) $this->unionType);
447
        self::assertSame('Enum', (string) $this->enumType);
448
        self::assertSame('InputObject', (string) $this->inputObjectType);
449
        self::assertSame('Object', (string) $this->objectType);
450
451
        self::assertSame('Int!', (string) new NonNull(Type::int()));
452
        self::assertSame('[Int]', (string) new ListOfType(Type::int()));
453
        self::assertSame('[Int]!', (string) new NonNull(new ListOfType(Type::int())));
454
        self::assertSame('[Int!]', (string) new ListOfType(new NonNull(Type::int())));
455
        self::assertSame('[[Int]]', (string) new ListOfType(new ListOfType(Type::int())));
456
    }
457
458
    /**
459
     * @see it('JSON stringifies simple types')
460
     */
461
    public function testJSONStringifiesSimpleTypes() : void
462
    {
463
        self::assertEquals('"Int"', json_encode(Type::int()));
464
        self::assertEquals('"Article"', json_encode($this->blogArticle));
465
        self::assertEquals('"Interface"', json_encode($this->interfaceType));
466
        self::assertEquals('"Union"', json_encode($this->unionType));
467
        self::assertEquals('"Enum"', json_encode($this->enumType));
468
        self::assertEquals('"InputObject"', json_encode($this->inputObjectType));
469
        self::assertEquals('"Int!"', json_encode(Type::nonNull(Type::int())));
470
        self::assertEquals('"[Int]"', json_encode(Type::listOf(Type::int())));
471
        self::assertEquals('"[Int]!"', json_encode(Type::nonNull(Type::listOf(Type::int()))));
472
        self::assertEquals('"[Int!]"', json_encode(Type::listOf(Type::nonNull(Type::int()))));
473
        self::assertEquals('"[[Int]]"', json_encode(Type::listOf(Type::listOf(Type::int()))));
474
    }
475
476
    /**
477
     * @see it('identifies input types')
478
     */
479
    public function testIdentifiesInputTypes() : void
480
    {
481
        $expected = [
482
            [Type::int(), true],
483
            [$this->objectType, false],
484
            [$this->interfaceType, false],
485
            [$this->unionType, false],
486
            [$this->enumType, true],
487
            [$this->inputObjectType, true],
488
489
            [Type::boolean(), true],
490
            [Type::float(),true ],
491
            [Type::id(), true],
492
            [Type::int(), true],
493
            [Type::listOf(Type::string()), true],
494
            [Type::listOf($this->objectType), false],
495
            [Type::nonNull(Type::string()), true],
496
            [Type::nonNull($this->objectType), false],
497
            [Type::string(), true],
498
        ];
499
500
        foreach ($expected as $index => $entry) {
501
            self::assertSame(
502
                $entry[1],
503
                Type::isInputType($entry[0]),
504
                sprintf('Type %s was detected incorrectly', $entry[0])
505
            );
506
        }
507
    }
508
509
    /**
510
     * @see it('identifies output types')
511
     */
512
    public function testIdentifiesOutputTypes() : void
513
    {
514
        $expected = [
515
            [Type::int(), true],
516
            [$this->objectType, true],
517
            [$this->interfaceType, true],
518
            [$this->unionType, true],
519
            [$this->enumType, true],
520
            [$this->inputObjectType, false],
521
522
            [Type::boolean(), true],
523
            [Type::float(),true ],
524
            [Type::id(), true],
525
            [Type::int(), true],
526
            [Type::listOf(Type::string()), true],
527
            [Type::listOf($this->objectType), true],
528
            [Type::nonNull(Type::string()), true],
529
            [Type::nonNull($this->objectType), true],
530
            [Type::string(), true],
531
        ];
532
533
        foreach ($expected as $index => $entry) {
534
            self::assertSame(
535
                $entry[1],
536
                Type::isOutputType($entry[0]),
537
                sprintf('Type %s was detected incorrectly', $entry[0])
538
            );
539
        }
540
    }
541
542
    /**
543
     * @see it('allows a thunk for Union member types')
544
     */
545
    public function testAllowsThunkForUnionTypes() : void
546
    {
547
        $union = new UnionType([
548
            'name'  => 'ThunkUnion',
549
            'types' => function () {
550
                return [$this->objectType];
551
            },
552
        ]);
553
554
        $types = $union->getTypes();
555
        self::assertEquals(1, count($types));
556
        self::assertSame($this->objectType, $types[0]);
557
    }
558
559
    public function testAllowsRecursiveDefinitions() : void
560
    {
561
        // See https://github.com/webonyx/graphql-php/issues/16
562
        $node = new InterfaceType([
563
            'name'   => 'Node',
564
            'fields' => [
565
                'id' => ['type' => Type::nonNull(Type::id())],
566
            ],
567
        ]);
568
569
        $blog   = null;
570
        $called = false;
571
572
        $user = new ObjectType([
573
            'name'       => 'User',
574
            'fields'     => static function () use (&$blog, &$called) {
575
                self::assertNotNull($blog, 'Blog type is expected to be defined at this point, but it is null');
576
                $called = true;
577
578
                return [
579
                    'id'    => ['type' => Type::nonNull(Type::id())],
580
                    'blogs' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($blog)))],
581
                ];
582
            },
583
            'interfaces' => static function () use ($node) {
584
                return [$node];
585
            },
586
        ]);
587
588
        $blog = new ObjectType([
589
            'name'       => 'Blog',
590
            'fields'     => static function () use ($user) {
591
                return [
592
                    'id'    => ['type' => Type::nonNull(Type::id())],
593
                    'owner' => ['type' => Type::nonNull($user)],
594
                ];
595
            },
596
            'interfaces' => static function () use ($node) {
597
                return [$node];
598
            },
599
        ]);
600
601
        $schema = new Schema([
602
            'query' => new ObjectType([
603
                'name'   => 'Query',
604
                'fields' => [
605
                    'node' => ['type' => $node],
606
                ],
607
            ]),
608
            'types' => [$user, $blog],
609
        ]);
610
611
        self::assertTrue($called);
612
        $schema->getType('Blog');
613
614
        self::assertEquals([$node], $blog->getInterfaces());
615
        self::assertEquals([$node], $user->getInterfaces());
616
617
        self::assertNotNull($user->getField('blogs'));
618
        /** @var NonNull $blogFieldReturnType */
619
        $blogFieldReturnType = $user->getField('blogs')->getType();
620
        self::assertSame($blog, $blogFieldReturnType->getWrappedType(true));
621
622
        self::assertNotNull($blog->getField('owner'));
623
        /** @var NonNull $ownerFieldReturnType */
624
        $ownerFieldReturnType = $blog->getField('owner')->getType();
625
        self::assertSame($user, $ownerFieldReturnType->getWrappedType(true));
626
    }
627
628
    public function testInputObjectTypeAllowsRecursiveDefinitions() : void
629
    {
630
        $called      = false;
631
        $inputObject = new InputObjectType([
632
            'name'   => 'InputObject',
633
            'fields' => static function () use (&$inputObject, &$called) {
634
                $called = true;
635
636
                return [
637
                    'value'  => ['type' => Type::string()],
638
                    'nested' => ['type' => $inputObject],
639
                ];
640
            },
641
        ]);
642
643
        $someMutation = new ObjectType([
644
            'name'   => 'SomeMutation',
645
            'fields' => [
646
                'mutateSomething' => [
647
                    'type' => $this->blogArticle,
648
                    'args' => ['input' => ['type' => $inputObject]],
649
                ],
650
            ],
651
        ]);
652
653
        $schema = new Schema([
654
            'query'    => $this->blogQuery,
655
            'mutation' => $someMutation,
656
        ]);
657
658
        self::assertSame($inputObject, $schema->getType('InputObject'));
659
        self::assertTrue($called);
660
        self::assertEquals(count($inputObject->getFields()), 2);
661
        self::assertSame($inputObject->getField('nested')->getType(), $inputObject);
662
        self::assertSame($someMutation->getField('mutateSomething')->getArg('input')->getType(), $inputObject);
663
    }
664
665
    public function testInterfaceTypeAllowsRecursiveDefinitions() : void
666
    {
667
        $called    = false;
668
        $interface = new InterfaceType([
669
            'name'   => 'SomeInterface',
670
            'fields' => static function () use (&$interface, &$called) {
671
                $called = true;
672
673
                return [
674
                    'value'  => ['type' => Type::string()],
675
                    'nested' => ['type' => $interface],
676
                ];
677
            },
678
        ]);
679
680
        $query = new ObjectType([
681
            'name'   => 'Query',
682
            'fields' => [
683
                'test' => ['type' => $interface],
684
            ],
685
        ]);
686
687
        $schema = new Schema(['query' => $query]);
688
689
        self::assertSame($interface, $schema->getType('SomeInterface'));
690
        self::assertTrue($called);
691
        self::assertEquals(count($interface->getFields()), 2);
692
        self::assertSame($interface->getField('nested')->getType(), $interface);
693
        self::assertSame($interface->getField('value')->getType(), Type::string());
694
    }
695
696
    public function testAllowsShorthandFieldDefinition() : void
697
    {
698
        $interface = new InterfaceType([
699
            'name'   => 'SomeInterface',
700
            'fields' => static function () use (&$interface) {
701
                return [
702
                    'value'   => Type::string(),
703
                    'nested'  => $interface,
704
                    'withArg' => [
705
                        'type' => Type::string(),
706
                        'args' => [
707
                            'arg1' => Type::int(),
708
                        ],
709
                    ],
710
                ];
711
            },
712
        ]);
713
714
        $query = new ObjectType([
715
            'name'   => 'Query',
716
            'fields' => ['test' => $interface],
717
        ]);
718
719
        $schema = new Schema(['query' => $query]);
720
721
        /** @var InterfaceType $SomeInterface */
722
        $SomeInterface = $schema->getType('SomeInterface');
723
724
        $valueField = $SomeInterface->getField('value');
725
        self::assertEquals(Type::string(), $valueField->getType());
726
727
        $nestedField = $SomeInterface->getField('nested');
728
        self::assertEquals($interface, $nestedField->getType());
729
730
        $withArg = $SomeInterface->getField('withArg');
731
        self::assertEquals(Type::string(), $withArg->getType());
732
733
        self::assertEquals('arg1', $withArg->args[0]->name);
734
        self::assertEquals(Type::int(), $withArg->args[0]->getType());
735
736
        /** @var ObjectType $Query */
737
        $Query     = $schema->getType('Query');
738
        $testField = $Query->getField('test');
739
        self::assertEquals($interface, $testField->getType());
740
        self::assertEquals('test', $testField->name);
741
    }
742
743
    public function testInfersNameFromClassname() : void
744
    {
745
        $myObj = new MyCustomType();
746
        self::assertEquals('MyCustom', $myObj->name);
747
748
        $otherCustom = new OtherCustom();
749
        self::assertEquals('OtherCustom', $otherCustom->name);
750
    }
751
752
    public function testAllowsOverridingInternalTypes() : void
753
    {
754
        $idType = new CustomScalarType([
755
            'name'         => 'ID',
756
            'serialize'    => static function () {
757
            },
758
            'parseValue'   => static function () {
759
            },
760
            'parseLiteral' => static function () {
761
            },
762
        ]);
763
764
        $schema = new Schema([
765
            'query' => new ObjectType(['name' => 'Query', 'fields' => []]),
766
            'types' => [$idType],
767
        ]);
768
769
        self::assertSame($idType, $schema->getType('ID'));
770
    }
771
772
    // Field config must be object
773
774
    /**
775
     * @see it('accepts an Object type with a field function')
776
     */
777
    public function testAcceptsAnObjectTypeWithAFieldFunction() : void
778
    {
779
        $objType = new ObjectType([
780
            'name'   => 'SomeObject',
781
            'fields' => static function () {
782
                return [
783
                    'f' => ['type' => Type::string()],
784
                ];
785
            },
786
        ]);
787
        $objType->assertValid();
788
        self::assertSame(Type::string(), $objType->getField('f')->getType());
789
    }
790
791
    /**
792
     * @see it('rejects an Object type field with undefined config')
793
     */
794
    public function testRejectsAnObjectTypeFieldWithUndefinedConfig() : void
795
    {
796
        $objType = new ObjectType([
797
            'name'   => 'SomeObject',
798
            'fields' => ['f' => null],
799
        ]);
800
        $this->expectException(InvariantViolation::class);
801
        $this->expectExceptionMessage(
802
            'SomeObject.f field config must be an array, but got: null'
803
        );
804
        $objType->getFields();
805
    }
806
807
    /**
808
     * @see it('rejects an Object type with incorrectly typed fields')
809
     */
810
    public function testRejectsAnObjectTypeWithIncorrectlyTypedFields() : void
811
    {
812
        $objType = new ObjectType([
813
            'name'   => 'SomeObject',
814
            'fields' => [['field' => Type::string()]],
815
        ]);
816
        $this->expectException(InvariantViolation::class);
817
        $this->expectExceptionMessage(
818
            'SomeObject fields must be an associative array with field names as keys or a ' .
819
            'function which returns such an array.'
820
        );
821
        $objType->getFields();
822
    }
823
824
    /**
825
     * @see it('rejects an Object type with a field function that returns incorrect type')
826
     */
827
    public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectType() : void
828
    {
829
        $objType = new ObjectType([
830
            'name'   => 'SomeObject',
831
            'fields' => static function () {
832
                return [['field' => Type::string()]];
833
            },
834
        ]);
835
        $this->expectException(InvariantViolation::class);
836
        $this->expectExceptionMessage(
837
            'SomeObject fields must be an associative array with field names as keys or a ' .
838
            'function which returns such an array.'
839
        );
840
        $objType->getFields();
841
    }
842
843
    // Field arg config must be object
844
845
    /**
846
     * @see it('accepts an Object type with field args')
847
     */
848
    public function testAcceptsAnObjectTypeWithFieldArgs() : void
849
    {
850
        $this->expectNotToPerformAssertions();
851
        $objType = new ObjectType([
852
            'name'   => 'SomeObject',
853
            'fields' => [
854
                'goodField' => [
855
                    'type' => Type::string(),
856
                    'args' => [
857
                        'goodArg' => ['type' => Type::string()],
858
                    ],
859
                ],
860
            ],
861
        ]);
862
        // Should not throw:
863
        $objType->assertValid();
864
    }
865
866
    // rejects an Object type with incorrectly typed field args
867
868
    /**
869
     * @see it('does not allow isDeprecated without deprecationReason on field')
870
     */
871
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField() : void
872
    {
873
        $OldObject = new ObjectType([
874
            'name'   => 'OldObject',
875
            'fields' => [
876
                'field' => [
877
                    'type'         => Type::string(),
878
                    'isDeprecated' => true,
879
                ],
880
            ],
881
        ]);
882
883
        $this->expectException(InvariantViolation::class);
884
        $this->expectExceptionMessage(
885
            'OldObject.field should provide "deprecationReason" instead of "isDeprecated".'
886
        );
887
        $OldObject->assertValid();
888
    }
889
890
    // Object interfaces must be array
891
892
    /**
893
     * @see it('accepts an Object type with array interfaces')
894
     */
895
    public function testAcceptsAnObjectTypeWithArrayInterfaces() : void
896
    {
897
        $objType = new ObjectType([
898
            'name'       => 'SomeObject',
899
            'interfaces' => [$this->interfaceType],
900
            'fields'     => ['f' => ['type' => Type::string()]],
901
        ]);
902
        self::assertSame($this->interfaceType, $objType->getInterfaces()[0]);
903
    }
904
905
    /**
906
     * @see it('accepts an Object type with interfaces as a function returning an array')
907
     */
908
    public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray() : void
909
    {
910
        $objType = new ObjectType([
911
            'name'       => 'SomeObject',
912
            'interfaces' => function () {
913
                return [$this->interfaceType];
914
            },
915
            'fields'     => ['f' => ['type' => Type::string()]],
916
        ]);
917
        self::assertSame($this->interfaceType, $objType->getInterfaces()[0]);
918
    }
919
920
    /**
921
     * @see it('rejects an Object type with incorrectly typed interfaces')
922
     */
923
    public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces() : void
924
    {
925
        $objType = new ObjectType([
926
            'name'       => 'SomeObject',
927
            'interfaces' => new stdClass(),
928
            'fields'     => ['f' => ['type' => Type::string()]],
929
        ]);
930
        $this->expectException(InvariantViolation::class);
931
        $this->expectExceptionMessage(
932
            'SomeObject interfaces must be an Array or a callable which returns an Array.'
933
        );
934
        $objType->getInterfaces();
935
    }
936
937
    /**
938
     * @see it('rejects an Object type with interfaces as a function returning an incorrect type')
939
     */
940
    public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType() : void
941
    {
942
        $objType = new ObjectType([
943
            'name'       => 'SomeObject',
944
            'interfaces' => static function () {
945
                return new stdClass();
946
            },
947
            'fields'     => ['f' => ['type' => Type::string()]],
948
        ]);
949
        $this->expectException(InvariantViolation::class);
950
        $this->expectExceptionMessage(
951
            'SomeObject interfaces must be an Array or a callable which returns an Array.'
952
        );
953
        $objType->getInterfaces();
954
    }
955
956
    // Type System: Object fields must have valid resolve values
957
958
    /**
959
     * @see it('accepts a lambda as an Object field resolver')
960
     */
961
    public function testAcceptsALambdaAsAnObjectFieldResolver() : void
962
    {
963
        $this->expectNotToPerformAssertions();
964
        // should not throw:
965
        $this->schemaWithObjectWithFieldResolver(static function () {
966
        });
967
    }
968
969
    private function schemaWithObjectWithFieldResolver($resolveValue)
970
    {
971
        $BadResolverType = new ObjectType([
972
            'name'   => 'BadResolver',
973
            'fields' => [
974
                'badField' => [
975
                    'type'    => Type::string(),
976
                    'resolve' => $resolveValue,
977
                ],
978
            ],
979
        ]);
980
981
        $schema = new Schema([
982
            'query' => new ObjectType([
983
                'name'   => 'Query',
984
                'fields' => [
985
                    'f' => ['type' => $BadResolverType],
986
                ],
987
            ]),
988
        ]);
989
        $schema->assertValid();
990
991
        return $schema;
992
    }
993
994
    /**
995
     * @see it('rejects an empty Object field resolver')
996
     */
997
    public function testRejectsAnEmptyObjectFieldResolver() : void
998
    {
999
        $this->expectException(InvariantViolation::class);
1000
        $this->expectExceptionMessage(
1001
            'BadResolver.badField field resolver must be a function if provided, but got: []'
1002
        );
1003
        $this->schemaWithObjectWithFieldResolver([]);
1004
    }
1005
1006
    /**
1007
     * @see it('rejects a constant scalar value resolver')
1008
     */
1009
    public function testRejectsAConstantScalarValueResolver() : void
1010
    {
1011
        $this->expectException(InvariantViolation::class);
1012
        $this->expectExceptionMessage(
1013
            'BadResolver.badField field resolver must be a function if provided, but got: 0'
1014
        );
1015
        $this->schemaWithObjectWithFieldResolver(0);
1016
    }
1017
1018
    // Type System: Interface types must be resolvable
1019
1020
    /**
1021
     * @see it('accepts an Interface type defining resolveType')
1022
     */
1023
    public function testAcceptsAnInterfaceTypeDefiningResolveType() : void
1024
    {
1025
        $this->expectNotToPerformAssertions();
1026
        $AnotherInterfaceType = new InterfaceType([
1027
            'name'   => 'AnotherInterface',
1028
            'fields' => ['f' => ['type' => Type::string()]],
1029
        ]);
1030
1031
        // Should not throw:
1032
        $this->schemaWithFieldType(
1033
            new ObjectType([
1034
                'name'       => 'SomeObject',
1035
                'interfaces' => [$AnotherInterfaceType],
1036
                'fields'     => ['f' => ['type' => Type::string()]],
1037
            ])
1038
        );
1039
    }
1040
1041
    private function schemaWithFieldType($type)
1042
    {
1043
        $schema = new Schema([
1044
            'query' => new ObjectType([
1045
                'name'   => 'Query',
1046
                'fields' => ['field' => ['type' => $type]],
1047
            ]),
1048
            'types' => [$type],
1049
        ]);
1050
        $schema->assertValid();
1051
1052
        return $schema;
1053
    }
1054
1055
    /**
1056
     * @see it('accepts an Interface with implementing type defining isTypeOf')
1057
     */
1058
    public function testAcceptsAnInterfaceWithImplementingTypeDefiningIsTypeOf() : void
1059
    {
1060
        $this->expectNotToPerformAssertions();
1061
        $InterfaceTypeWithoutResolveType = new InterfaceType([
1062
            'name'   => 'InterfaceTypeWithoutResolveType',
1063
            'fields' => ['f' => ['type' => Type::string()]],
1064
        ]);
1065
1066
        // Should not throw:
1067
        $this->schemaWithFieldType(
1068
            new ObjectType([
1069
                'name'       => 'SomeObject',
1070
                'interfaces' => [$InterfaceTypeWithoutResolveType],
1071
                'fields'     => ['f' => ['type' => Type::string()]],
1072
            ])
1073
        );
1074
    }
1075
1076
    /**
1077
     * @see it('accepts an Interface type defining resolveType with implementing type defining isTypeOf')
1078
     */
1079
    public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf() : void
1080
    {
1081
        $this->expectNotToPerformAssertions();
1082
        $AnotherInterfaceType = new InterfaceType([
1083
            'name'   => 'AnotherInterface',
1084
            'fields' => ['f' => ['type' => Type::string()]],
1085
        ]);
1086
1087
        // Should not throw:
1088
        $this->schemaWithFieldType(
1089
            new ObjectType([
1090
                'name'       => 'SomeObject',
1091
                'interfaces' => [$AnotherInterfaceType],
1092
                'fields'     => ['f' => ['type' => Type::string()]],
1093
            ])
1094
        );
1095
    }
1096
1097
    /**
1098
     * @see it('rejects an Interface type with an incorrect type for resolveType')
1099
     */
1100
    public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType() : void
1101
    {
1102
        $this->expectException(InvariantViolation::class);
1103
        $this->expectExceptionMessage(
1104
            'AnotherInterface must provide "resolveType" as a function, but got: instance of stdClass'
1105
        );
1106
1107
        $type = new InterfaceType([
1108
            'name'        => 'AnotherInterface',
1109
            'resolveType' => new stdClass(),
1110
            'fields'      => ['f' => ['type' => Type::string()]],
1111
        ]);
1112
        $type->assertValid();
1113
    }
1114
1115
    // Type System: Union types must be resolvable
1116
1117
    /**
1118
     * @see it('accepts a Union type defining resolveType')
1119
     */
1120
    public function testAcceptsAUnionTypeDefiningResolveType() : void
1121
    {
1122
        $this->expectNotToPerformAssertions();
1123
        // Should not throw:
1124
        $this->schemaWithFieldType(
1125
            new UnionType([
1126
                'name'  => 'SomeUnion',
1127
                'types' => [$this->objectType],
1128
            ])
1129
        );
1130
    }
1131
1132
    /**
1133
     * @see it('accepts a Union of Object types defining isTypeOf')
1134
     */
1135
    public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf() : void
1136
    {
1137
        $this->expectNotToPerformAssertions();
1138
        // Should not throw:
1139
        $this->schemaWithFieldType(
1140
            new UnionType([
1141
                'name'  => 'SomeUnion',
1142
                'types' => [$this->objectWithIsTypeOf],
1143
            ])
1144
        );
1145
    }
1146
1147
    /**
1148
     * @see it('accepts a Union type defining resolveType of Object types defining isTypeOf')
1149
     */
1150
    public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf() : void
1151
    {
1152
        $this->expectNotToPerformAssertions();
1153
        // Should not throw:
1154
        $this->schemaWithFieldType(
1155
            new UnionType([
1156
                'name'  => 'SomeUnion',
1157
                'types' => [$this->objectWithIsTypeOf],
1158
            ])
1159
        );
1160
    }
1161
1162
    /**
1163
     * @see it('rejects an Union type with an incorrect type for resolveType')
1164
     */
1165
    public function testRejectsAnUnionTypeWithAnIncorrectTypeForResolveType() : void
1166
    {
1167
        $this->expectException(InvariantViolation::class);
1168
        $this->expectExceptionMessage(
1169
            'SomeUnion must provide "resolveType" as a function, but got: instance of stdClass'
1170
        );
1171
        $this->schemaWithFieldType(
1172
            new UnionType([
1173
                'name'        => 'SomeUnion',
1174
                'resolveType' => new stdClass(),
1175
                'types'       => [$this->objectWithIsTypeOf],
1176
            ])
1177
        );
1178
    }
1179
1180
    /**
1181
     * @see it('accepts a Scalar type defining serialize')
1182
     */
1183
    public function testAcceptsAScalarTypeDefiningSerialize() : void
1184
    {
1185
        $this->expectNotToPerformAssertions();
1186
        // Should not throw
1187
        $this->schemaWithFieldType(
1188
            new CustomScalarType([
1189
                'name'      => 'SomeScalar',
1190
                'serialize' => static function () {
1191
                    return null;
1192
                },
1193
            ])
1194
        );
1195
    }
1196
1197
    // Type System: Scalar types must be serializable
1198
1199
    /**
1200
     * @see it('rejects a Scalar type not defining serialize')
1201
     */
1202
    public function testRejectsAScalarTypeNotDefiningSerialize() : void
1203
    {
1204
        $this->expectException(InvariantViolation::class);
1205
        $this->expectExceptionMessage(
1206
            'SomeScalar must provide "serialize" function. If this custom Scalar ' .
1207
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
1208
            'functions are also provided.'
1209
        );
1210
        $this->schemaWithFieldType(
1211
            new CustomScalarType(['name' => 'SomeScalar'])
1212
        );
1213
    }
1214
1215
    /**
1216
     * @see it('rejects a Scalar type defining serialize with an incorrect type')
1217
     */
1218
    public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType() : void
1219
    {
1220
        $this->expectException(InvariantViolation::class);
1221
        $this->expectExceptionMessage(
1222
            'SomeScalar must provide "serialize" function. If this custom Scalar ' .
1223
            'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
1224
            'functions are also provided.'
1225
        );
1226
        $this->schemaWithFieldType(
1227
            new CustomScalarType([
1228
                'name'      => 'SomeScalar',
1229
                'serialize' => new stdClass(),
1230
            ])
1231
        );
1232
    }
1233
1234
    /**
1235
     * @see it('accepts a Scalar type defining parseValue and parseLiteral')
1236
     */
1237
    public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral() : void
1238
    {
1239
        $this->expectNotToPerformAssertions();
1240
        // Should not throw:
1241
        $this->schemaWithFieldType(
1242
            new CustomScalarType([
1243
                'name'         => 'SomeScalar',
1244
                'serialize'    => static function () {
1245
                },
1246
                'parseValue'   => static function () {
1247
                },
1248
                'parseLiteral' => static function () {
1249
                },
1250
            ])
1251
        );
1252
    }
1253
1254
    /**
1255
     * @see it('rejects a Scalar type defining parseValue but not parseLiteral')
1256
     */
1257
    public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral() : void
1258
    {
1259
        $this->expectException(InvariantViolation::class);
1260
        $this->expectExceptionMessage(
1261
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1262
        );
1263
        $this->schemaWithFieldType(
1264
            new CustomScalarType([
1265
                'name'       => 'SomeScalar',
1266
                'serialize'  => static function () {
1267
                },
1268
                'parseValue' => static function () {
1269
                },
1270
            ])
1271
        );
1272
    }
1273
1274
    /**
1275
     * @see it('rejects a Scalar type defining parseLiteral but not parseValue')
1276
     */
1277
    public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue() : void
1278
    {
1279
        $this->expectException(InvariantViolation::class);
1280
        $this->expectExceptionMessage(
1281
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1282
        );
1283
        $this->schemaWithFieldType(
1284
            new CustomScalarType([
1285
                'name'         => 'SomeScalar',
1286
                'serialize'    => static function () {
1287
                },
1288
                'parseLiteral' => static function () {
1289
                },
1290
            ])
1291
        );
1292
    }
1293
1294
    /**
1295
     * @see it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type')
1296
     */
1297
    public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType() : void
1298
    {
1299
        $this->expectException(InvariantViolation::class);
1300
        $this->expectExceptionMessage(
1301
            'SomeScalar must provide both "parseValue" and "parseLiteral" functions.'
1302
        );
1303
        $this->schemaWithFieldType(
1304
            new CustomScalarType([
1305
                'name'         => 'SomeScalar',
1306
                'serialize'    => static function () {
1307
                },
1308
                'parseValue'   => new stdClass(),
1309
                'parseLiteral' => new stdClass(),
1310
            ])
1311
        );
1312
    }
1313
1314
    /**
1315
     * @see it('accepts an Object type with an isTypeOf function')
1316
     */
1317
    public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction() : void
1318
    {
1319
        $this->expectNotToPerformAssertions();
1320
        // Should not throw
1321
        $this->schemaWithFieldType(
1322
            new ObjectType([
1323
                'name'   => 'AnotherObject',
1324
                'fields' => ['f' => ['type' => Type::string()]],
1325
            ])
1326
        );
1327
    }
1328
1329
    // Type System: Object types must be assertable
1330
1331
    /**
1332
     * @see it('rejects an Object type with an incorrect type for isTypeOf')
1333
     */
1334
    public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf() : void
1335
    {
1336
        $this->expectException(InvariantViolation::class);
1337
        $this->expectExceptionMessage(
1338
            'AnotherObject must provide "isTypeOf" as a function, but got: instance of stdClass'
1339
        );
1340
        $this->schemaWithFieldType(
1341
            new ObjectType([
1342
                'name'     => 'AnotherObject',
1343
                'isTypeOf' => new stdClass(),
1344
                'fields'   => ['f' => ['type' => Type::string()]],
1345
            ])
1346
        );
1347
    }
1348
1349
    /**
1350
     * @see it('accepts a Union type with array types')
1351
     */
1352
    public function testAcceptsAUnionTypeWithArrayTypes() : void
1353
    {
1354
        $this->expectNotToPerformAssertions();
1355
        // Should not throw:
1356
        $this->schemaWithFieldType(
1357
            new UnionType([
1358
                'name'  => 'SomeUnion',
1359
                'types' => [$this->objectType],
1360
            ])
1361
        );
1362
    }
1363
1364
    // Type System: Union types must be array
1365
1366
    /**
1367
     * @see it('accepts a Union type with function returning an array of types')
1368
     */
1369
    public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes() : void
1370
    {
1371
        $this->expectNotToPerformAssertions();
1372
        $this->schemaWithFieldType(
1373
            new UnionType([
1374
                'name'  => 'SomeUnion',
1375
                'types' => function () {
1376
                    return [$this->objectType];
1377
                },
1378
            ])
1379
        );
1380
    }
1381
1382
    /**
1383
     * @see it('rejects a Union type without types')
1384
     */
1385
    public function testRejectsAUnionTypeWithoutTypes() : void
1386
    {
1387
        $this->expectException(InvariantViolation::class);
1388
        $this->expectExceptionMessage(
1389
            'Must provide Array of types or a callable which returns such an array for Union SomeUnion'
1390
        );
1391
        $this->schemaWithFieldType(
1392
            new UnionType(['name' => 'SomeUnion'])
1393
        );
1394
    }
1395
1396
    /**
1397
     * @see it('rejects a Union type with incorrectly typed types')
1398
     */
1399
    public function testRejectsAUnionTypeWithIncorrectlyTypedTypes() : void
1400
    {
1401
        $this->expectException(InvariantViolation::class);
1402
        $this->expectExceptionMessage(
1403
            'Must provide Array of types or a callable which returns such an array for Union SomeUnion'
1404
        );
1405
        $this->schemaWithFieldType(
1406
            new UnionType([
1407
                'name'  => 'SomeUnion',
1408
                'types' => (object) ['test' => $this->objectType],
1409
            ])
1410
        );
1411
    }
1412
1413
    /**
1414
     * @see it('accepts an Input Object type with fields')
1415
     */
1416
    public function testAcceptsAnInputObjectTypeWithFields() : void
1417
    {
1418
        $inputObjType = new InputObjectType([
1419
            'name'   => 'SomeInputObject',
1420
            'fields' => [
1421
                'f' => ['type' => Type::string()],
1422
            ],
1423
        ]);
1424
        $inputObjType->assertValid();
1425
        self::assertSame(Type::string(), $inputObjType->getField('f')->getType());
1426
    }
1427
1428
    // Type System: Input Objects must have fields
1429
1430
    /**
1431
     * @see it('accepts an Input Object type with a field function')
1432
     */
1433
    public function testAcceptsAnInputObjectTypeWithAFieldFunction() : void
1434
    {
1435
        $inputObjType = new InputObjectType([
1436
            'name'   => 'SomeInputObject',
1437
            'fields' => static function () {
1438
                return [
1439
                    'f' => ['type' => Type::string()],
1440
                ];
1441
            },
1442
        ]);
1443
        $inputObjType->assertValid();
1444
        self::assertSame(Type::string(), $inputObjType->getField('f')->getType());
1445
    }
1446
1447
    /**
1448
     * @see it('rejects an Input Object type with incorrect fields')
1449
     */
1450
    public function testRejectsAnInputObjectTypeWithIncorrectFields() : void
1451
    {
1452
        $inputObjType = new InputObjectType([
1453
            'name'   => 'SomeInputObject',
1454
            'fields' => [],
1455
        ]);
1456
        $this->expectException(InvariantViolation::class);
1457
        $this->expectExceptionMessage(
1458
            'SomeInputObject fields must be an associative array with field names as keys or a callable ' .
1459
            'which returns such an array.'
1460
        );
1461
        $inputObjType->assertValid();
1462
    }
1463
1464
    /**
1465
     * @see it('rejects an Input Object type with fields function that returns incorrect type')
1466
     */
1467
    public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorrectType() : void
1468
    {
1469
        $inputObjType = new InputObjectType([
1470
            'name'   => 'SomeInputObject',
1471
            'fields' => static function () {
1472
                return [];
1473
            },
1474
        ]);
1475
        $this->expectException(InvariantViolation::class);
1476
        $this->expectExceptionMessage(
1477
            'SomeInputObject fields must be an associative array with field names as keys or a ' .
1478
            'callable which returns such an array.'
1479
        );
1480
        $inputObjType->assertValid();
1481
    }
1482
1483
    /**
1484
     * @see it('rejects an Input Object type with resolvers')
1485
     */
1486
    public function testRejectsAnInputObjectTypeWithResolvers() : void
1487
    {
1488
        $inputObjType = new InputObjectType([
1489
            'name'   => 'SomeInputObject',
1490
            'fields' => [
1491
                'f' => [
1492
                    'type'    => Type::string(),
1493
                    'resolve' => static function () {
1494
                        return 0;
1495
                    },
1496
                ],
1497
            ],
1498
        ]);
1499
        $this->expectException(InvariantViolation::class);
1500
        $this->expectExceptionMessage(
1501
            'SomeInputObject.f field type has a resolve property, ' .
1502
            'but Input Types cannot define resolvers.'
1503
        );
1504
        $inputObjType->assertValid();
1505
    }
1506
1507
    // Type System: Input Object fields must not have resolvers
1508
1509
    /**
1510
     * @see it('rejects an Input Object type with resolver constant')
1511
     */
1512
    public function testRejectsAnInputObjectTypeWithResolverConstant() : void
1513
    {
1514
        $inputObjType = new InputObjectType([
1515
            'name'   => 'SomeInputObject',
1516
            'fields' => [
1517
                'f' => [
1518
                    'type'    => Type::string(),
1519
                    'resolve' => new stdClass(),
1520
                ],
1521
            ],
1522
        ]);
1523
        $this->expectException(InvariantViolation::class);
1524
        $this->expectExceptionMessage(
1525
            'SomeInputObject.f field type has a resolve property, ' .
1526
            'but Input Types cannot define resolvers.'
1527
        );
1528
        $inputObjType->assertValid();
1529
    }
1530
1531
    /**
1532
     * @see it('accepts a well defined Enum type with empty value definition')
1533
     */
1534
    public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition() : void
1535
    {
1536
        $enumType = new EnumType([
1537
            'name'   => 'SomeEnum',
1538
            'values' => [
1539
                'FOO' => [],
1540
                'BAR' => [],
1541
            ],
1542
        ]);
1543
        self::assertEquals('FOO', $enumType->getValue('FOO')->value);
1544
        self::assertEquals('BAR', $enumType->getValue('BAR')->value);
1545
    }
1546
1547
    // Type System: Enum types must be well defined
1548
1549
    /**
1550
     * @see it('accepts a well defined Enum type with internal value definition')
1551
     */
1552
    public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition() : void
1553
    {
1554
        $enumType = new EnumType([
1555
            'name'   => 'SomeEnum',
1556
            'values' => [
1557
                'FOO' => ['value' => 10],
1558
                'BAR' => ['value' => 20],
1559
            ],
1560
        ]);
1561
        self::assertEquals(10, $enumType->getValue('FOO')->value);
1562
        self::assertEquals(20, $enumType->getValue('BAR')->value);
1563
    }
1564
1565
    /**
1566
     * @see it('rejects an Enum type with incorrectly typed values')
1567
     */
1568
    public function testRejectsAnEnumTypeWithIncorrectlyTypedValues() : void
1569
    {
1570
        $enumType = new EnumType([
1571
            'name'   => 'SomeEnum',
1572
            'values' => [['FOO' => 10]],
1573
        ]);
1574
        $this->expectException(InvariantViolation::class);
1575
        $this->expectExceptionMessage(
1576
            'SomeEnum values must be an array with value names as keys.'
1577
        );
1578
        $enumType->assertValid();
1579
    }
1580
1581
    /**
1582
     * @see it('does not allow isDeprecated without deprecationReason on enum')
1583
     */
1584
    public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() : void
1585
    {
1586
        $enumType = new EnumType([
1587
            'name'   => 'SomeEnum',
1588
            'values' => [
1589
                'FOO' => ['isDeprecated' => true],
1590
            ],
1591
        ]);
1592
        $this->expectException(InvariantViolation::class);
1593
        $this->expectExceptionMessage(
1594
            'SomeEnum.FOO should provide "deprecationReason" instead ' .
1595
            'of "isDeprecated".'
1596
        );
1597
        $enumType->assertValid();
1598
    }
1599
1600
    /**
1601
     * @see it('rejects a Schema which redefines a built-in type')
1602
     */
1603
    public function testRejectsASchemaWhichRedefinesABuiltInType() : void
1604
    {
1605
        $FakeString = new CustomScalarType([
1606
            'name'      => 'String',
1607
            'serialize' => static function () {
1608
            },
1609
        ]);
1610
1611
        $QueryType = new ObjectType([
1612
            'name'   => 'Query',
1613
            'fields' => [
1614
                'normal' => ['type' => Type::string()],
1615
                'fake'   => ['type' => $FakeString],
1616
            ],
1617
        ]);
1618
1619
        $this->expectException(InvariantViolation::class);
1620
        $this->expectExceptionMessage(
1621
            'Schema must contain unique named types but contains multiple types named "String" ' .
1622
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1623
        );
1624
        $schema = new Schema(['query' => $QueryType]);
1625
        $schema->assertValid();
1626
    }
1627
1628
    // Type System: A Schema must contain uniquely named types
1629
1630
    /**
1631
     * @see it('rejects a Schema which defines an object type twice')
1632
     */
1633
    public function testRejectsASchemaWhichDefinesAnObjectTypeTwice() : void
1634
    {
1635
        $A = new ObjectType([
1636
            'name'   => 'SameName',
1637
            'fields' => ['f' => ['type' => Type::string()]],
1638
        ]);
1639
1640
        $B = new ObjectType([
1641
            'name'   => 'SameName',
1642
            'fields' => ['f' => ['type' => Type::string()]],
1643
        ]);
1644
1645
        $QueryType = new ObjectType([
1646
            'name'   => 'Query',
1647
            'fields' => [
1648
                'a' => ['type' => $A],
1649
                'b' => ['type' => $B],
1650
            ],
1651
        ]);
1652
        $this->expectException(InvariantViolation::class);
1653
        $this->expectExceptionMessage(
1654
            'Schema must contain unique named types but contains multiple types named "SameName" ' .
1655
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1656
        );
1657
        $schema = new Schema(['query' => $QueryType]);
1658
        $schema->assertValid();
1659
    }
1660
1661
    /**
1662
     * @see it('rejects a Schema which have same named objects implementing an interface')
1663
     */
1664
    public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface() : void
1665
    {
1666
        $AnotherInterface = new InterfaceType([
1667
            'name'   => 'AnotherInterface',
1668
            'fields' => ['f' => ['type' => Type::string()]],
1669
        ]);
1670
1671
        $FirstBadObject = new ObjectType([
1672
            'name'       => 'BadObject',
1673
            'interfaces' => [$AnotherInterface],
1674
            'fields'     => ['f' => ['type' => Type::string()]],
1675
        ]);
1676
1677
        $SecondBadObject = new ObjectType([
1678
            'name'       => 'BadObject',
1679
            'interfaces' => [$AnotherInterface],
1680
            'fields'     => ['f' => ['type' => Type::string()]],
1681
        ]);
1682
1683
        $QueryType = new ObjectType([
1684
            'name'   => 'Query',
1685
            'fields' => [
1686
                'iface' => ['type' => $AnotherInterface],
1687
            ],
1688
        ]);
1689
1690
        $this->expectException(InvariantViolation::class);
1691
        $this->expectExceptionMessage(
1692
            'Schema must contain unique named types but contains multiple types named "BadObject" ' .
1693
            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
1694
        );
1695
        $schema = new Schema([
1696
            'query' => $QueryType,
1697
            'types' => [$FirstBadObject, $SecondBadObject],
1698
        ]);
1699
        $schema->assertValid();
1700
    }
1701
1702
    public function objectWithIsTypeOf() : ObjectType
1703
    {
1704
        return new ObjectType([
1705
            'name'   => 'ObjectWithIsTypeOf',
1706
            'fields' => ['f' => ['type' => Type::string()]],
1707
        ]);
1708
    }
1709
}
1710