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