Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
| 1 | <?php |
||
| 34 | use Youshido\GraphQL\Type\Scalar\IdType; |
||
| 35 | use Youshido\GraphQL\Type\Scalar\IntType; |
||
| 36 | use Youshido\GraphQL\Type\Scalar\StringType; |
||
| 37 | use Youshido\GraphQL\Type\Union\UnionType; |
||
| 38 | use Youshido\Tests\DataProvider\TestEmptySchema; |
||
| 39 | use Youshido\Tests\DataProvider\TestEnumType; |
||
| 40 | use Youshido\Tests\DataProvider\TestInterfaceType; |
||
| 41 | use Youshido\Tests\DataProvider\TestObjectType; |
||
| 42 | use Youshido\Tests\DataProvider\TestSchema; |
||
| 43 | |||
| 44 | class ProcessorTest extends \PHPUnit_Framework_TestCase |
||
| 45 | { |
||
| 46 | private $_counter = 0; |
||
| 47 | |||
| 48 | public function testInit(): void |
||
| 49 | { |
||
| 50 | $processor = new Processor(new TestEmptySchema()); |
||
| 51 | $this->assertEquals([['message' => 'Schema has to have fields']], $processor->getExecutionContext()->getErrorsArray()); |
||
| 52 | } |
||
| 53 | |||
| 54 | public function testEmptyQueries(): void |
||
| 55 | { |
||
| 56 | $processor = new Processor(new TestSchema()); |
||
| 57 | $processor->processPayload(''); |
||
| 58 | $this->assertEquals(['errors' => [ |
||
| 59 | ['message' => 'Must provide an operation.'], |
||
| 60 | ]], $processor->getResponseData()); |
||
| 61 | |||
| 62 | $processor->getExecutionContext()->clearErrors(); |
||
| 63 | $processor->processPayload('{ me { name } }'); |
||
| 64 | $this->assertEquals(['data' => [ |
||
| 65 | 'me' => ['name' => 'John'], |
||
| 66 | ]], $processor->getResponseData()); |
||
| 67 | } |
||
| 68 | |||
| 69 | public function testNestedVariables(): void |
||
| 70 | { |
||
| 71 | $processor = new Processor(new TestSchema()); |
||
| 72 | $noArgsQuery = '{ me { echo(value:"foo") } }'; |
||
| 73 | $expectedData = ['data' => ['me' => ['echo' => 'foo']]]; |
||
| 74 | $processor->processPayload($noArgsQuery, ['value' => 'foo']); |
||
| 75 | $this->assertEquals($expectedData, $processor->getResponseData()); |
||
| 76 | |||
| 77 | $parameterizedFieldQuery = 'query nestedFieldQuery($value:String!){ |
||
| 78 | me { |
||
| 79 | echo(value:$value) |
||
| 80 | } |
||
| 81 | }'; |
||
| 82 | $processor->processPayload($parameterizedFieldQuery, ['value' => 'foo']); |
||
| 83 | $response = $processor->getResponseData(); |
||
| 84 | $this->assertEquals($expectedData, $response); |
||
| 85 | |||
| 86 | $parameterizedQueryQuery = 'query nestedQueryQuery($value:Int){ |
||
| 87 | me { |
||
| 88 | location(noop:$value) { |
||
| 89 | address |
||
| 90 | } |
||
| 91 | } |
||
| 92 | }'; |
||
| 93 | $processor->processPayload($parameterizedQueryQuery, ['value' => 1]); |
||
| 94 | $this->assertArrayNotHasKey('errors', $processor->getResponseData()); |
||
| 95 | } |
||
| 96 | |||
| 97 | public function testNullListVariable(): void |
||
| 98 | { |
||
| 99 | $processor = new Processor(new TestSchema()); |
||
| 100 | $processor->processPayload( |
||
| 101 | 'mutation ($list: [Int], $status: TestEnum) { updateStatus(list: $list, newStatus: $status) }', |
||
| 102 | [ |
||
| 103 | 'list' => null, |
||
| 104 | 'status' => null, |
||
| 105 | ] |
||
| 106 | ); |
||
| 107 | |||
| 108 | $this->assertArrayNotHasKey('errors', $processor->getResponseData()); |
||
| 109 | } |
||
| 110 | |||
| 111 | public function testListNullResponse(): void |
||
| 112 | { |
||
| 113 | $processor = new Processor(new Schema([ |
||
| 114 | 'query' => new ObjectType([ |
||
| 115 | 'name' => 'RootQuery', |
||
| 116 | 'fields' => [ |
||
| 117 | 'list' => [ |
||
| 118 | 'type' => new ListType(new StringType()), |
||
| 119 | 'resolve' => static function () { |
||
| 120 | }, |
||
| 121 | ], |
||
| 122 | ], |
||
| 123 | ]), |
||
| 124 | ])); |
||
| 125 | $data = $processor->processPayload(' { list }')->getResponseData(); |
||
| 126 | $this->assertEquals(['data' => ['list' => null]], $data); |
||
| 127 | } |
||
| 128 | |||
| 129 | public function testSubscriptionNullResponse(): void |
||
| 130 | { |
||
| 131 | $processor = new Processor(new Schema([ |
||
| 132 | 'query' => new ObjectType([ |
||
| 133 | 'name' => 'RootQuery', |
||
| 134 | 'fields' => [ |
||
| 135 | 'list' => [ |
||
| 136 | 'type' => new ListType(new StringType()), |
||
| 137 | 'resolve' => static function () { |
||
| 138 | }, |
||
| 139 | ], |
||
| 140 | ], |
||
| 141 | ]), |
||
| 142 | ])); |
||
| 143 | $data = $processor->processPayload(' { __schema { subscriptionType { name } } }')->getResponseData(); |
||
| 144 | $this->assertEquals(['data' => ['__schema' => ['subscriptionType' => null]]], $data); |
||
| 145 | } |
||
| 146 | |||
| 147 | public function testSchemaOperations(): void |
||
| 148 | { |
||
| 149 | $schema = new Schema([ |
||
| 150 | 'query' => new ObjectType([ |
||
| 151 | 'name' => 'RootQuery', |
||
| 152 | 'fields' => [ |
||
| 153 | 'me' => [ |
||
| 154 | 'type' => new ObjectType([ |
||
| 155 | 'name' => 'User', |
||
| 156 | 'fields' => [ |
||
| 157 | 'firstName' => [ |
||
| 158 | 'type' => new StringType(), |
||
| 159 | 'args' => [ |
||
| 160 | 'shorten' => new BooleanType(), |
||
| 161 | ], |
||
| 162 | 'resolve' => static function ($value, $args) { |
||
| 163 | return empty($args['shorten']) ? $value['firstName'] : $value['firstName']; |
||
| 164 | }, |
||
| 165 | ], |
||
| 166 | 'id_alias' => [ |
||
| 167 | 'type' => new IdType(), |
||
| 168 | 'resolve' => static function ($value) { |
||
| 169 | return $value['id']; |
||
| 170 | }, |
||
| 171 | ], |
||
| 172 | 'lastName' => new StringType(), |
||
| 173 | 'code' => new StringType(), |
||
| 174 | ], |
||
| 175 | ]), |
||
| 176 | 'resolve' => static function ($value, $args) { |
||
| 177 | $data = ['id' => '123', 'firstName' => 'John', 'code' => '007']; |
||
| 178 | |||
| 179 | if (!empty($args['upper'])) { |
||
| 180 | foreach ($data as $key => $value) { |
||
| 181 | $data[$key] = \mb_strtoupper($value); |
||
| 182 | } |
||
| 183 | } |
||
| 184 | |||
| 185 | return $data; |
||
| 186 | }, |
||
| 187 | 'args' => [ |
||
| 188 | 'upper' => [ |
||
| 189 | 'type' => new BooleanType(), |
||
| 190 | 'defaultValue' => false, |
||
| 191 | ], |
||
| 192 | ], |
||
| 193 | ], |
||
| 194 | 'randomUser' => [ |
||
| 195 | 'type' => new TestObjectType(), |
||
| 196 | 'resolve' => static function () { |
||
| 197 | return ['invalidField' => 'John']; |
||
| 198 | }, |
||
| 199 | ], |
||
| 200 | 'invalidValueQuery' => [ |
||
| 201 | 'type' => new TestObjectType(), |
||
| 202 | 'resolve' => static function () { |
||
| 203 | return 'stringValue'; |
||
| 204 | }, |
||
| 205 | ], |
||
| 206 | 'labels' => [ |
||
| 207 | 'type' => new ListType(new StringType()), |
||
| 208 | 'resolve' => static function () { |
||
| 209 | return ['one', 'two']; |
||
| 210 | }, |
||
| 211 | ], |
||
| 212 | ], |
||
| 213 | ]), |
||
| 214 | ]); |
||
| 215 | $processor = new Processor($schema); |
||
| 216 | |||
| 217 | $processor->processPayload('mutation { __typename }'); |
||
| 218 | $this->assertEquals(['data' => ['__typename' => 'RootSchemaMutation']], $processor->getResponseData()); |
||
| 219 | |||
| 220 | $processor->processPayload('{ __typename }'); |
||
| 221 | $this->assertEquals(['data' => ['__typename' => 'RootQuery']], $processor->getResponseData()); |
||
| 222 | |||
| 223 | $processor->processPayload('{ me { firstName } }'); |
||
| 224 | $this->assertEquals(['data' => ['me' => ['firstName' => 'John']]], $processor->getResponseData()); |
||
| 225 | |||
| 226 | $processor->processPayload('{ me { id_alias } }'); |
||
| 227 | $this->assertEquals(['data' => ['me' => ['id_alias' => '123']]], $processor->getResponseData()); |
||
| 228 | |||
| 229 | $processor->processPayload('{ me { firstName, lastName } }'); |
||
| 230 | $this->assertEquals(['data' => ['me' => ['firstName' => 'John', 'lastName' => null]]], $processor->getResponseData()); |
||
| 231 | |||
| 232 | $processor->processPayload('{ me { code } }'); |
||
| 233 | $this->assertEquals(['data' => ['me' => ['code' => 7]]], $processor->getResponseData()); |
||
| 234 | |||
| 235 | $processor->processPayload('{ me(upper:true) { firstName } }'); |
||
| 236 | $this->assertEquals(['data' => ['me' => ['firstName' => 'JOHN']]], $processor->getResponseData()); |
||
| 237 | |||
| 238 | $processor->processPayload('{ labels }'); |
||
| 239 | $this->assertEquals(['data' => ['labels' => ['one', 'two']]], $processor->getResponseData()); |
||
| 240 | |||
| 241 | $schema->getMutationType() |
||
| 242 | ->addField(new Field([ |
||
| 243 | 'name' => 'increaseCounter', |
||
| 244 | 'type' => new IntType(), |
||
| 245 | 'resolve' => function ($value, $args, ResolveInfo $info) { |
||
| 246 | return $this->_counter += $args['amount']; |
||
| 247 | }, |
||
| 248 | 'args' => [ |
||
| 249 | 'amount' => [ |
||
| 250 | 'type' => new IntType(), |
||
| 251 | 'defaultValue' => 1, |
||
| 252 | ], |
||
| 253 | ], |
||
| 254 | ]))->addField(new Field([ |
||
| 255 | 'name' => 'invalidResolveTypeMutation', |
||
| 256 | 'type' => new NonNullType(new IntType()), |
||
| 257 | 'resolve' => static function () { |
||
| 258 | }, |
||
| 259 | ]))->addField(new Field([ |
||
| 260 | 'name' => 'interfacedMutation', |
||
| 261 | 'type' => new TestInterfaceType(), |
||
| 262 | 'resolve' => static function () { |
||
| 263 | return ['name' => 'John']; |
||
| 264 | }, |
||
| 265 | ])); |
||
| 266 | $processor->processPayload('mutation { increaseCounter }'); |
||
| 267 | $this->assertEquals(['data' => ['increaseCounter' => 1]], $processor->getResponseData()); |
||
| 268 | |||
| 269 | $processor->processPayload('mutation { invalidMutation }'); |
||
| 270 | $this->assertEquals(['errors' => [[ |
||
| 271 | 'message' => 'Field "invalidMutation" not found in type "RootSchemaMutation". Available fields are: "increaseCounter", "invalidResolveTypeMutation", "interfacedMutation"', |
||
| 272 | 'locations' => [ |
||
| 273 | [ |
||
| 274 | 'line' => 1, |
||
| 275 | 'column' => 12, |
||
| 276 | ], |
||
| 277 | ], |
||
| 278 | ]]], $processor->getResponseData()); |
||
| 279 | $processor->getExecutionContext()->clearErrors(); |
||
| 280 | |||
| 281 | $processor->processPayload('mutation { increaseCounter(noArg: 2) }'); |
||
| 282 | $this->assertEquals([ |
||
| 283 | 'data' => ['increaseCounter' => null], |
||
| 284 | 'errors' => [[ |
||
| 285 | 'message' => 'Unknown argument "noArg" on field "increaseCounter"', |
||
| 286 | 'locations' => [ |
||
| 287 | [ |
||
| 288 | 'line' => 1, |
||
| 289 | 'column' => 28, |
||
| 290 | ], |
||
| 291 | ], |
||
| 292 | ]], |
||
| 293 | ], $processor->getResponseData()); |
||
| 294 | $processor->getExecutionContext()->clearErrors(); |
||
| 295 | |||
| 296 | $processor->processPayload('mutation { increaseCounter(amount: 2) { invalidProp } }'); |
||
| 297 | $this->assertEquals(['errors' => [[ |
||
| 298 | 'message' => 'You can\'t specify fields for scalar type "Int"', |
||
| 299 | 'locations' => [ |
||
| 300 | [ |
||
| 301 | 'line' => 1, |
||
| 302 | 'column' => 12, |
||
| 303 | ], |
||
| 304 | ], |
||
| 305 | ]], 'data' => ['increaseCounter' => null]], $processor->getResponseData()); |
||
| 306 | $processor->getExecutionContext()->clearErrors(); |
||
| 307 | |||
| 308 | $processor->processPayload('mutation { increaseCounter(amount: 2) }'); |
||
| 309 | $this->assertEquals(['data' => ['increaseCounter' => 3]], $processor->getResponseData()); |
||
| 310 | |||
| 311 | $processor->processPayload('{ invalidQuery }'); |
||
| 312 | $this->assertEquals(['errors' => [[ |
||
| 313 | 'message' => 'Field "invalidQuery" not found in type "RootQuery". Available fields are: "me", "randomUser", "invalidValueQuery", "labels", "__schema", "__type"', |
||
| 314 | 'locations' => [ |
||
| 315 | [ |
||
| 316 | 'line' => 1, |
||
| 317 | 'column' => 3, |
||
| 318 | ], |
||
| 319 | ], |
||
| 320 | ]]], $processor->getResponseData()); |
||
| 321 | $processor->getExecutionContext()->clearErrors(); |
||
| 322 | |||
| 323 | $processor->processPayload('{ invalidValueQuery { id } }'); |
||
| 324 | $this->assertEquals(['errors' => [['message' => 'Not valid resolved type for field "invalidValueQuery": (no details available)']], 'data' => ['invalidValueQuery' => null]], $processor->getResponseData()); |
||
| 325 | $processor->getExecutionContext()->clearErrors(); |
||
| 326 | |||
| 327 | $processor->processPayload('{ me { firstName(shorten: true), middle }}'); |
||
| 328 | $this->assertEquals([ |
||
| 329 | 'data' => ['me' => null], |
||
| 330 | 'errors' => [[ |
||
| 331 | 'message' => 'Field "middle" not found in type "User". Available fields are: "firstName", "id_alias", "lastName", "code"', |
||
| 332 | 'locations' => [ |
||
| 333 | [ |
||
| 334 | 'line' => 1, |
||
| 335 | 'column' => 34, |
||
| 336 | ], |
||
| 337 | ], |
||
| 338 | ]], |
||
| 339 | ], $processor->getResponseData()); |
||
| 340 | $processor->getExecutionContext()->clearErrors(); |
||
| 341 | |||
| 342 | $processor->processPayload('{ randomUser { region }}'); |
||
| 343 | $this->assertEquals([ |
||
| 344 | 'data' => ['randomUser' => null], |
||
| 345 | 'errors' => [[ |
||
| 346 | 'message' => 'You have to specify fields for "region"', |
||
| 347 | 'locations' => [ |
||
| 348 | [ |
||
| 349 | 'line' => 1, |
||
| 350 | 'column' => 16, |
||
| 351 | ], |
||
| 352 | ], |
||
| 353 | ]], |
||
| 354 | ], $processor->getResponseData()); |
||
| 355 | $processor->getExecutionContext()->clearErrors(); |
||
| 356 | |||
| 357 | $processor->processPayload('mutation { invalidResolveTypeMutation }'); |
||
| 358 | $this->assertEquals([ |
||
| 359 | 'data' => ['invalidResolveTypeMutation' => null], |
||
| 360 | 'errors' => [['message' => 'Cannot return null for non-nullable field "invalidResolveTypeMutation"']], |
||
| 361 | ], $processor->getResponseData()); |
||
| 362 | $processor->getExecutionContext()->clearErrors(); |
||
| 363 | |||
| 364 | $processor->processPayload('mutation { user:interfacedMutation { name } }'); |
||
| 365 | $this->assertEquals(['data' => ['user' => ['name' => 'John']]], $processor->getResponseData()); |
||
| 366 | } |
||
| 367 | |||
| 368 | public function testEnumType(): void |
||
| 369 | { |
||
| 370 | $processor = new Processor(new Schema([ |
||
| 371 | 'query' => new ObjectType([ |
||
| 372 | 'name' => 'RootQuery', |
||
| 373 | 'fields' => [ |
||
| 374 | 'test' => [ |
||
| 375 | 'args' => [ |
||
| 376 | 'argument1' => new NonNullType(new EnumType([ |
||
| 377 | 'name' => 'TestEnumType', |
||
| 378 | 'values' => [ |
||
| 379 | [ |
||
| 380 | 'name' => 'VALUE1', |
||
| 381 | 'value' => 'val1', |
||
| 382 | ], |
||
| 383 | [ |
||
| 384 | 'name' => 'VALUE2', |
||
| 385 | 'value' => 'val2', |
||
| 386 | ], |
||
| 387 | ], |
||
| 388 | ])), |
||
| 389 | ], |
||
| 390 | 'type' => new StringType(), |
||
| 391 | 'resolve' => static function ($value, $args) { |
||
| 392 | return $args['argument1']; |
||
| 393 | }, |
||
| 394 | ], |
||
| 395 | ], |
||
| 396 | ]), |
||
| 397 | ])); |
||
| 398 | |||
| 399 | $processor->processPayload('{ test }'); |
||
| 400 | $response = $processor->getResponseData(); |
||
| 401 | $this->assertEquals([ |
||
| 402 | 'data' => ['test' => null], |
||
| 403 | 'errors' => [['message' => 'Require "argument1" arguments to query "test"']], |
||
| 404 | ], $response); |
||
| 405 | $processor->getExecutionContext()->clearErrors(); |
||
| 406 | |||
| 407 | $processor->processPayload('{ alias: test() }'); |
||
| 408 | $response = $processor->getResponseData(); |
||
| 409 | $this->assertEquals([ |
||
| 410 | 'data' => ['alias' => null], |
||
| 411 | 'errors' => [['message' => 'Require "argument1" arguments to query "test"']], |
||
| 412 | ], $response); |
||
| 413 | $processor->getExecutionContext()->clearErrors(); |
||
| 414 | |||
| 415 | $processor->processPayload('{ alias: test(argument1: VALUE4) }'); |
||
| 416 | $response = $processor->getResponseData(); |
||
| 417 | $this->assertEquals([ |
||
| 418 | 'data' => ['alias' => null], |
||
| 419 | 'errors' => [[ |
||
| 420 | 'message' => 'Not valid type for argument "argument1" in query "test": Value must be one of the allowed ones: VALUE1 (val1), VALUE2 (val2)', |
||
| 421 | 'locations' => [ |
||
| 422 | [ |
||
| 423 | 'line' => 1, |
||
| 424 | 'column' => 15, |
||
| 425 | ], |
||
| 426 | ], |
||
| 427 | ]], |
||
| 428 | ], $response); |
||
| 429 | $processor->getExecutionContext()->clearErrors(); |
||
| 430 | |||
| 431 | $processor->processPayload('{ alias: test(argument1: VALUE1) }'); |
||
| 432 | $response = $processor->getResponseData(); |
||
| 433 | $this->assertEquals(['data' => ['alias' => 'val1']], $response); |
||
| 434 | } |
||
| 435 | |||
| 436 | public function testListEnumsSchemaOperations(): void |
||
| 437 | { |
||
| 438 | $processor = new Processor(new Schema([ |
||
| 439 | 'query' => new ObjectType([ |
||
| 440 | 'name' => 'RootQuery', |
||
| 441 | 'fields' => [ |
||
| 442 | 'listQuery' => [ |
||
| 443 | 'type' => new ListType(new TestEnumType()), |
||
| 444 | 'resolve' => static function () { |
||
| 445 | return 'invalid list'; |
||
| 446 | }, |
||
| 447 | ], |
||
| 448 | 'listEnumQuery' => [ |
||
| 449 | 'type' => new ListType(new TestEnumType()), |
||
| 450 | 'resolve' => static function () { |
||
| 451 | return ['invalid enum']; |
||
| 452 | }, |
||
| 453 | ], |
||
| 454 | 'invalidEnumQuery' => [ |
||
| 455 | 'type' => new TestEnumType(), |
||
| 456 | 'resolve' => static function () { |
||
| 457 | return 'invalid enum'; |
||
| 458 | }, |
||
| 459 | ], |
||
| 460 | 'enumQuery' => [ |
||
| 461 | 'type' => new TestEnumType(), |
||
| 462 | 'resolve' => static function () { |
||
| 463 | return 1; |
||
| 464 | }, |
||
| 465 | ], |
||
| 466 | 'invalidNonNullQuery' => [ |
||
| 467 | 'type' => new NonNullType(new IntType()), |
||
| 468 | 'resolve' => static function () { |
||
| 469 | }, |
||
| 470 | ], |
||
| 471 | 'invalidNonNullInsideQuery' => [ |
||
| 472 | 'type' => new NonNullType(new IntType()), |
||
| 473 | 'resolve' => static function () { |
||
| 474 | return 'hello'; |
||
| 475 | }, |
||
| 476 | ], |
||
| 477 | 'objectQuery' => [ |
||
| 478 | 'type' => new TestObjectType(), |
||
| 479 | 'resolve' => static function () { |
||
| 480 | return ['name' => 'John']; |
||
| 481 | }, |
||
| 482 | ], |
||
| 483 | 'deepObjectQuery' => [ |
||
| 484 | 'type' => new ObjectType([ |
||
| 485 | 'name' => 'deepObject', |
||
| 486 | 'fields' => [ |
||
| 487 | 'object' => new TestObjectType(), |
||
| 488 | 'enum' => new TestEnumType(), |
||
| 489 | ], |
||
| 490 | ]), |
||
| 491 | 'resolve' => static function () { |
||
| 492 | return [ |
||
| 493 | 'object' => [ |
||
| 494 | 'name' => 'John', |
||
| 495 | ], |
||
| 496 | 'enum' => 1, |
||
| 497 | ]; |
||
| 498 | }, |
||
| 499 | ], |
||
| 500 | ], |
||
| 501 | ]), |
||
| 502 | ])); |
||
| 503 | |||
| 504 | $processor->processPayload('{ listQuery }'); |
||
| 505 | $this->assertEquals([ |
||
| 506 | 'data' => ['listQuery' => null], |
||
| 507 | 'errors' => [['message' => 'Not valid resolved type for field "listQuery": The value is not an iterable.']], |
||
| 508 | ], $processor->getResponseData()); |
||
| 509 | $processor->getExecutionContext()->clearErrors(); |
||
| 510 | |||
| 511 | $processor->processPayload('{ listEnumQuery }'); |
||
| 512 | $this->assertEquals([ |
||
| 513 | 'data' => ['listEnumQuery' => [null]], |
||
| 514 | 'errors' => [['message' => 'Not valid resolved type for field "listEnumQuery": Value must be one of the allowed ones: FINISHED (1), NEW (0)']], |
||
| 515 | ], $processor->getResponseData()); |
||
| 516 | $processor->getExecutionContext()->clearErrors(); |
||
| 517 | |||
| 518 | $processor->processPayload('{ invalidEnumQuery }'); |
||
| 519 | $this->assertEquals([ |
||
| 520 | 'data' => ['invalidEnumQuery' => null], |
||
| 521 | 'errors' => [['message' => 'Not valid resolved type for field "invalidEnumQuery": Value must be one of the allowed ones: FINISHED (1), NEW (0)']], |
||
| 522 | ], $processor->getResponseData()); |
||
| 523 | $processor->getExecutionContext()->clearErrors(); |
||
| 524 | |||
| 525 | $processor->processPayload('{ enumQuery }'); |
||
| 526 | $this->assertEquals(['data' => ['enumQuery' => 'FINISHED']], $processor->getResponseData()); |
||
| 527 | |||
| 528 | $processor->processPayload('{ invalidNonNullQuery }'); |
||
| 529 | $this->assertEquals([ |
||
| 530 | 'data' => ['invalidNonNullQuery' => null], |
||
| 531 | 'errors' => [['message' => 'Cannot return null for non-nullable field "invalidNonNullQuery"']], |
||
| 532 | ], $processor->getResponseData()); |
||
| 533 | $processor->getExecutionContext()->clearErrors(); |
||
| 534 | |||
| 535 | $processor->processPayload('{ invalidNonNullInsideQuery }'); |
||
| 536 | $this->assertEquals([ |
||
| 537 | 'data' => ['invalidNonNullInsideQuery' => null], |
||
| 538 | 'errors' => [['message' => 'Not valid resolved type for field "invalidNonNullInsideQuery": (no details available)']], |
||
| 539 | ], $processor->getResponseData()); |
||
| 540 | $processor->getExecutionContext()->clearErrors(); |
||
| 541 | |||
| 542 | $processor->processPayload('{ test:deepObjectQuery { object { name } } }'); |
||
| 543 | $this->assertEquals(['data' => ['test' => ['object' => ['name' => 'John']]]], $processor->getResponseData()); |
||
| 544 | } |
||
| 545 | |||
| 546 | public function testTypedFragment(): void |
||
| 547 | { |
||
| 548 | $object1 = new ObjectType([ |
||
| 549 | 'name' => 'Object1', |
||
| 550 | 'fields' => [ |
||
| 551 | 'id' => ['type' => 'int', 'cost' => 13], |
||
| 552 | ], |
||
| 553 | ]); |
||
| 554 | |||
| 555 | $object2 = new ObjectType([ |
||
| 556 | 'name' => 'Object2', |
||
| 557 | 'fields' => [ |
||
| 558 | 'name' => ['type' => 'string'], |
||
| 559 | ], |
||
| 560 | ]); |
||
| 561 | |||
| 562 | $object3 = new ObjectType([ |
||
| 563 | 'name' => 'Object3', |
||
| 564 | 'fields' => [ |
||
| 565 | 'name' => ['type' => 'string'], |
||
| 566 | ], |
||
| 567 | ]); |
||
| 568 | |||
| 569 | $union = new UnionType([ |
||
| 570 | 'name' => 'TestUnion', |
||
| 571 | 'types' => [$object1, $object2], |
||
| 572 | 'resolveType' => static function ($object) use ($object1, $object2) { |
||
| 573 | if (isset($object['id'])) { |
||
| 574 | return $object1; |
||
| 575 | } |
||
| 576 | |||
| 577 | return $object2; |
||
| 578 | }, |
||
| 579 | ]); |
||
| 580 | $invalidUnion = new UnionType([ |
||
| 581 | 'name' => 'TestUnion', |
||
| 582 | 'types' => [$object1, $object2], |
||
| 583 | 'resolveType' => static function ($object) use ($object3) { |
||
| 584 | return $object3; |
||
| 585 | }, |
||
| 586 | ]); |
||
| 587 | $processor = new Processor(new Schema([ |
||
| 588 | 'query' => new ObjectType([ |
||
| 589 | 'name' => 'RootQuery', |
||
| 590 | 'fields' => [ |
||
| 591 | 'union' => [ |
||
| 592 | 'type' => $union, |
||
| 593 | 'args' => [ |
||
| 594 | 'type' => ['type' => 'string'], |
||
| 595 | ], |
||
| 596 | 'cost' => 10, |
||
| 597 | 'resolve' => static function ($value, $args) { |
||
| 598 | if ('object1' === $args['type']) { |
||
| 599 | return [ |
||
| 600 | 'id' => 43, |
||
| 601 | ]; |
||
| 602 | } |
||
| 603 | |||
| 604 | return [ |
||
| 605 | 'name' => 'name resolved', |
||
| 606 | ]; |
||
| 607 | }, |
||
| 608 | ], |
||
| 609 | 'invalidUnion' => [ |
||
| 610 | 'type' => $invalidUnion, |
||
| 611 | 'resolve' => static function () { |
||
| 612 | return ['name' => 'name resolved']; |
||
| 613 | }, |
||
| 614 | ], |
||
| 615 | ], |
||
| 616 | ]), |
||
| 617 | ])); |
||
| 618 | $processor->processPayload('{ union(type: "object1") { ... on Object2 { id } } }'); |
||
| 619 | $this->assertEquals(['data' => ['union' => []]], $processor->getResponseData()); |
||
| 620 | |||
| 621 | $processor->processPayload('{ union(type: "object1") { ... on Object1 { name } } }'); |
||
| 622 | $this->assertEquals([ |
||
| 623 | 'data' => [ |
||
| 624 | 'union' => null, |
||
| 625 | ], |
||
| 626 | 'errors' => [ |
||
| 627 | [ |
||
| 628 | 'message' => 'Field "name" not found in type "Object1". Available fields are: "id"', |
||
| 629 | 'locations' => [ |
||
| 630 | [ |
||
| 631 | 'line' => 1, |
||
| 632 | 'column' => 45, |
||
| 633 | ], |
||
| 634 | ], |
||
| 635 | ], |
||
| 636 | ], |
||
| 637 | ], $processor->getResponseData()); |
||
| 638 | $processor->getExecutionContext()->clearErrors(); |
||
| 639 | |||
| 640 | $processor->processPayload('{ union(type: "object1") { ... on Object1 { id } } }'); |
||
| 641 | $this->assertEquals(['data' => ['union' => ['id' => 43]]], $processor->getResponseData()); |
||
| 642 | |||
| 643 | $processor->processPayload('{ union(type: "asd") { ... on Object2 { name } } }'); |
||
| 644 | $this->assertEquals(['data' => ['union' => ['name' => 'name resolved']]], $processor->getResponseData()); |
||
| 645 | |||
| 646 | $processor->processPayload('{ invalidUnion { ... on Object2 { name } } }'); |
||
| 647 | $this->assertEquals([ |
||
| 648 | 'data' => [ |
||
| 649 | 'invalidUnion' => null, |
||
| 650 | ], |
||
| 651 | 'errors' => [ |
||
| 652 | ['message' => 'Type "Object3" not exist in types of "TestUnion"'], |
||
| 653 | ], |
||
| 654 | ], $processor->getResponseData()); |
||
| 655 | |||
| 656 | $visitor = new MaxComplexityQueryVisitor(1000); // arbitrarily high cost |
||
| 657 | $processor->processPayload('{ union(type: "object1") { ... on Object1 { id } } }', [], [$visitor]); |
||
| 658 | $this->assertEquals(10 + 13, $visitor->getMemo()); |
||
| 659 | |||
| 660 | $visitor = new MaxComplexityQueryVisitor(1000); // arbitrarily high cost |
||
| 661 | $processor->processPayload('{ union(type: "object1") { ... on Object1 { id }, ... on Object2 { name } } }', [], [$visitor]); |
||
| 662 | $this->assertEquals(10 + 13 + 1, $visitor->getMemo()); |
||
| 663 | |||
| 664 | // planning phase currently has no knowledge of what types the union will resolve to, this will have the same score as above |
||
| 665 | $visitor = new MaxComplexityQueryVisitor(1000); // arbitrarily high cost |
||
| 666 | $processor->processPayload('{ union(type: "object2") { ... on Object1 { id }, ... on Object2 { name } } }', [], [$visitor]); |
||
| 667 | $this->assertEquals(10 + 13 + 1, $visitor->getMemo()); |
||
| 668 | } |
||
| 669 | |||
| 670 | public function testContainer(): void |
||
| 671 | { |
||
| 672 | $container = new Container(); |
||
| 673 | $container->set('user', ['name' => 'Alex']); |
||
| 674 | |||
| 675 | $executionContext = new ExecutionContext(new Schema([ |
||
| 676 | 'query' => new ObjectType([ |
||
| 677 | 'name' => 'RootQuery', |
||
| 678 | 'fields' => [ |
||
| 679 | 'currentUser' => [ |
||
| 680 | 'type' => new StringType(), |
||
| 681 | 'resolve' => static function ($source, $args, ResolveInfo $info) { |
||
| 682 | return $info->getContainer()->get('user')['name']; |
||
| 683 | }, |
||
| 684 | ], |
||
| 685 | ], |
||
| 686 | ]), |
||
| 687 | ])); |
||
| 688 | $executionContext->setContainer($container); |
||
| 689 | $this->assertNotNull($executionContext->getContainer()); |
||
| 690 | |||
| 691 | $processor = new Processor($executionContext->getSchema()); |
||
| 692 | $processor->getExecutionContext()->setContainer($container); |
||
| 693 | |||
| 694 | $this->assertEquals(['data' => ['currentUser' => 'Alex']], $processor->processPayload('{ currentUser }')->getResponseData()); |
||
| 695 | } |
||
| 696 | |||
| 697 | public function testComplexityReducer(): void |
||
| 698 | { |
||
| 699 | $schema = new Schema( |
||
| 700 | [ |
||
| 701 | 'query' => new ObjectType( |
||
| 702 | [ |
||
| 703 | 'name' => 'RootQuery', |
||
| 704 | 'fields' => [ |
||
| 705 | 'me' => [ |
||
| 706 | 'type' => new ObjectType( |
||
| 707 | [ |
||
| 708 | 'name' => 'User', |
||
| 709 | 'fields' => [ |
||
| 710 | 'firstName' => [ |
||
| 711 | 'type' => new StringType(), |
||
| 712 | 'args' => [ |
||
| 713 | 'shorten' => new BooleanType(), |
||
| 714 | ], |
||
| 715 | 'resolve' => static function ($value, $args) { |
||
| 716 | return empty($args['shorten']) ? $value['firstName'] : $value['firstName']; |
||
| 717 | }, |
||
| 718 | ], |
||
| 719 | 'lastName' => new StringType(), |
||
| 720 | 'code' => new StringType(), |
||
| 721 | 'likes' => [ |
||
| 722 | 'type' => new IntType(), |
||
| 723 | 'cost' => 10, |
||
| 724 | 'resolve' => static function () { |
||
| 725 | return 42; |
||
| 726 | }, |
||
| 727 | ], |
||
| 728 | ], |
||
| 729 | ] |
||
| 730 | ), |
||
| 731 | 'cost' => static function ($args, $context, $childCost) { |
||
| 732 | $argsCost = $args['cost'] ?? 1; |
||
| 733 | |||
| 734 | return 1 + $argsCost * $childCost; |
||
| 735 | }, |
||
| 736 | 'resolve' => static function ($value, $args) { |
||
| 737 | $data = ['firstName' => 'John', 'code' => '007']; |
||
| 738 | |||
| 739 | return $data; |
||
| 740 | }, |
||
| 741 | 'args' => [ |
||
| 742 | 'cost' => [ |
||
| 743 | 'type' => new IntType(), |
||
| 744 | 'default' => 1, |
||
| 745 | ], |
||
| 746 | ], |
||
| 747 | ], |
||
| 748 | ], |
||
| 749 | ] |
||
| 750 | ), |
||
| 751 | ] |
||
| 752 | ); |
||
| 753 | $processor = new Processor($schema); |
||
| 754 | |||
| 755 | $processor->setMaxComplexity(10); |
||
| 756 | |||
| 757 | $processor->processPayload('{ me { firstName, lastName } }'); |
||
| 758 | $this->assertArrayNotHasKey('error', $processor->getResponseData()); |
||
| 759 | |||
| 760 | $processor->processPayload('{ me { } }'); |
||
| 761 | $this->assertEquals(['errors' => [[ |
||
| 762 | 'message' => 'Unexpected token "RBRACE"', |
||
| 763 | 'locations' => [ |
||
| 764 | [ |
||
| 765 | 'line' => 1, |
||
| 766 | 'column' => 10, |
||
| 767 | ], |
||
| 768 | ], |
||
| 769 | ]]], $processor->getResponseData()); |
||
| 770 | $processor->getExecutionContext()->clearErrors(); |
||
| 771 | |||
| 772 | $processor->processPayload('{ me { firstName, likes } }'); |
||
| 773 | $this->assertEquals(['errors' => [['message' => 'query exceeded max allowed complexity of 10']]], $processor->getResponseData()); |
||
| 774 | $processor->getExecutionContext()->clearErrors(); |
||
| 775 | |||
| 776 | // don't let complexity reducer affect query errors |
||
| 777 | $processor->processPayload('{ me { badfield } }'); |
||
| 778 | $this->assertArraySubset(['errors' => [['message' => 'Field "badfield" not found in type "User". Available fields are: "firstName", "lastName", "code", "likes"']]], $processor->getResponseData()); |
||
| 779 | $processor->getExecutionContext()->clearErrors(); |
||
| 780 | |||
| 781 | foreach (\range(1, 5) as $cost_multiplier) { |
||
| 782 | $visitor = new MaxComplexityQueryVisitor(1000); // arbitrarily high cost |
||
| 783 | $processor->processPayload("{ me (cost: ${cost_multiplier}) { firstName, lastName, code, likes } }", ['cost' => $cost_multiplier], [$visitor]); |
||
| 784 | $expected = 1 + 13 * (1 + $cost_multiplier); |
||
| 785 | $this->assertEquals($expected, $visitor->getMemo()); |
||
| 786 | } |
||
| 787 | |||
| 788 | // TODO, variables not yet supported |
||
| 789 | /*$query = 'query costQuery ($cost: Int) { me (cost: $cost) { firstName, lastName, code, likes } }'; |
||
| 790 | foreach (range(1,5) as $cost_multiplier) { |
||
| 791 | $visitor = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor(1000); // arbitrarily high cost |
||
| 792 | $processor->processPayload($query, ['cost' => $cost_multiplier], [$visitor]); |
||
| 793 | $expected = 1 + 13 * (1 + $cost_multiplier); |
||
| 794 | $this->assertEquals($expected, $visitor->getMemo()); |
||
| 795 | }*/ |
||
| 796 | } |
||
| 797 | } |
||
| 798 |