Completed
Pull Request — master (#22)
by
unknown
03:33
created

ProcessorTest::testComplexityReducer()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 84
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 84
rs 8.4704
cc 4
eloc 45
nc 2
nop 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * This file is a part of GraphQL project.
4
 *
5
 * @author Alexandr Viniychuk <[email protected]>
6
 * created: 11:02 PM 5/13/16
7
 */
8
9
namespace Youshido\Tests\Schema;
10
11
12
use Youshido\GraphQL\Execution\Processor;
13
use Youshido\GraphQL\Execution\ResolveInfo;
14
use Youshido\GraphQL\Field\Field;
15
use Youshido\GraphQL\Schema\Schema;
16
use Youshido\GraphQL\Type\ListType\ListType;
17
use Youshido\GraphQL\Type\NonNullType;
18
use Youshido\GraphQL\Type\Object\ObjectType;
19
use Youshido\GraphQL\Type\Scalar\BooleanType;
20
use Youshido\GraphQL\Type\Scalar\IntType;
21
use Youshido\GraphQL\Type\Scalar\StringType;
22
use Youshido\GraphQL\Type\Union\UnionType;
23
use Youshido\Tests\DataProvider\TestEmptySchema;
24
use Youshido\Tests\DataProvider\TestEnumType;
25
use Youshido\Tests\DataProvider\TestInterfaceType;
26
use Youshido\Tests\DataProvider\TestObjectType;
27
use Youshido\Tests\DataProvider\TestSchema;
28
29
class ProcessorTest extends \PHPUnit_Framework_TestCase
30
{
31
32
    private $_counter = 0;
33
34
    /**
35
     * @expectedException \Youshido\GraphQL\Validator\Exception\ConfigurationException
36
     * @expectedExceptionMessage Schema has to have fields
37
     */
38
    public function testInit()
39
    {
40
        new Processor(new TestEmptySchema());
41
    }
42
43
    public function testEmptyQueries()
44
    {
45
        $processor = new Processor(new TestSchema());
46
        $processor->processPayload('');
47
        $this->assertEquals(['errors' => [
48
            ['message' => 'Must provide an operation.']
49
        ]], $processor->getResponseData());
50
51
        $processor->processPayload('{ me { name } }');
52
        $this->assertEquals(['data' => [
53
            'me' => ['name' => 'John']
54
        ]], $processor->getResponseData());
55
56
    }
57
58
  public function testNestedVariables() {
59
    $processor = new Processor(new TestSchema());
60
    $noArgsQuery = '{ me { echo(value:"foo") } }';
61
    $expectedData = ['data' => ['me' => ['echo' => 'foo']]];
62
    $processor->processPayload($noArgsQuery, ['value' => 'foo']);
63
    $this->assertEquals($expectedData, $processor->getResponseData());
64
65
    $parameterizedFieldQuery =
66
        'query nestedFieldQuery($value:String!){
67
          me {
68
            echo(value:$value)
69
          }
70
        }';
71
    $processor->processPayload($parameterizedFieldQuery, ['value' => 'foo']);
72
    $this->assertEquals($expectedData, $processor->getResponseData());
73
74
    $parameterizedQueryQuery =
75
        'query nestedQueryQuery($value:Int){
76
          me {
77
            location(noop:$value) {
78
              address
79
            }
80
          }
81
        }';
82
    $processor->processPayload($parameterizedQueryQuery, ['value' => 1]);
83
    $this->assertArrayNotHasKey('errors', $processor->getResponseData());
84
  }
85
86 View Code Duplication
    public function testListNullResponse()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
87
    {
88
        $processor = new Processor(new Schema([
89
            'query' => new ObjectType([
90
                'name' => 'RootQuery',
91
                'fields' => [
92
                    'list' => [
93
                        'type' => new ListType(new StringType()),
94
                        'resolve' => function() {
95
                            return null;
96
                        }
97
                    ]
98
                ]
99
            ])
100
        ]));
101
        $data = $processor->processPayload(' { list }')->getResponseData();
102
        $this->assertEquals(['data' => ['list' => null]], $data);
103
    }
104
105
106 View Code Duplication
    public function testSubscriptionNullResponse()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
107
    {
108
        $processor = new Processor(new Schema([
109
            'query' => new ObjectType([
110
                'name' => 'RootQuery',
111
                'fields' => [
112
                    'list' => [
113
                        'type' => new ListType(new StringType()),
114
                        'resolve' => function() {
115
                            return null;
116
                        }
117
                    ]
118
                ]
119
            ])
120
        ]));
121
        $data = $processor->processPayload(' { __schema { subscriptionType { name } } }')->getResponseData();
122
        $this->assertEquals(['data' => ['__schema' => ['subscriptionType' => null]]], $data);
123
    }
124
125
    public function testSchemaOperations()
126
    {
127
        $schema    = new Schema([
128
            'query' => new ObjectType([
129
                'name'   => 'RootQuery',
130
                'fields' => [
131
                    'me'                => [
132
                        'type'    => new ObjectType([
133
                            'name'   => 'User',
134
                            'fields' => [
135
                                'firstName' => [
136
                                    'type'    => new StringType(),
137
                                    'args'    => [
138
                                        'shorten' => new BooleanType()
139
                                    ],
140
                                    'resolve' => function ($value, $args) {
141
                                        return empty($args['shorten']) ? $value : $value;
142
                                    }
143
                                ],
144
                                'lastName'  => new StringType(),
145
                                'code'      => new StringType(),
146
                            ]
147
                        ]),
148
                        'resolve' => function ($value, $args) {
149
                            $data = ['firstName' => 'John', 'code' => '007'];
150
                            if (!empty($args['upper'])) {
151
                                foreach ($data as $key => $value) {
152
                                    $data[$key] = strtoupper($value);
153
                                }
154
                            }
155
156
                            return $data;
157
                        },
158
                        'args'    => [
159
                            'upper' => [
160
                                'type'    => new BooleanType(),
161
                                'default' => false
162
                            ]
163
                        ]
164
                    ],
165
                    'randomUser'        => [
166
                        'type'    => new TestObjectType(),
167
                        'resolve' => function () {
168
                            return ['invalidField' => 'John'];
169
                        }
170
                    ],
171
                    'invalidValueQuery' => [
172
                        'type'    => new TestObjectType(),
173
                        'resolve' => function () {
174
                            return 'stringValue';
175
                        }
176
                    ],
177
                ],
178
            ])
179
        ]);
180
        $processor = new Processor($schema);
181
182
        $processor->processPayload('{ me { firstName } }');
183
        $this->assertEquals(['data' => ['me' => ['firstName' => 'John']]], $processor->getResponseData());
184
185
        $processor->processPayload('{ me { firstName, lastName } }');
186
        $this->assertEquals(['data' => ['me' => ['firstName' => 'John', 'lastName' => null]]], $processor->getResponseData());
187
188
        $processor->processPayload('{ me { code } }');
189
        $this->assertEquals(['data' => ['me' => ['code' => 7]]], $processor->getResponseData());
190
191
        $processor->processPayload('{ me(upper:true) { firstName } }');
192
        $this->assertEquals(['data' => ['me' => ['firstName' => 'JOHN']]], $processor->getResponseData());
193
194
        $schema->getMutationType()
195
               ->addField(new Field([
196
                   'name'    => 'increaseCounter',
197
                   'type'    => new IntType(),
198
                   'resolve' => function ($value, $args, ResolveInfo $info) {
0 ignored issues
show
Unused Code introduced by
The parameter $info is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
199
                       return $this->_counter += $args['amount'];
200
                   },
201
                   'args'    => [
202
                       'amount' => [
203
                           'type'    => new IntType(),
204
                           'default' => 1
205
                       ]
206
                   ]
207
               ]))->addField(new Field([
208
                'name'    => 'invalidResolveTypeMutation',
209
                'type'    => new NonNullType(new IntType()),
210
                'resolve' => function () {
211
                    return null;
212
                }
213
            ]))->addField(new Field([
214
                'name'    => 'interfacedMutation',
215
                'type'    => new TestInterfaceType(),
216
                'resolve' => function () {
217
                    return ['name' => 'John'];
218
                }
219
            ]));
220
        $processor->processPayload('mutation { increaseCounter }');
221
        $this->assertEquals(['data' => ['increaseCounter' => 1]], $processor->getResponseData());
222
223
        $processor->processPayload('mutation { invalidMutation }');
224
        $this->assertEquals(['errors' => [['message' => 'Field "invalidMutation" not found in type "RootSchemaMutation"']]], $processor->getResponseData());
225
226
        $processor->processPayload('mutation { increaseCounter(noArg: 2) }');
227
        $this->assertEquals(['errors' => [['message' => 'Unknown argument "noArg" on field "increaseCounter"']]], $processor->getResponseData());
228
229
        $processor->processPayload('mutation { increaseCounter(amount: 2) { invalidProp } }');
230
        $this->assertEquals(['errors' => [['message' => 'Field "invalidProp" not found in type "Int"']], 'data' => ['increaseCounter' => null]], $processor->getResponseData());
231
232
        $processor->processPayload('mutation { increaseCounter(amount: 2) }');
233
        $this->assertEquals(['data' => ['increaseCounter' => 5]], $processor->getResponseData());
234
235
        $processor->processPayload('{ invalidQuery }');
236
        $this->assertEquals(['errors' => [['message' => 'Field "invalidQuery" not found in type "RootQuery"']]], $processor->getResponseData());
237
238
        $processor->processPayload('{ invalidValueQuery { id } }');
239
        $this->assertEquals(['errors' => [['message' => 'Not valid value for OBJECT field invalidValueQuery']], 'data' => ['invalidValueQuery' => null]], $processor->getResponseData());
240
241
        $processor->processPayload('{ me { firstName(shorten: true), middle }}');
242
        $this->assertEquals(['errors' => [['message' => 'Field "middle" not found in type "User"']], 'data' => ['me' => null]], $processor->getResponseData());
243
244
        $processor->processPayload('{ randomUser { region }}');
245
        $this->assertEquals(['errors' => [['message' => 'Property "region" not found in resolve result']]], $processor->getResponseData());
246
247
        $processor->processPayload('mutation { invalidResolveTypeMutation }');
248
        $this->assertEquals(['errors' => [['message' => 'Cannot return null for non-nullable field invalidResolveTypeMutation']], 'data' => ['invalidResolveTypeMutation' => null]], $processor->getResponseData());
249
250
        $processor->processPayload('mutation { user:interfacedMutation { name }  }');
251
        $this->assertEquals(['data' => ['user' => ['name' => 'John']]], $processor->getResponseData());
252
    }
253
254
    public function testListEnumsSchemaOperations()
255
    {
256
        $processor = new Processor(new Schema([
257
            'query' => new ObjectType([
258
                'name'   => 'RootQuery',
259
                'fields' => [
260
                    'listQuery'                 => [
261
                        'type'    => new ListType(new TestEnumType()),
262
                        'resolve' => function () {
263
                            return 'invalid list';
264
                        }
265
                    ],
266
                    'listEnumQuery'             => [
267
                        'type'    => new ListType(new TestEnumType()),
268
                        'resolve' => function () {
269
                            return ['invalid enum'];
270
                        }
271
                    ],
272
                    'invalidEnumQuery'          => [
273
                        'type'    => new TestEnumType(),
274
                        'resolve' => function () {
275
                            return 'invalid enum';
276
                        }
277
                    ],
278
                    'enumQuery'                 => [
279
                        'type'    => new TestEnumType(),
280
                        'resolve' => function () {
281
                            return 1;
282
                        }
283
                    ],
284
                    'invalidNonNullQuery'       => [
285
                        'type'    => new NonNullType(new IntType()),
286
                        'resolve' => function () {
287
                            return null;
288
                        }
289
                    ],
290
                    'invalidNonNullInsideQuery' => [
291
                        'type'    => new NonNullType(new IntType()),
292
                        'resolve' => function () {
293
                            return 'hello';
294
                        }
295
                    ],
296
                    'objectQuery'               => [
297
                        'type'    => new TestObjectType(),
298
                        'resolve' => function () {
299
                            return ['name' => 'John'];
300
                        }
301
                    ],
302
                    'deepObjectQuery'           => [
303
                        'type'    => new ObjectType([
304
                            'name'   => 'deepObject',
305
                            'fields' => [
306
                                'object' => new TestObjectType(),
307
                                'enum'   => new TestEnumType(),
308
                            ],
309
                        ]),
310
                        'resolve' => function () {
311
                            return [
312
                                'object' => [
313
                                    'name' => 'John'
314
                                ],
315
                                'enum'   => 1
316
                            ];
317
                        },
318
                    ],
319
                ]
320
            ])
321
        ]));
322
323
        $processor->processPayload('{ listQuery }');
324
        $this->assertEquals(['errors' => [
325
            ['message' => 'Not valid value for LIST field listQuery']
326
        ], 'data'                     => ['listQuery' => null]], $processor->getResponseData());
327
328
        $processor->processPayload('{ listEnumQuery }');
329
        $this->assertEquals(['errors' => [
330
            ['message' => 'Not valid resolve value in listEnumQuery field']
331
        ], 'data'                     => ['listEnumQuery' => [null]]], $processor->getResponseData());
332
333
        $processor->processPayload('{ invalidEnumQuery }');
334
        $this->assertEquals(['errors' => [
335
            ['message' => 'Not valid value for ENUM field invalidEnumQuery']
336
        ], 'data'                     => ['invalidEnumQuery' => null]], $processor->getResponseData());
337
338
        $processor->processPayload('{ enumQuery }');
339
        $this->assertEquals(['data' => ['enumQuery' => 'FINISHED']], $processor->getResponseData());
340
341
        $processor->processPayload('{ invalidNonNullQuery }');
342
        $this->assertEquals(['errors' => [
343
            ['message' => 'Cannot return null for non-nullable field invalidNonNullQuery']
344
        ], 'data'                     => ['invalidNonNullQuery' => null]], $processor->getResponseData());
345
346
        $processor->processPayload('{ invalidNonNullInsideQuery }');
347
        $this->assertEquals(['errors' => [
348
            ['message' => 'Not valid value for SCALAR field invalidNonNullInsideQuery']
349
        ], 'data'                     => ['invalidNonNullInsideQuery' => null]], $processor->getResponseData());
350
351
        $processor->processPayload('{ test:deepObjectQuery { object { name } } }');
352
        $this->assertEquals(['data' => ['test' => ['object' => ['name' => 'John']]]], $processor->getResponseData());
353
    }
354
355
    public function testTypedFragment()
356
    {
357
358
        $object1 = new ObjectType([
359
            'name'   => 'Object1',
360
            'fields' => [
361
                'id' => ['type' => 'int', 'cost' => 13]
362
            ]
363
        ]);
364
365
        $object2 = new ObjectType([
366
            'name'   => 'Object2',
367
            'fields' => [
368
                'name' => ['type' => 'string']
369
            ]
370
        ]);
371
372
        $object3 = new ObjectType([
373
            'name'   => 'Object3',
374
            'fields' => [
375
                'name' => ['type' => 'string']
376
            ]
377
        ]);
378
379
        $union        = new UnionType([
380
            'name'        => 'TestUnion',
381
            'types'       => [$object1, $object2],
382
            'resolveType' => function ($object) use ($object1, $object2) {
383
                if (isset($object['id'])) {
384
                    return $object1;
385
                }
386
387
                return $object2;
388
            }
389
        ]);
390
        $invalidUnion = new UnionType([
391
            'name'        => 'TestUnion',
392
            'types'       => [$object1, $object2],
393
            'resolveType' => function ($object) use ($object3) {
0 ignored issues
show
Unused Code introduced by
The parameter $object is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
394
                return $object3;
395
            }
396
        ]);
397
        $processor    = new Processor(new Schema([
398
            'query' => new ObjectType([
399
                'name'   => 'RootQuery',
400
                'fields' => [
401
                    'union'        => [
402
                        'type'    => $union,
403
                        'args'    => [
404
                            'type' => ['type' => 'string']
405
                        ],
406
                        'cost' => 10,
407
                        'resolve' => function ($value, $args) {
408
                            if ($args['type'] == 'object1') {
409
                                return [
410
                                    'id' => 43
411
                                ];
412
                            } else {
413
                                return [
414
                                    'name' => 'name resolved'
415
                                ];
416
                            }
417
                        }
418
                    ],
419
                    'invalidUnion' => [
420
                        'type'    => $invalidUnion,
421
                        'resolve' => function () {
422
                            return ['name' => 'name resolved'];
423
                        }
424
                    ],
425
                ]
426
            ])
427
        ]));
428
        $processor->processPayload('{ union(type: "object1") { ... on Object2 { id } } }');
429
        $this->assertEquals(['data' => ['union' => []]], $processor->getResponseData());
430
431
        $processor->processPayload('{ union(type: "object1") { ... on Object1 { name } } }');
432
        $this->assertEquals([
433
            'data'   => [
434
                'union' => []
435
            ],
436
            'errors' => [
437
                ['message' => 'Field "name" not found in type "Object1"']
438
            ]
439
        ], $processor->getResponseData());
440
441
        $processor->processPayload('{ union(type: "object1") { ... on Object1 { id } } }');
442
        $this->assertEquals(['data' => ['union' => ['id' => 43]]], $processor->getResponseData());
443
444
        $processor->processPayload('{ union(type: "asd") { ... on Object2 { name } } }');
445
        $this->assertEquals(['data' => ['union' => ['name' => 'name resolved']]], $processor->getResponseData());
446
447
        $processor->processPayload('{ invalidUnion { ... on Object2 { name } } }');
448
        $this->assertEquals(['errors' => [['message' => 'Type Object3 not exist in types of Object2']]], $processor->getResponseData());
449
450
        $visitor = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor(1000); // arbitrarily high cost
451
        $processor->processPayload('{ union(type: "object1") { ... on Object1 { id } } }', [], [$visitor]);
452
        $this->assertEquals(10 + 13, $visitor->getMemo());
453
454
        $visitor = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor(1000); // arbitrarily high cost
455
        $processor->processPayload('{ union(type: "object1") { ... on Object1 { id }, ... on Object2 { name } } }', [], [$visitor]);
456
        $this->assertEquals(10 + 13 + 1, $visitor->getMemo());
457
458
        // planning phase currently has no knowledge of what types the union will resolve to, this will have the same score as above
459
        $visitor = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor(1000); // arbitrarily high cost
460
        $processor->processPayload('{ union(type: "object2") { ... on Object1 { id }, ... on Object2 { name } } }', [], [$visitor]);
461
        $this->assertEquals(10 + 13 + 1, $visitor->getMemo());
462
    }
463
464
    public function testComplexityReducer() {
465
      $schema = new Schema(
466
          [
467
              'query' => new ObjectType(
468
                  [
469
                      'name'   => 'RootQuery',
470
                      'fields' => [
471
                          'me' => [
472
                              'type'    => new ObjectType(
473
                                  [
474
                                      'name'   => 'User',
475
                                      'fields' => [
476
                                          'firstName' => [
477
                                              'type'    => new StringType(),
478
                                              'args'    => [
479
                                                  'shorten' => new BooleanType()
480
                                              ],
481
                                              'resolve' => function ($value, $args) {
482
                                                return empty($args['shorten']) ? $value : $value;
483
                                              }
484
                                          ],
485
                                          'lastName'  => new StringType(),
486
                                          'code'      => new StringType(),
487
                                          'likes'     => [
488
                                              'type'    => new IntType(),
489
                                              'cost'    => 10,
490
                                              'resolve' => function () {
491
                                                return 42;
492
                                              }
493
                                          ]
494
                                      ]
495
                                  ]
496
                              ),
497
                              'cost' => function ($args, $context, $childCost) {
498
                                $argsCost = isset($args['cost']) ? $args['cost'] : 1;
499
                                return 1 + $argsCost * $childCost;
500
                              },
501
                              'resolve' => function ($value, $args) {
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $args is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
502
                                $data = ['firstName' => 'John', 'code' => '007'];
503
504
                                return $data;
505
                              },
506
                              'args'    => [
507
                                  'cost' => [
508
                                      'type'    => new IntType(),
509
                                      'default' => 1
510
                                  ]
511
                              ]
512
                          ]
513
                      ]
514
                  ]
515
              )
516
          ]
517
      );
518
      $processor = new Processor($schema);
519
520
      $processor->setMaxComplexity(10);
521
522
      $processor->processPayload('{ me { firstName, lastName } }');
523
      $this->assertArrayNotHasKey('error', $processor->getResponseData());
524
525
      $processor->processPayload('{ me { firstName, likes } }');
526
      $this->assertEquals(['errors' => [['message' => 'query exceeded max allowed complexity of 10']]], $processor->getResponseData());
527
528
      // don't let complexity reducer affect query errors
529
      $processor->processPayload('{ me { badfield } }');
530
      $this->assertArraySubset(['errors' => [['message' => 'Field "badfield" not found in type "User"']]], $processor->getResponseData());
531
532
      foreach (range(1,5) as $cost_multiplier) {
533
        $visitor = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor(1000); // arbitrarily high cost
534
        $processor->processPayload("{ me (cost: $cost_multiplier) { firstName, lastName, code, likes } }", ['cost' => $cost_multiplier], [$visitor]);
535
        $expected = 1 + 13 * (1 + $cost_multiplier);
536
        $this->assertEquals($expected, $visitor->getMemo());
537
      }
538
539
      // TODO, variables not yet supported
540
      /*$query = 'query costQuery ($cost: Int) { me (cost: $cost) { firstName, lastName, code, likes } }';
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
541
      foreach (range(1,5) as $cost_multiplier) {
542
        $visitor = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor(1000); // arbitrarily high cost
543
        $processor->processPayload($query, ['cost' => $cost_multiplier], [$visitor]);
544
        $expected = 1 + 13 * (1 + $cost_multiplier);
545
        $this->assertEquals($expected, $visitor->getMemo());
546
      }*/
547
    }
548
}
549