1use lib 't/lib'; 2use GQLTest; 3use Devel::StrictMode; 4 5my $JSON = JSON::MaybeXS->new->allow_nonref->canonical; 6 7BEGIN { 8 use_ok( 'GraphQL::Schema' ) || print "Bail out!\n"; 9 use_ok( 'GraphQL::Type::Object' ) || print "Bail out!\n"; 10 use_ok( 'GraphQL::Type::Scalar', qw($String $Int $Boolean) ) || print "Bail out!\n"; 11 use_ok( 'GraphQL::Execution', qw(execute) ) || print "Bail out!\n"; 12 use_ok( 'GraphQL::Language::Parser', qw(parse) ) || print "Bail out!\n"; 13} 14 15subtest 'throws if no document is provided' => sub { 16 plan skip_all => 'Type check disabled' unless STRICT; 17 my $schema = GraphQL::Schema->new( 18 query => GraphQL::Type::Object->new( 19 name => 'Type', 20 fields => { 21 a => { type => $String }, 22 } 23 ) 24 ); 25 throws_ok { execute($schema, undef) } qr/Undef did not pass type constraint/; 26}; 27 28subtest 'executes arbitrary code' => sub { 29 my ($deep_data, $data); 30 $data = { 31 a => sub { 'Apple' }, 32 b => sub { 'Banana' }, 33 c => sub { 'Cookie' }, 34 d => sub { 'Donut' }, 35 e => sub { 'Egg' }, 36 f => 'Fish', 37 pic => sub { 38 my $size = shift; 39 return 'Pic of size: ' . ($size || 50); 40 }, 41 deep => sub { $deep_data }, 42 promise => sub { FakePromise->resolve($data) }, 43 }; 44 45 $deep_data = { 46 a => sub { 'Already Been Done' }, 47 b => sub { 'Boring' }, 48 c => sub { ['Contrived', undef, 'Confusing'] }, 49 deeper => sub { [$data, undef, $data] } 50 }; 51 52 my ($DeepDataType, $DataType); 53 $DataType = GraphQL::Type::Object->new( 54 name => 'DataType', 55 fields => sub { { 56 a => { type => $String }, 57 b => { type => $String }, 58 c => { type => $String }, 59 d => { type => $String }, 60 e => { type => $String }, 61 f => { type => $String }, 62 pic => { 63 args => { size => { type => $Int } }, 64 type => $String, 65 resolve => sub { 66 my ($obj, $args) = @_; 67 return $obj->{pic}->($args->{size}); 68 } 69 }, 70 deep => { type => $DeepDataType }, 71 promise => { type => $DataType }, 72 } } 73 ); 74 75 $DeepDataType = GraphQL::Type::Object->new( 76 name => 'DeepDataType', 77 fields => { 78 a => { type => $String }, 79 b => { type => $String }, 80 c => { type => $String->list }, 81 deeper => { type => $DataType->list }, 82 } 83 ); 84 85 my $schema = GraphQL::Schema->new( 86 query => $DataType 87 ); 88 89 my $doc = <<'EOF'; 90query Example($size: Int) { 91 a, 92 b, 93 x: c 94 ...c 95 f 96 ...on DataType { 97 pic(size: $size) 98 promise { 99 a 100 } 101 } 102 deep { 103 a 104 b 105 c 106 deeper { 107 a 108 b 109 } 110 } 111} 112fragment c on DataType { 113 d 114 e 115} 116EOF 117 my $ast = parse($doc); 118 119 run_test([$schema, $ast, $data, undef, { size => 100 }, 'Example'], { 120 data => { 121 a => 'Apple', 122 b => 'Banana', 123 x => 'Cookie', 124 d => 'Donut', 125 e => 'Egg', 126 f => 'Fish', 127 pic => 'Pic of size: 100', 128 promise => { a => 'Apple' }, 129 deep => { 130 a => 'Already Been Done', 131 b => 'Boring', 132 c => ['Contrived', undef, 'Confusing'], 133 deeper => [ 134 { a => 'Apple', b => 'Banana' }, 135 undef, 136 { a => 'Apple', b => 'Banana' }, 137 ], 138 }, 139 }, 140 }); 141}; 142 143subtest 'merges parallel fragments' => sub{ 144 my $ast = parse(' 145{ a, ...FragOne, ...FragTwo } 146fragment FragOne on Type { 147 b 148 deep { b, deeper: deep { b } } 149} 150fragment FragTwo on Type { 151 c 152 deep { c, deeper: deep { c } } 153} 154 '); 155 156 my $Type; 157 $Type = GraphQL::Type::Object->new( 158 name => 'Type', 159 fields => sub { { 160 a => { type => $String, resolve => sub { 'Apple' } }, 161 b => { type => $String, resolve => sub { 'Banana' } }, 162 c => { type => $String, resolve => sub { 'Cherry' } }, 163 deep => { type => $Type, resolve => sub { {} } }, 164 } }, 165 ); 166 my $schema = GraphQL::Schema->new(query => $Type); 167 168 run_test([$schema, $ast], { 169 data => { 170 a => 'Apple', 171 b => 'Banana', 172 c => 'Cherry', 173 deep => { 174 b => 'Banana', 175 c => 'Cherry', 176 deeper => { 177 b => 'Banana', 178 c => 'Cherry' 179 } 180 } 181 } 182 }); 183}; 184 185subtest 'provides info about current execution state' => sub { 186 my $ast = parse('query ($var: String) { result: test }'); 187 my $info; 188 my $schema = GraphQL::Schema->new( 189 query => GraphQL::Type::Object->new( 190 name => 'Test', 191 fields => { 192 test => { 193 type => $String, 194 resolve => sub { 195 my ($val, $args, $ctx, $_info) = @_; 196 $info = $_info; 197 }, 198 }, 199 }, 200 ) 201 ); 202 my $rootValue = { root => 'val' }; 203 204 execute($schema, $ast, $rootValue, undef, { var => 123 }); 205 206 is_deeply [sort keys %$info], [qw/ 207 field_name 208 field_nodes 209 fragments 210 operation 211 parent_type 212 path 213 promise_code 214 return_type 215 root_value 216 schema 217 variable_values 218 /]; 219 is $info->{field_name}, 'test'; 220 is scalar(@{ $info->{field_nodes} }), 1; 221 is_deeply $info->{field_nodes}[0], $ast->[0]{selections}[0]; 222 is $info->{return_type}->name, $String->name; 223 is $info->{parent_type}, $schema->query; 224 is_deeply $info->{path}, [ 'result' ]; 225 is $info->{schema}, $schema; 226 is $info->{root_value}, $rootValue; 227 is $info->{operation}, $ast->[0]; 228 is_deeply $info->{variable_values}, { var => {type => $String, value => '123'} }; 229}; 230 231subtest 'threads root value context correctly' => sub { 232 my $doc = 'query Example { a }'; 233 my $data = { 234 context_thing => 'thing', 235 }; 236 237 my $resolved_root_value; 238 239 my $schema = GraphQL::Schema->new( 240 query => GraphQL::Type::Object->new( 241 name => 'Type', 242 fields => { 243 a => { 244 type => $String, 245 resolve => sub { 246 my ($root_value) = @_; 247 $resolved_root_value = $root_value; 248 }, 249 }, 250 }, 251 ) 252 ); 253 254 execute($schema, parse($doc), $data); 255 is $resolved_root_value->{context_thing}, 'thing'; 256}; 257 258subtest 'correctly threads arguments' => sub { 259 my $doc = <<'EOF'; 260query Example { 261 b(num_arg: 123, string_arg: "foo") 262} 263EOF 264 265 my $resolved_args; 266 my $schema = GraphQL::Schema->new( 267 query => GraphQL::Type::Object->new( 268 name => 'Type', 269 fields => { 270 b => { 271 args => { 272 num_arg => { type => $Int }, 273 string_arg => { type => $String } 274 }, 275 type => $String, 276 resolve => sub { 277 my (undef, $args) = @_; 278 $resolved_args = $args; 279 } 280 } 281 } 282 ) 283 ); 284 285 execute($schema, parse($doc)); 286 287 is $resolved_args->{num_arg}, 123; 288 is $resolved_args->{string_arg}, 'foo'; 289}; 290 291subtest 'nulls out error subtrees' => sub { 292 my $doc = '{ 293 sync 294 syncError 295 syncRawError 296 syncReturnError 297 syncReturnErrorList 298 async 299 # asyncReject - no because Perl no "Error" exception class 300 asyncRawReject 301 # asyncEmptyReject - no because now FakePromise uses die more 302 # asyncError - no because Perl no "Error" exception class 303 asyncRawError 304 # asyncReturnError - no because Perl no "Error" exception class 305 }'; 306 my $data = { 307 sync => sub { 'sync' }, 308 syncError => sub { die "Error getting syncError\n" }, 309 syncRawError => sub { die "Error getting syncRawError\n" }, 310 syncReturnError => sub { GraphQL::Error->coerce('Error getting syncReturnError') }, 311 syncReturnErrorList => sub { 312 [ 313 'sync0', 314 GraphQL::Error->coerce('Error getting syncReturnErrorList1'), 315 'sync2', 316 GraphQL::Error->coerce('Error getting syncReturnErrorList3') 317 ]; 318 }, 319 async => sub { FakePromise->resolve('async') }, 320 asyncRawError => sub { 321 FakePromise->resolve('')->then(sub { 322 die "Error getting asyncRawError\n" 323 }) 324 }, 325 asyncRawReject => sub { FakePromise->reject("Error getting asyncRawReject\n") }, 326 }; 327 my $ast = parse($doc); 328 my $schema = GraphQL::Schema->new( 329 query => GraphQL::Type::Object->new( 330 name => 'Type', 331 fields => { 332 sync => { type => $String }, 333 syncError => { type => $String }, 334 syncRawError => { type => $String }, 335 syncReturnError => { type => $String }, 336 syncReturnErrorList => { type => $String->list }, 337 async => { type => $String }, 338 asyncRawReject => { type => $String }, 339 asyncRawError => { type => $String }, 340 } 341 ) 342 ); 343 run_test([$schema, $ast, $data], { 344 data => { 345 sync => 'sync', 346 syncError => undef, 347 syncRawError => undef, 348 syncReturnError => undef, 349 syncReturnErrorList => ['sync0', undef, 'sync2', undef], 350 async => 'async', 351 asyncRawError => undef, 352 asyncRawReject => undef, 353 }, 354 errors => bag( 355 { 356 'locations' => [{ 'column' => 3, 'line' => 14 }], 357 'message' => "Error getting asyncRawError\n", 358 'path' => [ 'asyncRawError' ] 359 }, 360 { 361 'locations' => [{ 'column' => 5, 'line' => 12 }], 362 'message' => "Error getting asyncRawReject\n", 363 'path' => [ 'asyncRawReject' ] 364 }, 365 { 366 message => "Error getting syncError\n", 367 locations => [{ line => 4, column => 5 }], 368 path => ['syncError'] 369 }, 370 { 371 message => "Error getting syncRawError\n", 372 locations => [{ line => 5, column => 5 }], 373 path => ['syncRawError'] 374 }, 375 { 376 message => "Error getting syncReturnError", 377 locations => [{ line => 6, column => 5 }], 378 path => ['syncReturnError'] 379 }, 380 { 381 message => "Error getting syncReturnErrorList1", 382 locations => [{ line => 7, column => 5 }], 383 path => ['syncReturnErrorList', 1] 384 }, 385 { 386 message => "Error getting syncReturnErrorList3", 387 locations => [{ line => 7, column => 5 }], 388 path => ['syncReturnErrorList', 3] 389 }, 390 ), 391 }); 392}; 393 394subtest 'nulls error subtree for promise rejection #1071' => sub { 395 my $doc = '{ 396 foods { 397 name 398 } 399 }'; 400 my $ast = parse($doc); 401 my $schema = GraphQL::Schema->new( 402 query => GraphQL::Type::Object->new( 403 name => 'Query', 404 fields => { 405 foods => { 406 type => GraphQL::Type::Object->new( 407 name => 'Food', 408 fields => { name => { type => $String } }, 409 )->list, 410 resolve => sub { FakePromise->reject("Dangit\n") }, 411 }, 412 }, 413 ) 414 ); 415 my $got = run_test([ $schema, $ast ], { 416 data => { foods => undef }, 417 errors => [ 418 { 419 'locations' => [{ 'column' => 3, 'line' => 5 }], 420 'message' => "Dangit\n", 421 'path' => [ 'foods' ] 422 }, 423 ] 424 }); 425}; 426 427subtest 'Full response path is included for non-nullable fields' => sub { 428 my $A; $A = GraphQL::Type::Object->new( 429 name => 'A', 430 fields => sub { { 431 nullableA => { 432 type => $A, 433 resolve => sub { {} }, 434 }, 435 nonNullA => { 436 type => $A->non_null, 437 resolve => sub { {} }, 438 }, 439 throws => { 440 type => $String->non_null, 441 resolve => sub { die GraphQL::Error->coerce('Catch me if you can') }, 442 }, 443 } }, 444 ); 445 my $queryType = GraphQL::Type::Object->new( 446 name => 'query', 447 fields => sub { { 448 nullableA => { 449 type => $A, 450 resolve => sub { {} }, 451 } 452 } }, 453 ); 454 my $schema = GraphQL::Schema->new( 455 query => $queryType, 456 ); 457 my $query = <<EOF; 458query { 459 nullableA { 460 aliasedA: nullableA { 461 nonNullA { 462 anotherA: nonNullA { 463 throws 464 } 465 } 466 } 467 } 468} 469EOF 470 run_test([$schema, parse($query)], { 471 data => { nullableA => { aliasedA => undef } }, 472 errors => [{ 473 message => 'Catch me if you can', 474 locations => [{ line => 7, column => 9 }], 475 path => ['nullableA', 'aliasedA', 'nonNullA', 'anotherA', 'throws'], 476 }], 477 }); 478}; 479 480subtest 'uses the inline operation if no operation name is provided' => sub { 481 my $doc = '{ a }'; 482 my $data = { a => 'b' }; 483 my $ast = parse($doc); 484 my $schema = GraphQL::Schema->new( 485 query => GraphQL::Type::Object->new( 486 name => 'Type', 487 fields => { 488 a => { type => $String }, 489 } 490 ) 491 ); 492 493 my $result = execute($schema, $ast, $data); 494 is_deeply $result, { data => { a => 'b' } }; 495}; 496 497subtest 'uses the only operation if no operation name is provided' => sub { 498 my $doc = 'query Example { a }'; 499 my $data = { a => 'b' }; 500 my $ast = parse($doc); 501 my $schema = GraphQL::Schema->new( 502 query => GraphQL::Type::Object->new( 503 name => 'Type', 504 fields => { 505 a => { type => $String }, 506 } 507 ) 508 ); 509 510 my $result = execute($schema, $ast, $data); 511 512 is_deeply $result, { data => { a => 'b' } }; 513}; 514 515subtest 'uses the named operation if operation name is provided' => sub { 516 my $doc = 'query Example { first: a } query OtherExample { second: a }'; 517 my $data = { a => 'b' }; 518 my $ast = parse($doc); 519 my $schema = GraphQL::Schema->new( 520 query => GraphQL::Type::Object->new( 521 name => 'Type', 522 fields => { 523 a => { type => $String }, 524 } 525 ) 526 ); 527 528 my $result = execute($schema, $ast, $data, undef, undef, 'OtherExample'); 529 530 is_deeply $result, { data => { second => 'b' } }; 531}; 532 533subtest 'throws if no operation is provided' => sub { 534 my $doc = 'fragment Example on Type { a }'; 535 my $data = { a => 'b' }; 536 my $ast = parse($doc); 537 my $schema = GraphQL::Schema->new( 538 query => GraphQL::Type::Object->new( 539 name => 'Type', 540 fields => { 541 a => { type => $String }, 542 } 543 ) 544 ); 545 run_test([$schema, $ast, $data], { 546 errors => [ { 547 message => "No operations supplied.\n", 548 } ], 549 }); 550}; 551 552subtest 'throws if no operation name is provided with multiple operations' => sub { 553 my $doc = 'query Example { a } query OtherExample { a }'; 554 my $data = { a => 'b' }; 555 my $ast = parse($doc); 556 my $schema = GraphQL::Schema->new( 557 query => GraphQL::Type::Object->new( 558 name => 'Type', 559 fields => { 560 a => { type => $String }, 561 } 562 ) 563 ); 564 run_test([$schema, $ast, $data], { 565 errors => [ { 566 message => "Must provide operation name if query contains multiple operations.\n", 567 } ], 568 }); 569}; 570 571subtest 'throws if unknown operation name is provided' => sub { 572 my $doc = 'query Example { a } query OtherExample { a }'; 573 my $data = { a => 'b' }; 574 my $ast = parse($doc); 575 my $schema = GraphQL::Schema->new( 576 query => GraphQL::Type::Object->new( 577 name => 'Type', 578 fields => { 579 a => { type => $String }, 580 } 581 ) 582 ); 583 run_test([$schema, $ast, $data, undef, undef, 'UnknownExample'], { 584 errors => [ { 585 message => qq{No operations matching 'UnknownExample' found.\n}, 586 } ], 587 }); 588}; 589 590subtest 'uses the query schema for queries' => sub { 591 my $doc = 'query Q { a } mutation M { c } subscription S { a }'; 592 my $data = { a => 'b', c => 'd' }; 593 my $ast = parse($doc); 594 my $schema = GraphQL::Schema->new( 595 query => GraphQL::Type::Object->new( 596 name => 'Q', 597 fields => { 598 a => { type => $String }, 599 } 600 ), 601 mutation => GraphQL::Type::Object->new( 602 name => 'M', 603 fields => { 604 c => { type => $String }, 605 } 606 ), 607 subscription => GraphQL::Type::Object->new( 608 name => 'S', 609 fields => { 610 a => { type => $String }, 611 } 612 ) 613 ); 614 615 my $result = execute($schema, $ast, $data, undef, {}, 'Q'); 616 is_deeply $result, { data => { a => 'b' } }; 617}; 618 619subtest 'uses the mutation schema for mutations' => sub { 620 my $doc = 'query Q { a } mutation M { c }'; 621 my $data = { a => 'b', c => 'd' }; 622 my $ast = parse($doc); 623 my $schema = GraphQL::Schema->new( 624 query => GraphQL::Type::Object->new( 625 name => 'Q', 626 fields => { 627 a => { type => $String }, 628 } 629 ), 630 mutation => GraphQL::Type::Object->new( 631 name => 'M', 632 fields => { 633 c => { type => $String }, 634 } 635 ) 636 ); 637 638 my $mutationResult = execute($schema, $ast, $data, undef, {}, 'M'); 639 is_deeply $mutationResult, { data => { c => 'd' } }; 640}; 641 642subtest 'uses the subscription schema for subscriptions' => sub { 643 my $doc = 'query Q { a } subscription S { a }'; 644 my $data = { a => 'b', c => 'd' }; 645 my $ast = parse($doc); 646 my $schema = GraphQL::Schema->new( 647 query => GraphQL::Type::Object->new( 648 name => 'Q', 649 fields => { 650 a => { type => $String }, 651 } 652 ), 653 subscription => GraphQL::Type::Object->new( 654 name => 'S', 655 fields => { 656 a => { type => $String }, 657 } 658 ) 659 ); 660 661 my $subscription_result = execute($schema, $ast, $data, undef, {}, 'S'); 662 is_deeply $subscription_result, { data => { a => 'b' } }; 663}; 664 665subtest 'Avoids recursion' => sub { 666 my $doc = ' 667 query Q { 668 a 669 ...Frag 670 ...Frag 671 } 672 fragment Frag on Type { 673 a, 674 ...Frag 675 } 676 '; 677 my $data = { a => 'b' }; 678 my $ast = parse($doc); 679 my $schema = GraphQL::Schema->new( 680 query => GraphQL::Type::Object->new( 681 name => 'Type', 682 fields => { 683 a => { type => $String }, 684 } 685 ), 686 ); 687 688 my $queryResult = execute($schema, $ast, $data, undef, {}, 'Q'); 689 is_deeply $queryResult, { data => { a => 'b' } }; 690}; 691 692subtest 'does not include illegal fields in output' => sub { 693 my $doc = 'mutation M { 694 thisIsIllegalDontIncludeMe 695 }'; 696 my $ast = parse($doc); 697 my $schema = GraphQL::Schema->new( 698 query => GraphQL::Type::Object->new( 699 name => 'Q', 700 fields => { 701 a => { type => $String }, 702 } 703 ), 704 mutation => GraphQL::Type::Object->new( 705 name => 'M', 706 fields => { 707 c => { type => $String }, 708 } 709 ), 710 ); 711 712 run_test([$schema, $ast], { data => undef }); 713}; 714 715subtest 'does not include arguments that were not set' => sub { 716 my $schema = GraphQL::Schema->new( 717 query => GraphQL::Type::Object->new( 718 name => 'Type', 719 fields => { 720 field => { 721 type => $String, 722 resolve => sub { 723 my ($data, $args) = @_; 724 return $JSON->encode($args); 725 }, 726 args => { 727 a => { type => $Boolean }, 728 b => { type => $Boolean }, 729 c => { type => $Boolean }, 730 d => { type => $Int }, 731 e => { type => $Int }, 732 }, 733 } 734 } 735 ) 736 ); 737 738 my $query = parse('{ field(a: true, c: false, e: 0) }'); 739 run_test([$schema, $query], { 740 data => { field => '{"a":1,"c":0,"e":0}' } 741 }); 742}; 743 744subtest 'fails when an is_type_of check is not met' => sub { 745 { 746 package Special; 747 sub new { 748 my ($class, $value) = @_; 749 return bless { value => $value }, $class; 750 } 751 sub value { shift->{value} } 752 753 package NotSpecial; 754 sub new { 755 my ($class, $value) = @_; 756 return bless { value => $value }, $class; 757 } 758 sub value { shift->{value} } 759 } 760 761 my $SpecialType = GraphQL::Type::Object->new( 762 name => 'SpecialType', 763 is_type_of => sub { 764 my $obj = shift; 765 return $obj->isa('Special'); 766 }, 767 fields => { 768 value => { type => $String } 769 } 770 ); 771 772 my $schema = GraphQL::Schema->new( 773 query => GraphQL::Type::Object->new( 774 name => 'Query', 775 fields => { 776 specials => { 777 type => $SpecialType->list, 778 resolve => sub { 779 my $root_value = shift; 780 return $root_value->{specials}; 781 } 782 } 783 } 784 ) 785 ); 786 787 my $query = parse('{ specials { value } }'); 788 my $value = { 789 specials => [Special->new('foo'), NotSpecial->new('bar')] 790 }; 791 run_test([$schema, $query, $value], { 792 data => { 793 specials => [ 794 { value => 'foo' }, 795 undef, 796 ], 797 }, 798 errors => [ 799 { 800 message => "Expected a value of type 'SpecialType' but received: 'NotSpecial'.", 801 locations => [{ line => 1, column => 22 }], 802 path => [ 'specials', 1 ], 803 } 804 ], 805 }); 806}; 807 808subtest 'fails to execute a query containing a type definition' => sub { 809 my $query = parse(' 810 { foo } 811 type Query { foo: String } 812 '); 813 814 my $schema = GraphQL::Schema->new( 815 query => GraphQL::Type::Object->new( 816 name => 'Query', 817 fields => { 818 foo => { type => $String } 819 } 820 ) 821 ); 822 run_test([$schema, $query], { 823 errors => [ { 824 message => "Can only execute document containing fragments or operations\n", 825 } ], 826 }); 827}; 828 829done_testing; 830