Total Complexity | 84 |
Total Lines | 1735 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like DefinitionTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DefinitionTest, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | class DefinitionTest extends TestCase |
||
28 | { |
||
29 | /** @var ObjectType */ |
||
30 | public $blogImage; |
||
31 | |||
32 | /** @var ObjectType */ |
||
33 | public $blogArticle; |
||
34 | |||
35 | /** @var ObjectType */ |
||
36 | public $blogAuthor; |
||
37 | |||
38 | /** @var ObjectType */ |
||
39 | public $blogMutation; |
||
40 | |||
41 | /** @var ObjectType */ |
||
42 | public $blogQuery; |
||
43 | |||
44 | /** @var ObjectType */ |
||
45 | public $blogSubscription; |
||
46 | |||
47 | /** @var ObjectType */ |
||
48 | public $objectType; |
||
49 | |||
50 | /** @var ObjectType */ |
||
51 | public $objectWithIsTypeOf; |
||
52 | |||
53 | /** @var InterfaceType */ |
||
54 | public $interfaceType; |
||
55 | |||
56 | /** @var UnionType */ |
||
57 | public $unionType; |
||
58 | |||
59 | /** @var EnumType */ |
||
60 | public $enumType; |
||
61 | |||
62 | /** @var InputObjectType */ |
||
63 | public $inputObjectType; |
||
64 | |||
65 | /** @var CustomScalarType */ |
||
66 | public $scalarType; |
||
67 | |||
68 | public function setUp() |
||
69 | { |
||
70 | $this->objectType = new ObjectType(['name' => 'Object', 'fields' => ['tmp' => Type::string()]]); |
||
71 | $this->interfaceType = new InterfaceType(['name' => 'Interface']); |
||
72 | $this->unionType = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]); |
||
73 | $this->enumType = new EnumType(['name' => 'Enum']); |
||
74 | $this->inputObjectType = new InputObjectType(['name' => 'InputObject']); |
||
75 | |||
76 | $this->objectWithIsTypeOf = new ObjectType([ |
||
77 | 'name' => 'ObjectWithIsTypeOf', |
||
78 | 'fields' => ['f' => ['type' => Type::string()]], |
||
79 | ]); |
||
80 | |||
81 | $this->scalarType = new CustomScalarType([ |
||
82 | 'name' => 'Scalar', |
||
83 | 'serialize' => function () { |
||
84 | }, |
||
85 | 'parseValue' => function () { |
||
86 | }, |
||
87 | 'parseLiteral' => function () { |
||
88 | }, |
||
89 | ]); |
||
90 | |||
91 | $this->blogImage = new ObjectType([ |
||
92 | 'name' => 'Image', |
||
93 | 'fields' => [ |
||
94 | 'url' => ['type' => Type::string()], |
||
95 | 'width' => ['type' => Type::int()], |
||
96 | 'height' => ['type' => Type::int()], |
||
97 | ], |
||
98 | ]); |
||
99 | |||
100 | $this->blogAuthor = new ObjectType([ |
||
101 | 'name' => 'Author', |
||
102 | 'fields' => function () { |
||
103 | return [ |
||
104 | 'id' => ['type' => Type::string()], |
||
105 | 'name' => ['type' => Type::string()], |
||
106 | 'pic' => [ |
||
107 | 'type' => $this->blogImage, |
||
108 | 'args' => [ |
||
109 | 'width' => ['type' => Type::int()], |
||
110 | 'height' => ['type' => Type::int()], |
||
111 | ], |
||
112 | ], |
||
113 | 'recentArticle' => $this->blogArticle, |
||
114 | ]; |
||
115 | }, |
||
116 | ]); |
||
117 | |||
118 | $this->blogArticle = new ObjectType([ |
||
119 | 'name' => 'Article', |
||
120 | 'fields' => [ |
||
121 | 'id' => ['type' => Type::string()], |
||
122 | 'isPublished' => ['type' => Type::boolean()], |
||
123 | 'author' => ['type' => $this->blogAuthor], |
||
124 | 'title' => ['type' => Type::string()], |
||
125 | 'body' => ['type' => Type::string()], |
||
126 | ], |
||
127 | ]); |
||
128 | |||
129 | $this->blogQuery = new ObjectType([ |
||
130 | 'name' => 'Query', |
||
131 | 'fields' => [ |
||
132 | 'article' => [ |
||
133 | 'type' => $this->blogArticle, |
||
134 | 'args' => [ |
||
135 | 'id' => ['type' => Type::string()], |
||
136 | ], |
||
137 | ], |
||
138 | 'feed' => ['type' => new ListOfType($this->blogArticle)], |
||
139 | ], |
||
140 | ]); |
||
141 | |||
142 | $this->blogMutation = new ObjectType([ |
||
143 | 'name' => 'Mutation', |
||
144 | 'fields' => [ |
||
145 | 'writeArticle' => ['type' => $this->blogArticle], |
||
146 | ], |
||
147 | ]); |
||
148 | |||
149 | $this->blogSubscription = new ObjectType([ |
||
150 | 'name' => 'Subscription', |
||
151 | 'fields' => [ |
||
152 | 'articleSubscribe' => [ |
||
153 | 'args' => ['id' => ['type' => Type::string()]], |
||
154 | 'type' => $this->blogArticle, |
||
155 | ], |
||
156 | ], |
||
157 | ]); |
||
158 | } |
||
159 | |||
160 | // Type System: Example |
||
161 | |||
162 | /** |
||
163 | * @see it('defines a query only schema') |
||
164 | */ |
||
165 | public function testDefinesAQueryOnlySchema() : void |
||
166 | { |
||
167 | $blogSchema = new Schema([ |
||
168 | 'query' => $this->blogQuery, |
||
169 | ]); |
||
170 | |||
171 | $this->assertSame($blogSchema->getQueryType(), $this->blogQuery); |
||
172 | |||
173 | $articleField = $this->blogQuery->getField('article'); |
||
174 | $this->assertSame($articleField->getType(), $this->blogArticle); |
||
175 | $this->assertSame($articleField->getType()->name, 'Article'); |
||
176 | $this->assertSame($articleField->name, 'article'); |
||
177 | |||
178 | /** @var ObjectType $articleFieldType */ |
||
179 | $articleFieldType = $articleField->getType(); |
||
180 | $titleField = $articleFieldType->getField('title'); |
||
181 | |||
182 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $titleField); |
||
183 | $this->assertSame('title', $titleField->name); |
||
184 | $this->assertSame(Type::string(), $titleField->getType()); |
||
185 | |||
186 | $authorField = $articleFieldType->getField('author'); |
||
187 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $authorField); |
||
188 | |||
189 | /** @var ObjectType $authorFieldType */ |
||
190 | $authorFieldType = $authorField->getType(); |
||
191 | $this->assertSame($this->blogAuthor, $authorFieldType); |
||
192 | |||
193 | $recentArticleField = $authorFieldType->getField('recentArticle'); |
||
194 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $recentArticleField); |
||
195 | $this->assertSame($this->blogArticle, $recentArticleField->getType()); |
||
196 | |||
197 | $feedField = $this->blogQuery->getField('feed'); |
||
198 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $feedField); |
||
199 | |||
200 | /** @var ListOfType $feedFieldType */ |
||
201 | $feedFieldType = $feedField->getType(); |
||
202 | $this->assertInstanceOf('GraphQL\Type\Definition\ListOfType', $feedFieldType); |
||
203 | $this->assertSame($this->blogArticle, $feedFieldType->getWrappedType()); |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * @see it('defines a mutation schema') |
||
208 | */ |
||
209 | public function testDefinesAMutationSchema() : void |
||
210 | { |
||
211 | $schema = new Schema([ |
||
212 | 'query' => $this->blogQuery, |
||
213 | 'mutation' => $this->blogMutation, |
||
214 | ]); |
||
215 | |||
216 | $this->assertSame($this->blogMutation, $schema->getMutationType()); |
||
217 | $writeMutation = $this->blogMutation->getField('writeArticle'); |
||
218 | |||
219 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $writeMutation); |
||
220 | $this->assertSame($this->blogArticle, $writeMutation->getType()); |
||
221 | $this->assertSame('Article', $writeMutation->getType()->name); |
||
222 | $this->assertSame('writeArticle', $writeMutation->name); |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * @see it('defines a subscription schema') |
||
227 | */ |
||
228 | public function testDefinesSubscriptionSchema() : void |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * @see it('defines an enum type with deprecated value') |
||
245 | */ |
||
246 | public function testDefinesEnumTypeWithDeprecatedValue() : void |
||
247 | { |
||
248 | $enumTypeWithDeprecatedValue = new EnumType([ |
||
249 | 'name' => 'EnumWithDeprecatedValue', |
||
250 | 'values' => [ |
||
251 | 'foo' => ['deprecationReason' => 'Just because'], |
||
252 | ], |
||
253 | ]); |
||
254 | |||
255 | $value = $enumTypeWithDeprecatedValue->getValues()[0]; |
||
256 | |||
257 | $this->assertArraySubset( |
||
258 | [ |
||
259 | 'name' => 'foo', |
||
260 | 'description' => null, |
||
261 | 'deprecationReason' => 'Just because', |
||
262 | 'value' => 'foo', |
||
263 | 'astNode' => null, |
||
264 | ], |
||
265 | (array) $value |
||
266 | ); |
||
267 | |||
268 | $this->assertEquals(true, $value->isDeprecated()); |
||
269 | } |
||
270 | |||
271 | /** |
||
272 | * @see it('defines an enum type with a value of `null` and `undefined`') |
||
273 | */ |
||
274 | public function testDefinesAnEnumTypeWithAValueOfNullAndUndefined() : void |
||
275 | { |
||
276 | $EnumTypeWithNullishValue = new EnumType([ |
||
277 | 'name' => 'EnumWithNullishValue', |
||
278 | 'values' => [ |
||
279 | 'NULL' => ['value' => null], |
||
280 | 'UNDEFINED' => ['value' => null], |
||
281 | ], |
||
282 | ]); |
||
283 | |||
284 | $expected = [ |
||
285 | [ |
||
286 | 'name' => 'NULL', |
||
287 | 'description' => null, |
||
288 | 'deprecationReason' => null, |
||
289 | 'value' => null, |
||
290 | 'astNode' => null, |
||
291 | ], |
||
292 | [ |
||
293 | 'name' => 'UNDEFINED', |
||
294 | 'description' => null, |
||
295 | 'deprecationReason' => null, |
||
296 | 'value' => null, |
||
297 | 'astNode' => null, |
||
298 | ], |
||
299 | ]; |
||
300 | |||
301 | $actual = $EnumTypeWithNullishValue->getValues(); |
||
302 | |||
303 | $this->assertEquals(count($expected), count($actual)); |
||
304 | $this->assertArraySubset($expected[0], (array) $actual[0]); |
||
305 | $this->assertArraySubset($expected[1], (array) $actual[1]); |
||
306 | } |
||
307 | |||
308 | /** |
||
309 | * @see it('defines an object type with deprecated field') |
||
310 | */ |
||
311 | public function testDefinesAnObjectTypeWithDeprecatedField() : void |
||
312 | { |
||
313 | $TypeWithDeprecatedField = new ObjectType([ |
||
314 | 'name' => 'foo', |
||
315 | 'fields' => [ |
||
316 | 'bar' => [ |
||
317 | 'type' => Type::string(), |
||
318 | 'deprecationReason' => 'A terrible reason', |
||
319 | ], |
||
320 | ], |
||
321 | ]); |
||
322 | |||
323 | $field = $TypeWithDeprecatedField->getField('bar'); |
||
324 | |||
325 | $this->assertEquals(Type::string(), $field->getType()); |
||
326 | $this->assertEquals(true, $field->isDeprecated()); |
||
327 | $this->assertEquals('A terrible reason', $field->deprecationReason); |
||
328 | $this->assertEquals('bar', $field->name); |
||
329 | $this->assertEquals([], $field->args); |
||
330 | } |
||
331 | |||
332 | /** |
||
333 | * @see it('includes nested input objects in the map') |
||
334 | */ |
||
335 | public function testIncludesNestedInputObjectInTheMap() : void |
||
336 | { |
||
337 | $nestedInputObject = new InputObjectType([ |
||
338 | 'name' => 'NestedInputObject', |
||
339 | 'fields' => ['value' => ['type' => Type::string()]], |
||
340 | ]); |
||
341 | $someInputObject = new InputObjectType([ |
||
342 | 'name' => 'SomeInputObject', |
||
343 | 'fields' => ['nested' => ['type' => $nestedInputObject]], |
||
344 | ]); |
||
345 | $someMutation = new ObjectType([ |
||
346 | 'name' => 'SomeMutation', |
||
347 | 'fields' => [ |
||
348 | 'mutateSomething' => [ |
||
349 | 'type' => $this->blogArticle, |
||
350 | 'args' => ['input' => ['type' => $someInputObject]], |
||
351 | ], |
||
352 | ], |
||
353 | ]); |
||
354 | |||
355 | $schema = new Schema([ |
||
356 | 'query' => $this->blogQuery, |
||
357 | 'mutation' => $someMutation, |
||
358 | ]); |
||
359 | $this->assertSame($nestedInputObject, $schema->getType('NestedInputObject')); |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * @see it('includes interface possible types in the type map') |
||
364 | */ |
||
365 | public function testIncludesInterfaceSubtypesInTheTypeMap() : void |
||
366 | { |
||
367 | $someInterface = new InterfaceType([ |
||
368 | 'name' => 'SomeInterface', |
||
369 | 'fields' => [ |
||
370 | 'f' => ['type' => Type::int()], |
||
371 | ], |
||
372 | ]); |
||
373 | |||
374 | $someSubtype = new ObjectType([ |
||
375 | 'name' => 'SomeSubtype', |
||
376 | 'fields' => [ |
||
377 | 'f' => ['type' => Type::int()], |
||
378 | ], |
||
379 | 'interfaces' => [$someInterface], |
||
380 | ]); |
||
381 | |||
382 | $schema = new Schema([ |
||
383 | 'query' => new ObjectType([ |
||
384 | 'name' => 'Query', |
||
385 | 'fields' => [ |
||
386 | 'iface' => ['type' => $someInterface], |
||
387 | ], |
||
388 | ]), |
||
389 | 'types' => [$someSubtype], |
||
390 | ]); |
||
391 | $this->assertSame($someSubtype, $schema->getType('SomeSubtype')); |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * @see it('includes interfaces' thunk subtypes in the type map') |
||
396 | */ |
||
397 | public function testIncludesInterfacesThunkSubtypesInTheTypeMap() : void |
||
398 | { |
||
399 | $someInterface = null; |
||
400 | |||
401 | $someSubtype = new ObjectType([ |
||
402 | 'name' => 'SomeSubtype', |
||
403 | 'fields' => [ |
||
404 | 'f' => ['type' => Type::int()], |
||
405 | ], |
||
406 | 'interfaces' => function () use (&$someInterface) { |
||
407 | return [$someInterface]; |
||
408 | }, |
||
409 | ]); |
||
410 | |||
411 | $someInterface = new InterfaceType([ |
||
412 | 'name' => 'SomeInterface', |
||
413 | 'fields' => [ |
||
414 | 'f' => ['type' => Type::int()], |
||
415 | ], |
||
416 | ]); |
||
417 | |||
418 | $schema = new Schema([ |
||
419 | 'query' => new ObjectType([ |
||
420 | 'name' => 'Query', |
||
421 | 'fields' => [ |
||
422 | 'iface' => ['type' => $someInterface], |
||
423 | ], |
||
424 | ]), |
||
425 | 'types' => [$someSubtype], |
||
426 | ]); |
||
427 | |||
428 | $this->assertSame($someSubtype, $schema->getType('SomeSubtype')); |
||
429 | } |
||
430 | |||
431 | /** |
||
432 | * @see it('stringifies simple types') |
||
433 | */ |
||
434 | public function testStringifiesSimpleTypes() : void |
||
435 | { |
||
436 | $this->assertSame('Int', (string) Type::int()); |
||
437 | $this->assertSame('Article', (string) $this->blogArticle); |
||
438 | |||
439 | $this->assertSame('Interface', (string) $this->interfaceType); |
||
440 | $this->assertSame('Union', (string) $this->unionType); |
||
441 | $this->assertSame('Enum', (string) $this->enumType); |
||
442 | $this->assertSame('InputObject', (string) $this->inputObjectType); |
||
443 | $this->assertSame('Object', (string) $this->objectType); |
||
444 | |||
445 | $this->assertSame('Int!', (string) new NonNull(Type::int())); |
||
446 | $this->assertSame('[Int]', (string) new ListOfType(Type::int())); |
||
447 | $this->assertSame('[Int]!', (string) new NonNull(new ListOfType(Type::int()))); |
||
448 | $this->assertSame('[Int!]', (string) new ListOfType(new NonNull(Type::int()))); |
||
449 | $this->assertSame('[[Int]]', (string) new ListOfType(new ListOfType(Type::int()))); |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * @see it('JSON stringifies simple types') |
||
454 | */ |
||
455 | public function testJSONStringifiesSimpleTypes() : void |
||
468 | } |
||
469 | |||
470 | /** |
||
471 | * @see it('identifies input types') |
||
472 | */ |
||
473 | public function testIdentifiesInputTypes() : void |
||
474 | { |
||
475 | $expected = [ |
||
476 | [Type::int(), true], |
||
477 | [$this->objectType, false], |
||
478 | [$this->interfaceType, false], |
||
479 | [$this->unionType, false], |
||
480 | [$this->enumType, true], |
||
481 | [$this->inputObjectType, true], |
||
482 | ]; |
||
483 | |||
484 | foreach ($expected as $index => $entry) { |
||
485 | $this->assertSame( |
||
486 | $entry[1], |
||
487 | Type::isInputType($entry[0]), |
||
488 | sprintf('Type %s was detected incorrectly', $entry[0]) |
||
489 | ); |
||
490 | } |
||
491 | } |
||
492 | |||
493 | /** |
||
494 | * @see it('identifies output types') |
||
495 | */ |
||
496 | public function testIdentifiesOutputTypes() : void |
||
512 | ); |
||
513 | } |
||
514 | } |
||
515 | |||
516 | /** |
||
517 | * @see it('prohibits nesting NonNull inside NonNull') |
||
518 | */ |
||
519 | public function testProhibitsNestingNonNullInsideNonNull() : void |
||
520 | { |
||
521 | $this->expectException(InvariantViolation::class); |
||
522 | $this->expectExceptionMessage( |
||
523 | 'Expected Int! to be a GraphQL nullable type.' |
||
524 | ); |
||
525 | Type::nonNull(Type::nonNull(Type::int())); |
||
526 | } |
||
527 | |||
528 | /** |
||
529 | * @see it('allows a thunk for Union member types') |
||
530 | */ |
||
531 | public function testAllowsThunkForUnionTypes() : void |
||
532 | { |
||
533 | $union = new UnionType([ |
||
534 | 'name' => 'ThunkUnion', |
||
535 | 'types' => function () { |
||
536 | return [$this->objectType]; |
||
537 | }, |
||
538 | ]); |
||
539 | |||
540 | $types = $union->getTypes(); |
||
541 | $this->assertEquals(1, count($types)); |
||
542 | $this->assertSame($this->objectType, $types[0]); |
||
543 | } |
||
544 | |||
545 | public function testAllowsRecursiveDefinitions() : void |
||
608 | } |
||
609 | |||
610 | public function testInputObjectTypeAllowsRecursiveDefinitions() : void |
||
611 | { |
||
612 | $called = false; |
||
613 | $inputObject = new InputObjectType([ |
||
614 | 'name' => 'InputObject', |
||
615 | 'fields' => function () use (&$inputObject, &$called) { |
||
616 | $called = true; |
||
617 | |||
618 | return [ |
||
619 | 'value' => ['type' => Type::string()], |
||
620 | 'nested' => ['type' => $inputObject], |
||
621 | ]; |
||
622 | }, |
||
623 | ]); |
||
624 | |||
625 | $someMutation = new ObjectType([ |
||
626 | 'name' => 'SomeMutation', |
||
627 | 'fields' => [ |
||
628 | 'mutateSomething' => [ |
||
629 | 'type' => $this->blogArticle, |
||
630 | 'args' => ['input' => ['type' => $inputObject]], |
||
631 | ], |
||
632 | ], |
||
633 | ]); |
||
634 | |||
635 | $schema = new Schema([ |
||
636 | 'query' => $this->blogQuery, |
||
637 | 'mutation' => $someMutation, |
||
638 | ]); |
||
639 | |||
640 | $this->assertSame($inputObject, $schema->getType('InputObject')); |
||
641 | $this->assertTrue($called); |
||
642 | $this->assertEquals(count($inputObject->getFields()), 2); |
||
643 | $this->assertSame($inputObject->getField('nested')->getType(), $inputObject); |
||
644 | $this->assertSame($someMutation->getField('mutateSomething')->getArg('input')->getType(), $inputObject); |
||
645 | } |
||
646 | |||
647 | public function testInterfaceTypeAllowsRecursiveDefinitions() : void |
||
648 | { |
||
649 | $called = false; |
||
650 | $interface = new InterfaceType([ |
||
651 | 'name' => 'SomeInterface', |
||
652 | 'fields' => function () use (&$interface, &$called) { |
||
653 | $called = true; |
||
654 | |||
655 | return [ |
||
656 | 'value' => ['type' => Type::string()], |
||
657 | 'nested' => ['type' => $interface], |
||
658 | ]; |
||
659 | }, |
||
660 | ]); |
||
661 | |||
662 | $query = new ObjectType([ |
||
663 | 'name' => 'Query', |
||
664 | 'fields' => [ |
||
665 | 'test' => ['type' => $interface], |
||
666 | ], |
||
667 | ]); |
||
668 | |||
669 | $schema = new Schema(['query' => $query]); |
||
670 | |||
671 | $this->assertSame($interface, $schema->getType('SomeInterface')); |
||
672 | $this->assertTrue($called); |
||
673 | $this->assertEquals(count($interface->getFields()), 2); |
||
674 | $this->assertSame($interface->getField('nested')->getType(), $interface); |
||
675 | $this->assertSame($interface->getField('value')->getType(), Type::string()); |
||
676 | } |
||
677 | |||
678 | public function testAllowsShorthandFieldDefinition() : void |
||
679 | { |
||
680 | $interface = new InterfaceType([ |
||
681 | 'name' => 'SomeInterface', |
||
682 | 'fields' => function () use (&$interface) { |
||
683 | return [ |
||
684 | 'value' => Type::string(), |
||
685 | 'nested' => $interface, |
||
686 | 'withArg' => [ |
||
687 | 'type' => Type::string(), |
||
688 | 'args' => [ |
||
689 | 'arg1' => Type::int(), |
||
690 | ], |
||
691 | ], |
||
692 | ]; |
||
693 | }, |
||
694 | ]); |
||
695 | |||
696 | $query = new ObjectType([ |
||
697 | 'name' => 'Query', |
||
698 | 'fields' => ['test' => $interface], |
||
699 | ]); |
||
700 | |||
701 | $schema = new Schema(['query' => $query]); |
||
702 | |||
703 | $valueField = $schema->getType('SomeInterface')->getField('value'); |
||
704 | $nestedField = $schema->getType('SomeInterface')->getField('nested'); |
||
705 | |||
706 | $this->assertEquals(Type::string(), $valueField->getType()); |
||
707 | $this->assertEquals($interface, $nestedField->getType()); |
||
708 | |||
709 | $withArg = $schema->getType('SomeInterface')->getField('withArg'); |
||
710 | $this->assertEquals(Type::string(), $withArg->getType()); |
||
711 | |||
712 | $this->assertEquals('arg1', $withArg->args[0]->name); |
||
713 | $this->assertEquals(Type::int(), $withArg->args[0]->getType()); |
||
714 | |||
715 | $testField = $schema->getType('Query')->getField('test'); |
||
716 | $this->assertEquals($interface, $testField->getType()); |
||
717 | $this->assertEquals('test', $testField->name); |
||
718 | } |
||
719 | |||
720 | public function testInfersNameFromClassname() : void |
||
721 | { |
||
722 | $myObj = new MyCustomType(); |
||
723 | $this->assertEquals('MyCustom', $myObj->name); |
||
724 | |||
725 | $otherCustom = new OtherCustom(); |
||
726 | $this->assertEquals('OtherCustom', $otherCustom->name); |
||
727 | } |
||
728 | |||
729 | public function testAllowsOverridingInternalTypes() : void |
||
730 | { |
||
731 | $idType = new CustomScalarType([ |
||
732 | 'name' => 'ID', |
||
733 | 'serialize' => function () { |
||
734 | }, |
||
735 | 'parseValue' => function () { |
||
736 | }, |
||
737 | 'parseLiteral' => function () { |
||
738 | }, |
||
739 | ]); |
||
740 | |||
741 | $schema = new Schema([ |
||
742 | 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), |
||
743 | 'types' => [$idType], |
||
744 | ]); |
||
745 | |||
746 | $this->assertSame($idType, $schema->getType('ID')); |
||
747 | } |
||
748 | |||
749 | // Field config must be object |
||
750 | |||
751 | /** |
||
752 | * @see it('accepts an Object type with a field function') |
||
753 | */ |
||
754 | public function testAcceptsAnObjectTypeWithAFieldFunction() : void |
||
755 | { |
||
756 | $objType = new ObjectType([ |
||
757 | 'name' => 'SomeObject', |
||
758 | 'fields' => function () { |
||
759 | return [ |
||
760 | 'f' => ['type' => Type::string()], |
||
761 | ]; |
||
762 | }, |
||
763 | ]); |
||
764 | $objType->assertValid(true); |
||
765 | $this->assertSame(Type::string(), $objType->getField('f')->getType()); |
||
766 | } |
||
767 | |||
768 | /** |
||
769 | * @see it('rejects an Object type field with undefined config') |
||
770 | */ |
||
771 | public function testRejectsAnObjectTypeFieldWithUndefinedConfig() : void |
||
772 | { |
||
773 | $objType = new ObjectType([ |
||
774 | 'name' => 'SomeObject', |
||
775 | 'fields' => ['f' => null], |
||
776 | ]); |
||
777 | $this->expectException(InvariantViolation::class); |
||
778 | $this->expectExceptionMessage( |
||
779 | 'SomeObject.f field config must be an array, but got: null' |
||
780 | ); |
||
781 | $objType->getFields(); |
||
782 | } |
||
783 | |||
784 | /** |
||
785 | * @see it('rejects an Object type with incorrectly typed fields') |
||
786 | */ |
||
787 | public function testRejectsAnObjectTypeWithIncorrectlyTypedFields() : void |
||
788 | { |
||
789 | $objType = new ObjectType([ |
||
790 | 'name' => 'SomeObject', |
||
791 | 'fields' => [['field' => Type::string()]], |
||
792 | ]); |
||
793 | $this->expectException(InvariantViolation::class); |
||
794 | $this->expectExceptionMessage( |
||
795 | 'SomeObject fields must be an associative array with field names as keys or a ' . |
||
796 | 'function which returns such an array.' |
||
797 | ); |
||
798 | $objType->getFields(); |
||
799 | } |
||
800 | |||
801 | /** |
||
802 | * @see it('rejects an Object type with a field function that returns incorrect type') |
||
803 | */ |
||
804 | public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectType() : void |
||
805 | { |
||
806 | $objType = new ObjectType([ |
||
807 | 'name' => 'SomeObject', |
||
808 | 'fields' => function () { |
||
809 | return [['field' => Type::string()]]; |
||
810 | }, |
||
811 | ]); |
||
812 | $this->expectException(InvariantViolation::class); |
||
813 | $this->expectExceptionMessage( |
||
814 | 'SomeObject fields must be an associative array with field names as keys or a ' . |
||
815 | 'function which returns such an array.' |
||
816 | ); |
||
817 | $objType->getFields(); |
||
818 | } |
||
819 | |||
820 | // Field arg config must be object |
||
821 | |||
822 | /** |
||
823 | * @see it('accepts an Object type with field args') |
||
824 | */ |
||
825 | public function testAcceptsAnObjectTypeWithFieldArgs() : void |
||
826 | { |
||
827 | $this->expectNotToPerformAssertions(); |
||
828 | $objType = new ObjectType([ |
||
829 | 'name' => 'SomeObject', |
||
830 | 'fields' => [ |
||
831 | 'goodField' => [ |
||
832 | 'type' => Type::string(), |
||
833 | 'args' => [ |
||
834 | 'goodArg' => ['type' => Type::string()], |
||
835 | ], |
||
836 | ], |
||
837 | ], |
||
838 | ]); |
||
839 | // Should not throw: |
||
840 | $objType->assertValid(); |
||
841 | } |
||
842 | |||
843 | // rejects an Object type with incorrectly typed field args |
||
844 | |||
845 | /** |
||
846 | * @see it('does not allow isDeprecated without deprecationReason on field') |
||
847 | */ |
||
848 | public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField() : void |
||
849 | { |
||
850 | $OldObject = new ObjectType([ |
||
851 | 'name' => 'OldObject', |
||
852 | 'fields' => [ |
||
853 | 'field' => [ |
||
854 | 'type' => Type::string(), |
||
855 | 'isDeprecated' => true, |
||
856 | ], |
||
857 | ], |
||
858 | ]); |
||
859 | |||
860 | $this->expectException(InvariantViolation::class); |
||
861 | $this->expectExceptionMessage( |
||
862 | 'OldObject.field should provide "deprecationReason" instead of "isDeprecated".' |
||
863 | ); |
||
864 | $OldObject->assertValid(); |
||
865 | } |
||
866 | |||
867 | // Object interfaces must be array |
||
868 | |||
869 | /** |
||
870 | * @see it('accepts an Object type with array interfaces') |
||
871 | */ |
||
872 | public function testAcceptsAnObjectTypeWithArrayInterfaces() : void |
||
880 | } |
||
881 | |||
882 | /** |
||
883 | * @see it('accepts an Object type with interfaces as a function returning an array') |
||
884 | */ |
||
885 | public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray() : void |
||
886 | { |
||
887 | $objType = new ObjectType([ |
||
888 | 'name' => 'SomeObject', |
||
889 | 'interfaces' => function () { |
||
890 | return [$this->interfaceType]; |
||
891 | }, |
||
892 | 'fields' => ['f' => ['type' => Type::string()]], |
||
893 | ]); |
||
894 | $this->assertSame($this->interfaceType, $objType->getInterfaces()[0]); |
||
895 | } |
||
896 | |||
897 | /** |
||
898 | * @see it('rejects an Object type with incorrectly typed interfaces') |
||
899 | */ |
||
900 | public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces() : void |
||
901 | { |
||
902 | $objType = new ObjectType([ |
||
903 | 'name' => 'SomeObject', |
||
904 | 'interfaces' => new \stdClass(), |
||
905 | 'fields' => ['f' => ['type' => Type::string()]], |
||
906 | ]); |
||
907 | $this->expectException(InvariantViolation::class); |
||
908 | $this->expectExceptionMessage( |
||
909 | 'SomeObject interfaces must be an Array or a callable which returns an Array.' |
||
910 | ); |
||
911 | $objType->getInterfaces(); |
||
912 | } |
||
913 | |||
914 | /** |
||
915 | * @see it('rejects an Object type with interfaces as a function returning an incorrect type') |
||
916 | */ |
||
917 | public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType() : void |
||
918 | { |
||
919 | $objType = new ObjectType([ |
||
920 | 'name' => 'SomeObject', |
||
921 | 'interfaces' => function () { |
||
922 | return new \stdClass(); |
||
923 | }, |
||
924 | 'fields' => ['f' => ['type' => Type::string()]], |
||
925 | ]); |
||
926 | $this->expectException(InvariantViolation::class); |
||
927 | $this->expectExceptionMessage( |
||
928 | 'SomeObject interfaces must be an Array or a callable which returns an Array.' |
||
929 | ); |
||
930 | $objType->getInterfaces(); |
||
931 | } |
||
932 | |||
933 | // Type System: Object fields must have valid resolve values |
||
934 | |||
935 | /** |
||
936 | * @see it('accepts a lambda as an Object field resolver') |
||
937 | */ |
||
938 | public function testAcceptsALambdaAsAnObjectFieldResolver() : void |
||
939 | { |
||
940 | $this->expectNotToPerformAssertions(); |
||
941 | // should not throw: |
||
942 | $this->schemaWithObjectWithFieldResolver(function () { |
||
943 | }); |
||
944 | } |
||
945 | |||
946 | private function schemaWithObjectWithFieldResolver($resolveValue) |
||
947 | { |
||
948 | $BadResolverType = new ObjectType([ |
||
949 | 'name' => 'BadResolver', |
||
950 | 'fields' => [ |
||
951 | 'badField' => [ |
||
952 | 'type' => Type::string(), |
||
953 | 'resolve' => $resolveValue, |
||
954 | ], |
||
955 | ], |
||
956 | ]); |
||
957 | |||
958 | $schema = new Schema([ |
||
959 | 'query' => new ObjectType([ |
||
960 | 'name' => 'Query', |
||
961 | 'fields' => [ |
||
962 | 'f' => ['type' => $BadResolverType], |
||
963 | ], |
||
964 | ]), |
||
965 | ]); |
||
966 | $schema->assertValid(); |
||
967 | |||
968 | return $schema; |
||
969 | } |
||
970 | |||
971 | /** |
||
972 | * @see it('rejects an empty Object field resolver') |
||
973 | */ |
||
974 | public function testRejectsAnEmptyObjectFieldResolver() : void |
||
975 | { |
||
976 | $this->expectException(InvariantViolation::class); |
||
977 | $this->expectExceptionMessage( |
||
978 | 'BadResolver.badField field resolver must be a function if provided, but got: []' |
||
979 | ); |
||
980 | $this->schemaWithObjectWithFieldResolver([]); |
||
981 | } |
||
982 | |||
983 | /** |
||
984 | * @see it('rejects a constant scalar value resolver') |
||
985 | */ |
||
986 | public function testRejectsAConstantScalarValueResolver() : void |
||
987 | { |
||
988 | $this->expectException(InvariantViolation::class); |
||
989 | $this->expectExceptionMessage( |
||
990 | 'BadResolver.badField field resolver must be a function if provided, but got: 0' |
||
991 | ); |
||
992 | $this->schemaWithObjectWithFieldResolver(0); |
||
993 | } |
||
994 | |||
995 | // Type System: Interface types must be resolvable |
||
996 | |||
997 | /** |
||
998 | * @see it('accepts an Interface type defining resolveType') |
||
999 | */ |
||
1000 | public function testAcceptsAnInterfaceTypeDefiningResolveType() : void |
||
1001 | { |
||
1002 | $this->expectNotToPerformAssertions(); |
||
1003 | $AnotherInterfaceType = new InterfaceType([ |
||
1004 | 'name' => 'AnotherInterface', |
||
1005 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1006 | ]); |
||
1007 | |||
1008 | // Should not throw: |
||
1009 | $this->schemaWithFieldType( |
||
1010 | new ObjectType([ |
||
1011 | 'name' => 'SomeObject', |
||
1012 | 'interfaces' => [$AnotherInterfaceType], |
||
1013 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1014 | ]) |
||
1015 | ); |
||
1016 | } |
||
1017 | |||
1018 | private function schemaWithFieldType($type) |
||
1019 | { |
||
1020 | $schema = new Schema([ |
||
1021 | 'query' => new ObjectType([ |
||
1022 | 'name' => 'Query', |
||
1023 | 'fields' => ['field' => ['type' => $type]], |
||
1024 | ]), |
||
1025 | 'types' => [$type], |
||
1026 | ]); |
||
1027 | $schema->assertValid(); |
||
1028 | |||
1029 | return $schema; |
||
1030 | } |
||
1031 | |||
1032 | /** |
||
1033 | * @see it('accepts an Interface with implementing type defining isTypeOf') |
||
1034 | */ |
||
1035 | public function testAcceptsAnInterfaceWithImplementingTypeDefiningIsTypeOf() : void |
||
1036 | { |
||
1037 | $this->expectNotToPerformAssertions(); |
||
1038 | $InterfaceTypeWithoutResolveType = new InterfaceType([ |
||
1039 | 'name' => 'InterfaceTypeWithoutResolveType', |
||
1040 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1041 | ]); |
||
1042 | |||
1043 | // Should not throw: |
||
1044 | $this->schemaWithFieldType( |
||
1045 | new ObjectType([ |
||
1046 | 'name' => 'SomeObject', |
||
1047 | 'interfaces' => [$InterfaceTypeWithoutResolveType], |
||
1048 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1049 | ]) |
||
1050 | ); |
||
1051 | } |
||
1052 | |||
1053 | /** |
||
1054 | * @see it('accepts an Interface type defining resolveType with implementing type defining isTypeOf') |
||
1055 | */ |
||
1056 | public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf() : void |
||
1057 | { |
||
1058 | $this->expectNotToPerformAssertions(); |
||
1059 | $AnotherInterfaceType = new InterfaceType([ |
||
1060 | 'name' => 'AnotherInterface', |
||
1061 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1062 | ]); |
||
1063 | |||
1064 | // Should not throw: |
||
1065 | $this->schemaWithFieldType( |
||
1066 | new ObjectType([ |
||
1067 | 'name' => 'SomeObject', |
||
1068 | 'interfaces' => [$AnotherInterfaceType], |
||
1069 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1070 | ]) |
||
1071 | ); |
||
1072 | } |
||
1073 | |||
1074 | /** |
||
1075 | * @see it('rejects an Interface type with an incorrect type for resolveType') |
||
1076 | */ |
||
1077 | public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType() : void |
||
1078 | { |
||
1079 | $this->expectException(InvariantViolation::class); |
||
1080 | $this->expectExceptionMessage( |
||
1081 | 'AnotherInterface must provide "resolveType" as a function, but got: instance of stdClass' |
||
1082 | ); |
||
1083 | |||
1084 | $type = new InterfaceType([ |
||
1085 | 'name' => 'AnotherInterface', |
||
1086 | 'resolveType' => new \stdClass(), |
||
1087 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1088 | ]); |
||
1089 | $type->assertValid(); |
||
1090 | } |
||
1091 | |||
1092 | // Type System: Union types must be resolvable |
||
1093 | |||
1094 | /** |
||
1095 | * @see it('accepts a Union type defining resolveType') |
||
1096 | */ |
||
1097 | public function testAcceptsAUnionTypeDefiningResolveType() : void |
||
1105 | ]) |
||
1106 | ); |
||
1107 | } |
||
1108 | |||
1109 | /** |
||
1110 | * @see it('accepts a Union of Object types defining isTypeOf') |
||
1111 | */ |
||
1112 | public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf() : void |
||
1120 | ]) |
||
1121 | ); |
||
1122 | } |
||
1123 | |||
1124 | /** |
||
1125 | * @see it('accepts a Union type defining resolveType of Object types defining isTypeOf') |
||
1126 | */ |
||
1127 | public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf() : void |
||
1128 | { |
||
1129 | $this->expectNotToPerformAssertions(); |
||
1130 | // Should not throw: |
||
1131 | $this->schemaWithFieldType( |
||
1132 | new UnionType([ |
||
1133 | 'name' => 'SomeUnion', |
||
1134 | 'types' => [$this->objectWithIsTypeOf], |
||
1135 | ]) |
||
1136 | ); |
||
1137 | } |
||
1138 | |||
1139 | /** |
||
1140 | * @see it('rejects an Union type with an incorrect type for resolveType') |
||
1141 | */ |
||
1142 | public function testRejectsAnUnionTypeWithAnIncorrectTypeForResolveType() : void |
||
1143 | { |
||
1144 | $this->expectException(InvariantViolation::class); |
||
1145 | $this->expectExceptionMessage( |
||
1146 | 'SomeUnion must provide "resolveType" as a function, but got: instance of stdClass' |
||
1147 | ); |
||
1148 | $this->schemaWithFieldType( |
||
1149 | new UnionType([ |
||
1150 | 'name' => 'SomeUnion', |
||
1151 | 'resolveType' => new \stdClass(), |
||
1152 | 'types' => [$this->objectWithIsTypeOf], |
||
1153 | ]) |
||
1154 | ); |
||
1155 | } |
||
1156 | |||
1157 | /** |
||
1158 | * @see it('accepts a Scalar type defining serialize') |
||
1159 | */ |
||
1160 | public function testAcceptsAScalarTypeDefiningSerialize() : void |
||
1161 | { |
||
1162 | $this->expectNotToPerformAssertions(); |
||
1163 | // Should not throw |
||
1164 | $this->schemaWithFieldType( |
||
1165 | new CustomScalarType([ |
||
1166 | 'name' => 'SomeScalar', |
||
1167 | 'serialize' => function () { |
||
1168 | return null; |
||
1169 | }, |
||
1170 | ]) |
||
1171 | ); |
||
1172 | } |
||
1173 | |||
1174 | // Type System: Scalar types must be serializable |
||
1175 | |||
1176 | /** |
||
1177 | * @see it('rejects a Scalar type not defining serialize') |
||
1178 | */ |
||
1179 | public function testRejectsAScalarTypeNotDefiningSerialize() : void |
||
1180 | { |
||
1181 | $this->expectException(InvariantViolation::class); |
||
1182 | $this->expectExceptionMessage( |
||
1183 | 'SomeScalar must provide "serialize" function. If this custom Scalar ' . |
||
1184 | 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . |
||
1185 | 'functions are also provided.' |
||
1186 | ); |
||
1187 | $this->schemaWithFieldType( |
||
1188 | new CustomScalarType(['name' => 'SomeScalar']) |
||
1189 | ); |
||
1190 | } |
||
1191 | |||
1192 | /** |
||
1193 | * @see it('rejects a Scalar type defining serialize with an incorrect type') |
||
1194 | */ |
||
1195 | public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType() : void |
||
1196 | { |
||
1197 | $this->expectException(InvariantViolation::class); |
||
1198 | $this->expectExceptionMessage( |
||
1199 | 'SomeScalar must provide "serialize" function. If this custom Scalar ' . |
||
1200 | 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . |
||
1201 | 'functions are also provided.' |
||
1202 | ); |
||
1203 | $this->schemaWithFieldType( |
||
1204 | new CustomScalarType([ |
||
1205 | 'name' => 'SomeScalar', |
||
1206 | 'serialize' => new \stdClass(), |
||
1207 | ]) |
||
1208 | ); |
||
1209 | } |
||
1210 | |||
1211 | /** |
||
1212 | * @see it('accepts a Scalar type defining parseValue and parseLiteral') |
||
1213 | */ |
||
1214 | public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral() : void |
||
1215 | { |
||
1216 | $this->expectNotToPerformAssertions(); |
||
1217 | // Should not throw: |
||
1218 | $this->schemaWithFieldType( |
||
1219 | new CustomScalarType([ |
||
1220 | 'name' => 'SomeScalar', |
||
1221 | 'serialize' => function () { |
||
1222 | }, |
||
1223 | 'parseValue' => function () { |
||
1224 | }, |
||
1225 | 'parseLiteral' => function () { |
||
1226 | }, |
||
1227 | ]) |
||
1228 | ); |
||
1229 | } |
||
1230 | |||
1231 | /** |
||
1232 | * @see it('rejects a Scalar type defining parseValue but not parseLiteral') |
||
1233 | */ |
||
1234 | public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral() : void |
||
1235 | { |
||
1236 | $this->expectException(InvariantViolation::class); |
||
1237 | $this->expectExceptionMessage( |
||
1238 | 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' |
||
1239 | ); |
||
1240 | $this->schemaWithFieldType( |
||
1241 | new CustomScalarType([ |
||
1242 | 'name' => 'SomeScalar', |
||
1243 | 'serialize' => function () { |
||
1244 | }, |
||
1245 | 'parseValue' => function () { |
||
1246 | }, |
||
1247 | ]) |
||
1248 | ); |
||
1249 | } |
||
1250 | |||
1251 | /** |
||
1252 | * @see it('rejects a Scalar type defining parseLiteral but not parseValue') |
||
1253 | */ |
||
1254 | public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue() : void |
||
1255 | { |
||
1256 | $this->expectException(InvariantViolation::class); |
||
1257 | $this->expectExceptionMessage( |
||
1258 | 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' |
||
1259 | ); |
||
1260 | $this->schemaWithFieldType( |
||
1261 | new CustomScalarType([ |
||
1262 | 'name' => 'SomeScalar', |
||
1263 | 'serialize' => function () { |
||
1264 | }, |
||
1265 | 'parseLiteral' => function () { |
||
1266 | }, |
||
1267 | ]) |
||
1268 | ); |
||
1269 | } |
||
1270 | |||
1271 | /** |
||
1272 | * @see it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type') |
||
1273 | */ |
||
1274 | public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType() : void |
||
1275 | { |
||
1276 | $this->expectException(InvariantViolation::class); |
||
1277 | $this->expectExceptionMessage( |
||
1278 | 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' |
||
1279 | ); |
||
1280 | $this->schemaWithFieldType( |
||
1281 | new CustomScalarType([ |
||
1282 | 'name' => 'SomeScalar', |
||
1283 | 'serialize' => function () { |
||
1284 | }, |
||
1285 | 'parseValue' => new \stdClass(), |
||
1286 | 'parseLiteral' => new \stdClass(), |
||
1287 | ]) |
||
1288 | ); |
||
1289 | } |
||
1290 | |||
1291 | /** |
||
1292 | * @see it('accepts an Object type with an isTypeOf function') |
||
1293 | */ |
||
1294 | public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction() : void |
||
1295 | { |
||
1296 | $this->expectNotToPerformAssertions(); |
||
1297 | // Should not throw |
||
1298 | $this->schemaWithFieldType( |
||
1299 | new ObjectType([ |
||
1300 | 'name' => 'AnotherObject', |
||
1301 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1302 | ]) |
||
1303 | ); |
||
1304 | } |
||
1305 | |||
1306 | // Type System: Object types must be assertable |
||
1307 | |||
1308 | /** |
||
1309 | * @see it('rejects an Object type with an incorrect type for isTypeOf') |
||
1310 | */ |
||
1311 | public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf() : void |
||
1312 | { |
||
1313 | $this->expectException(InvariantViolation::class); |
||
1314 | $this->expectExceptionMessage( |
||
1315 | 'AnotherObject must provide "isTypeOf" as a function, but got: instance of stdClass' |
||
1316 | ); |
||
1317 | $this->schemaWithFieldType( |
||
1318 | new ObjectType([ |
||
1319 | 'name' => 'AnotherObject', |
||
1320 | 'isTypeOf' => new \stdClass(), |
||
1321 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1322 | ]) |
||
1323 | ); |
||
1324 | } |
||
1325 | |||
1326 | /** |
||
1327 | * @see it('accepts a Union type with array types') |
||
1328 | */ |
||
1329 | public function testAcceptsAUnionTypeWithArrayTypes() : void |
||
1330 | { |
||
1331 | $this->expectNotToPerformAssertions(); |
||
1332 | // Should not throw: |
||
1333 | $this->schemaWithFieldType( |
||
1334 | new UnionType([ |
||
1335 | 'name' => 'SomeUnion', |
||
1336 | 'types' => [$this->objectType], |
||
1337 | ]) |
||
1338 | ); |
||
1339 | } |
||
1340 | |||
1341 | // Type System: Union types must be array |
||
1342 | |||
1343 | /** |
||
1344 | * @see it('accepts a Union type with function returning an array of types') |
||
1345 | */ |
||
1346 | public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes() : void |
||
1347 | { |
||
1348 | $this->expectNotToPerformAssertions(); |
||
1349 | $this->schemaWithFieldType( |
||
1350 | new UnionType([ |
||
1351 | 'name' => 'SomeUnion', |
||
1352 | 'types' => function () { |
||
1353 | return [$this->objectType]; |
||
1354 | }, |
||
1355 | ]) |
||
1356 | ); |
||
1357 | } |
||
1358 | |||
1359 | /** |
||
1360 | * @see it('rejects a Union type without types') |
||
1361 | */ |
||
1362 | public function testRejectsAUnionTypeWithoutTypes() : void |
||
1363 | { |
||
1364 | $this->expectException(InvariantViolation::class); |
||
1365 | $this->expectExceptionMessage( |
||
1366 | 'Must provide Array of types or a callable which returns such an array for Union SomeUnion' |
||
1367 | ); |
||
1368 | $this->schemaWithFieldType( |
||
1369 | new UnionType(['name' => 'SomeUnion']) |
||
1370 | ); |
||
1371 | } |
||
1372 | |||
1373 | /** |
||
1374 | * @see it('rejects a Union type with incorrectly typed types') |
||
1375 | */ |
||
1376 | public function testRejectsAUnionTypeWithIncorrectlyTypedTypes() : void |
||
1377 | { |
||
1378 | $this->expectException(InvariantViolation::class); |
||
1379 | $this->expectExceptionMessage( |
||
1380 | 'Must provide Array of types or a callable which returns such an array for Union SomeUnion' |
||
1381 | ); |
||
1382 | $this->schemaWithFieldType( |
||
1383 | new UnionType([ |
||
1384 | 'name' => 'SomeUnion', |
||
1385 | 'types' => (object) ['test' => $this->objectType], |
||
1386 | ]) |
||
1387 | ); |
||
1388 | } |
||
1389 | |||
1390 | /** |
||
1391 | * @see it('accepts an Input Object type with fields') |
||
1392 | */ |
||
1393 | public function testAcceptsAnInputObjectTypeWithFields() : void |
||
1394 | { |
||
1395 | $inputObjType = new InputObjectType([ |
||
1396 | 'name' => 'SomeInputObject', |
||
1397 | 'fields' => [ |
||
1398 | 'f' => ['type' => Type::string()], |
||
1399 | ], |
||
1400 | ]); |
||
1401 | $inputObjType->assertValid(); |
||
1402 | $this->assertSame(Type::string(), $inputObjType->getField('f')->getType()); |
||
1403 | } |
||
1404 | |||
1405 | // Type System: Input Objects must have fields |
||
1406 | |||
1407 | /** |
||
1408 | * @see it('accepts an Input Object type with a field function') |
||
1409 | */ |
||
1410 | public function testAcceptsAnInputObjectTypeWithAFieldFunction() : void |
||
1411 | { |
||
1412 | $inputObjType = new InputObjectType([ |
||
1413 | 'name' => 'SomeInputObject', |
||
1414 | 'fields' => function () { |
||
1415 | return [ |
||
1416 | 'f' => ['type' => Type::string()], |
||
1417 | ]; |
||
1418 | }, |
||
1419 | ]); |
||
1420 | $inputObjType->assertValid(); |
||
1421 | $this->assertSame(Type::string(), $inputObjType->getField('f')->getType()); |
||
1422 | } |
||
1423 | |||
1424 | /** |
||
1425 | * @see it('rejects an Input Object type with incorrect fields') |
||
1426 | */ |
||
1427 | public function testRejectsAnInputObjectTypeWithIncorrectFields() : void |
||
1428 | { |
||
1429 | $inputObjType = new InputObjectType([ |
||
1430 | 'name' => 'SomeInputObject', |
||
1431 | 'fields' => [], |
||
1432 | ]); |
||
1433 | $this->expectException(InvariantViolation::class); |
||
1434 | $this->expectExceptionMessage( |
||
1435 | 'SomeInputObject fields must be an associative array with field names as keys or a callable ' . |
||
1436 | 'which returns such an array.' |
||
1437 | ); |
||
1438 | $inputObjType->assertValid(); |
||
1439 | } |
||
1440 | |||
1441 | /** |
||
1442 | * @see it('rejects an Input Object type with fields function that returns incorrect type') |
||
1443 | */ |
||
1444 | public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorrectType() : void |
||
1458 | } |
||
1459 | |||
1460 | /** |
||
1461 | * @see it('rejects an Input Object type with resolvers') |
||
1462 | */ |
||
1463 | public function testRejectsAnInputObjectTypeWithResolvers() : void |
||
1464 | { |
||
1465 | $inputObjType = new InputObjectType([ |
||
1466 | 'name' => 'SomeInputObject', |
||
1467 | 'fields' => [ |
||
1468 | 'f' => [ |
||
1469 | 'type' => Type::string(), |
||
1470 | 'resolve' => function () { |
||
1471 | return 0; |
||
1472 | }, |
||
1473 | ], |
||
1474 | ], |
||
1475 | ]); |
||
1476 | $this->expectException(InvariantViolation::class); |
||
1477 | $this->expectExceptionMessage( |
||
1478 | 'SomeInputObject.f field type has a resolve property, ' . |
||
1479 | 'but Input Types cannot define resolvers.' |
||
1480 | ); |
||
1481 | $inputObjType->assertValid(); |
||
1482 | } |
||
1483 | |||
1484 | // Type System: Input Object fields must not have resolvers |
||
1485 | |||
1486 | /** |
||
1487 | * @see it('rejects an Input Object type with resolver constant') |
||
1488 | */ |
||
1489 | public function testRejectsAnInputObjectTypeWithResolverConstant() : void |
||
1490 | { |
||
1491 | $inputObjType = new InputObjectType([ |
||
1492 | 'name' => 'SomeInputObject', |
||
1493 | 'fields' => [ |
||
1494 | 'f' => [ |
||
1495 | 'type' => Type::string(), |
||
1496 | 'resolve' => new \stdClass(), |
||
1497 | ], |
||
1498 | ], |
||
1499 | ]); |
||
1500 | $this->expectException(InvariantViolation::class); |
||
1501 | $this->expectExceptionMessage( |
||
1502 | 'SomeInputObject.f field type has a resolve property, ' . |
||
1503 | 'but Input Types cannot define resolvers.' |
||
1504 | ); |
||
1505 | $inputObjType->assertValid(); |
||
1506 | } |
||
1507 | |||
1508 | /** |
||
1509 | * @see it('accepts a well defined Enum type with empty value definition') |
||
1510 | */ |
||
1511 | public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition() : void |
||
1512 | { |
||
1513 | $enumType = new EnumType([ |
||
1514 | 'name' => 'SomeEnum', |
||
1515 | 'values' => [ |
||
1516 | 'FOO' => [], |
||
1517 | 'BAR' => [], |
||
1518 | ], |
||
1519 | ]); |
||
1520 | $this->assertEquals('FOO', $enumType->getValue('FOO')->value); |
||
1521 | $this->assertEquals('BAR', $enumType->getValue('BAR')->value); |
||
1522 | } |
||
1523 | |||
1524 | // Type System: Enum types must be well defined |
||
1525 | |||
1526 | /** |
||
1527 | * @see it('accepts a well defined Enum type with internal value definition') |
||
1528 | */ |
||
1529 | public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition() : void |
||
1530 | { |
||
1531 | $enumType = new EnumType([ |
||
1532 | 'name' => 'SomeEnum', |
||
1533 | 'values' => [ |
||
1534 | 'FOO' => ['value' => 10], |
||
1535 | 'BAR' => ['value' => 20], |
||
1536 | ], |
||
1537 | ]); |
||
1538 | $this->assertEquals(10, $enumType->getValue('FOO')->value); |
||
1539 | $this->assertEquals(20, $enumType->getValue('BAR')->value); |
||
1540 | } |
||
1541 | |||
1542 | /** |
||
1543 | * @see it('rejects an Enum type with incorrectly typed values') |
||
1544 | */ |
||
1545 | public function testRejectsAnEnumTypeWithIncorrectlyTypedValues() : void |
||
1546 | { |
||
1547 | $enumType = new EnumType([ |
||
1548 | 'name' => 'SomeEnum', |
||
1549 | 'values' => [['FOO' => 10]], |
||
1550 | ]); |
||
1551 | $this->expectException(InvariantViolation::class); |
||
1552 | $this->expectExceptionMessage( |
||
1553 | 'SomeEnum values must be an array with value names as keys.' |
||
1554 | ); |
||
1555 | $enumType->assertValid(); |
||
1556 | } |
||
1557 | |||
1558 | /** |
||
1559 | * @see it('does not allow isDeprecated without deprecationReason on enum') |
||
1560 | */ |
||
1561 | public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() : void |
||
1562 | { |
||
1563 | $enumType = new EnumType([ |
||
1564 | 'name' => 'SomeEnum', |
||
1565 | 'values' => [ |
||
1566 | 'FOO' => ['isDeprecated' => true], |
||
1567 | ], |
||
1568 | ]); |
||
1569 | $this->expectException(InvariantViolation::class); |
||
1570 | $this->expectExceptionMessage( |
||
1571 | 'SomeEnum.FOO should provide "deprecationReason" instead ' . |
||
1572 | 'of "isDeprecated".' |
||
1573 | ); |
||
1574 | $enumType->assertValid(); |
||
1575 | } |
||
1576 | |||
1577 | /** |
||
1578 | * Type System: List must accept only types |
||
1579 | */ |
||
1580 | public function testListMustAcceptOnlyTypes() : void |
||
1581 | { |
||
1582 | $types = [ |
||
1583 | Type::string(), |
||
1584 | $this->scalarType, |
||
1585 | $this->objectType, |
||
1586 | $this->unionType, |
||
1587 | $this->interfaceType, |
||
1588 | $this->enumType, |
||
1589 | $this->inputObjectType, |
||
1590 | Type::listOf(Type::string()), |
||
1591 | Type::nonNull(Type::string()), |
||
1592 | ]; |
||
1593 | |||
1594 | $badTypes = [[], new \stdClass(), '', null]; |
||
1595 | |||
1596 | foreach ($types as $type) { |
||
1597 | try { |
||
1598 | Type::listOf($type); |
||
1599 | } catch (\Throwable $e) { |
||
1600 | $this->fail('List is expected to accept type: ' . get_class($type) . ', but got error: ' . $e->getMessage()); |
||
1601 | } |
||
1602 | } |
||
1603 | foreach ($badTypes as $badType) { |
||
1604 | $typeStr = Utils::printSafe($badType); |
||
1605 | try { |
||
1606 | Type::listOf($badType); |
||
1607 | $this->fail(sprintf('List should not accept %s', $typeStr)); |
||
1608 | } catch (InvariantViolation $e) { |
||
1609 | $this->assertEquals(sprintf('Expected %s to be a GraphQL type.', $typeStr), $e->getMessage()); |
||
1610 | } |
||
1611 | } |
||
1612 | } |
||
1613 | |||
1614 | /** |
||
1615 | * Type System: NonNull must only accept non-nullable types |
||
1616 | */ |
||
1617 | public function testNonNullMustOnlyAcceptNonNullableTypes() : void |
||
1618 | { |
||
1619 | $nullableTypes = [ |
||
1620 | Type::string(), |
||
1621 | $this->scalarType, |
||
1622 | $this->objectType, |
||
1623 | $this->unionType, |
||
1624 | $this->interfaceType, |
||
1625 | $this->enumType, |
||
1626 | $this->inputObjectType, |
||
1627 | Type::listOf(Type::string()), |
||
1628 | Type::listOf(Type::nonNull(Type::string())), |
||
1629 | ]; |
||
1630 | $notNullableTypes = [ |
||
1631 | Type::nonNull(Type::string()), |
||
1632 | [], |
||
1633 | new \stdClass(), |
||
1634 | '', |
||
1635 | null, |
||
1636 | ]; |
||
1637 | foreach ($nullableTypes as $type) { |
||
1638 | try { |
||
1639 | Type::nonNull($type); |
||
1640 | } catch (\Throwable $e) { |
||
1641 | $this->fail('NonNull is expected to accept type: ' . get_class($type) . ', but got error: ' . $e->getMessage()); |
||
1642 | } |
||
1643 | } |
||
1644 | foreach ($notNullableTypes as $badType) { |
||
1645 | $typeStr = Utils::printSafe($badType); |
||
1646 | try { |
||
1647 | Type::nonNull($badType); |
||
1648 | $this->fail(sprintf('Nulls should not accept %s', $typeStr)); |
||
1649 | } catch (InvariantViolation $e) { |
||
1650 | $this->assertEquals(sprintf('Expected %s to be a GraphQL nullable type.', $typeStr), $e->getMessage()); |
||
1651 | } |
||
1652 | } |
||
1653 | } |
||
1654 | |||
1655 | /** |
||
1656 | * @see it('rejects a Schema which redefines a built-in type') |
||
1657 | */ |
||
1658 | public function testRejectsASchemaWhichRedefinesABuiltInType() : void |
||
1659 | { |
||
1660 | $FakeString = new CustomScalarType([ |
||
1661 | 'name' => 'String', |
||
1662 | 'serialize' => function () { |
||
1663 | }, |
||
1664 | ]); |
||
1665 | |||
1666 | $QueryType = new ObjectType([ |
||
1667 | 'name' => 'Query', |
||
1668 | 'fields' => [ |
||
1669 | 'normal' => ['type' => Type::string()], |
||
1670 | 'fake' => ['type' => $FakeString], |
||
1671 | ], |
||
1672 | ]); |
||
1673 | |||
1674 | $this->expectException(InvariantViolation::class); |
||
1675 | $this->expectExceptionMessage( |
||
1676 | 'Schema must contain unique named types but contains multiple types named "String" ' . |
||
1677 | '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).' |
||
1678 | ); |
||
1679 | $schema = new Schema(['query' => $QueryType]); |
||
1680 | $schema->assertValid(); |
||
1681 | } |
||
1682 | |||
1683 | // Type System: A Schema must contain uniquely named types |
||
1684 | |||
1685 | /** |
||
1686 | * @see it('rejects a Schema which defines an object type twice') |
||
1687 | */ |
||
1688 | public function testRejectsASchemaWhichDefinesAnObjectTypeTwice() : void |
||
1689 | { |
||
1690 | $A = new ObjectType([ |
||
1691 | 'name' => 'SameName', |
||
1692 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1693 | ]); |
||
1694 | |||
1695 | $B = new ObjectType([ |
||
1696 | 'name' => 'SameName', |
||
1697 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1698 | ]); |
||
1699 | |||
1700 | $QueryType = new ObjectType([ |
||
1701 | 'name' => 'Query', |
||
1702 | 'fields' => [ |
||
1703 | 'a' => ['type' => $A], |
||
1704 | 'b' => ['type' => $B], |
||
1705 | ], |
||
1706 | ]); |
||
1707 | $this->expectException(InvariantViolation::class); |
||
1708 | $this->expectExceptionMessage( |
||
1709 | 'Schema must contain unique named types but contains multiple types named "SameName" ' . |
||
1710 | '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).' |
||
1711 | ); |
||
1712 | $schema = new Schema(['query' => $QueryType]); |
||
1713 | $schema->assertValid(); |
||
1714 | } |
||
1715 | |||
1716 | /** |
||
1717 | * @see it('rejects a Schema which have same named objects implementing an interface') |
||
1718 | */ |
||
1719 | public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface() : void |
||
1755 | } |
||
1756 | |||
1757 | public function objectWithIsTypeOf() : ObjectType |
||
1758 | { |
||
1759 | return new ObjectType([ |
||
1760 | 'name' => 'ObjectWithIsTypeOf', |
||
1761 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1762 | ]); |
||
1763 | } |
||
1764 | } |
||
1765 |