Skip to content

Commit 92d8df8

Browse files
committed
feat(GraphQL::Compatibility) add ExecutionSpec
1 parent 9d4b6c1 commit 92d8df8

File tree

3 files changed

+354
-0
lines changed

3 files changed

+354
-0
lines changed

lib/graphql/compatibility.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require "graphql/compatibility/execution_spec"
2+
3+
4+
module GraphQL
5+
# This module contains specifications for third-party GraphQL extensions.
6+
# It's not loaded by default. To use any members of this module, add:
7+
#
8+
# ```
9+
# require "graphql/compatibility"
10+
# ```
11+
#
12+
# to your project.
13+
module Compatibility
14+
end
15+
end
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
module GraphQL
2+
module Compatibility
3+
# Test an execution strategy. This spec is not meant as a development aid.
4+
# Rather, when the strategy _works_, run it here to see if it has any differences
5+
# from the built-in strategy.
6+
#
7+
# - Custom scalar input / output
8+
# - Null propagation
9+
# - Query-level masking
10+
# - Directive support
11+
# - Typecasting
12+
# - Error handling (raise / return GraphQL::ExecutionError)
13+
# - Provides Irep & AST node to resolve fn
14+
#
15+
# Some things are explicitly _not_ tested here, because they're handled
16+
# by other parts of the system:
17+
#
18+
# - Schema definition (including types and fields)
19+
# - Parsing & parse errors
20+
# - AST -> IRep transformation (eg, fragment merging)
21+
# - Query validation and analysis
22+
# - Relay features
23+
#
24+
module ExecutionSpec
25+
DATA = {
26+
"1001" => OpenStruct.new({
27+
name: "Fannie Lou Hamer",
28+
birthdate: Time.new(1917, 10, 6),
29+
organization_ids: [],
30+
}),
31+
"1002" => OpenStruct.new({
32+
name: "John Lewis",
33+
birthdate: Time.new(1940, 2, 21),
34+
organization_ids: ["2001"],
35+
}),
36+
"1003" => OpenStruct.new({
37+
name: "Diane Nash",
38+
birthdate: Time.new(1938, 5, 15),
39+
organization_ids: ["2001", "2002"],
40+
}),
41+
"1004" => OpenStruct.new({
42+
name: "Ralph Abernathy",
43+
birthdate: Time.new(1926, 3, 11),
44+
organization_ids: ["2002"],
45+
}),
46+
"2001" => OpenStruct.new({
47+
name: "SNCC",
48+
leader_id: nil, # fail on purpose
49+
}),
50+
"2002" => OpenStruct.new({
51+
name: "SCLC",
52+
leader_id: "1004",
53+
}),
54+
}
55+
56+
# Make a minitest suite for this execution strategy, making sure it
57+
# fulfills all the requirements of this library.
58+
# @param execution_strategy [<#new, #execute>] An execution strategy class
59+
# @return [Class<Minitest::Test>] A test suite for this execution strategy
60+
def self.build_suite(execution_strategy)
61+
Class.new(Minitest::Test) do
62+
def self.build_schema(execution_strategy)
63+
organization_type = nil
64+
65+
timestamp_type = GraphQL::ScalarType.define do
66+
name "Timestamp"
67+
coerce_input ->(value) { Time.at(value.to_i) }
68+
coerce_result ->(value) { value.to_i }
69+
end
70+
71+
named_entity_interface_type = GraphQL::InterfaceType.define do
72+
name "NamedEntity"
73+
field :name, !types.String
74+
end
75+
76+
person_type = GraphQL::ObjectType.define do
77+
name "Person"
78+
interfaces [named_entity_interface_type]
79+
field :name, !types.String
80+
field :birthdate, timestamp_type
81+
field :age, types.Int do
82+
argument :on, !timestamp_type
83+
resolve ->(obj, args, ctx) {
84+
if obj.birthdate.nil?
85+
nil
86+
else
87+
age_on = args[:on]
88+
age_years = age_on.year - obj.birthdate.year
89+
this_year_birthday = Time.new(age_on.year, obj.birthdate.month, obj.birthdate.day)
90+
if this_year_birthday > age_on
91+
age_years -= 1
92+
end
93+
end
94+
age_years
95+
}
96+
end
97+
field :organizations, types[organization_type] do
98+
resolve ->(obj, args, ctx) {
99+
obj.organization_ids.map { |id| DATA[id] }
100+
}
101+
end
102+
end
103+
104+
organization_type = GraphQL::ObjectType.define do
105+
name "Organization"
106+
interfaces [named_entity_interface_type]
107+
field :name, !types.String
108+
field :leader, !person_type do
109+
resolve ->(obj, args, ctx) {
110+
DATA[obj.leader_id]
111+
}
112+
end
113+
field :returnedError, types.String do
114+
resolve ->(o, a, c) {
115+
GraphQL::ExecutionError.new("This error was returned")
116+
}
117+
end
118+
field :raisedError, types.String do
119+
resolve ->(o, a, c) {
120+
raise GraphQL::ExecutionError.new("This error was raised")
121+
}
122+
end
123+
124+
field :nodePresence, !types[!types.Boolean] do
125+
resolve ->(o, a, ctx) {
126+
[
127+
ctx.irep_node.is_a?(GraphQL::InternalRepresentation::Node),
128+
ctx.ast_node.is_a?(GraphQL::Language::Nodes::AbstractNode),
129+
false, # just testing
130+
]
131+
}
132+
end
133+
end
134+
135+
node_union_type = GraphQL::UnionType.define do
136+
name "Node"
137+
possible_types [person_type, organization_type]
138+
end
139+
140+
query_type = GraphQL::ObjectType.define do
141+
name "Query"
142+
field :node, node_union_type do
143+
argument :id, !types.ID
144+
resolve ->(obj, args, ctx) {
145+
obj[args[:id]]
146+
}
147+
end
148+
149+
field :organization, !organization_type do
150+
argument :id, !types.ID
151+
resolve ->(obj, args, ctx) {
152+
args[:id].start_with?("2") && obj[args[:id]]
153+
}
154+
end
155+
end
156+
157+
GraphQL::Schema.define do
158+
query_execution_strategy execution_strategy
159+
query query_type
160+
161+
resolve_type ->(obj, ctx) {
162+
obj.respond_to?(:birthdate) ? person_type : organization_type
163+
}
164+
end
165+
end
166+
167+
@@schema = build_schema(execution_strategy)
168+
169+
def execute_query(query_string, **kwargs)
170+
kwargs[:root_value] = DATA
171+
@@schema.execute(query_string, **kwargs)
172+
end
173+
174+
def test_it_fetches_data
175+
query_string = %|
176+
query getData($nodeId: ID = "1001") {
177+
flh: node(id: $nodeId) {
178+
__typename
179+
... on Person {
180+
name @include(if: true)
181+
skippedName: name @skip(if: true)
182+
birthdate
183+
age(on: 1477660133)
184+
}
185+
186+
... on NamedEntity {
187+
ne_tn: __typename
188+
ne_n: name
189+
}
190+
191+
... on Organization {
192+
org_n: name
193+
}
194+
}
195+
}
196+
|
197+
res = execute_query(query_string)
198+
199+
assert_equal nil, res["errors"], "It doesn't have an errors key"
200+
201+
flh = res["data"]["flh"]
202+
assert_equal "Fannie Lou Hamer", flh["name"], "It returns values"
203+
assert_equal Time.new(1917, 10, 6).to_i, flh["birthdate"], "It returns custom scalars"
204+
assert_equal 99, flh["age"], "It runs resolve functions"
205+
assert_equal "Person", flh["__typename"], "It serves __typename"
206+
assert_equal "Person", flh["ne_tn"], "It serves __typename on interfaces"
207+
assert_equal "Fannie Lou Hamer", flh["ne_n"], "It serves interface fields"
208+
assert_equal false, flh.key?("skippedName"), "It obeys @skip"
209+
assert_equal false, flh.key?("org_n"), "It doesn't apply other type fields"
210+
end
211+
212+
def test_it_propagates_nulls_to_field
213+
query_string = %|
214+
query getOrg($id: ID = "2001"){
215+
failure: node(id: $id) {
216+
... on Organization {
217+
name
218+
leader { name }
219+
}
220+
}
221+
success: node(id: $id) {
222+
... on Organization {
223+
name
224+
}
225+
}
226+
}
227+
|
228+
res = execute_query(query_string)
229+
230+
failure = res["data"]["failure"]
231+
success = res["data"]["success"]
232+
233+
assert_equal nil, failure, "It propagates nulls to the next nullable field"
234+
assert_equal "SNCC", success["name"], "It serves the same object if no invalid null is encountered"
235+
assert_equal 1, res["errors"].length , "It returns an error for the invalid null"
236+
end
237+
238+
def test_it_propages_nulls_to_operation
239+
query_string = %|
240+
{
241+
foundOrg: organization(id: "2001") {
242+
name
243+
}
244+
organization(id: "2999") {
245+
name
246+
}
247+
}
248+
|
249+
250+
res = execute_query(query_string)
251+
assert_equal nil, res["data"]
252+
assert_equal 1, res["errors"].length
253+
end
254+
255+
def test_it_exposes_raised_and_returned_user_execution_errors
256+
query_string = %|
257+
{
258+
organization(id: "2001") {
259+
name
260+
returnedError
261+
raisedError
262+
}
263+
}
264+
|
265+
266+
res = execute_query(query_string)
267+
268+
assert_equal "SNCC", res["data"]["organization"]["name"], "It runs the rest of the query"
269+
270+
expected_returned_error = {
271+
"message"=>"This error was returned",
272+
"locations"=>[{"line"=>5, "column"=>19}],
273+
"path"=>["organization", "returnedError"]
274+
}
275+
assert_includes res["errors"], expected_returned_error, "It turns returned errors into response errors"
276+
277+
expected_raised_error = {
278+
"message"=>"This error was raised",
279+
"locations"=>[{"line"=>6, "column"=>19}],
280+
"path"=>["organization", "raisedError"]
281+
}
282+
assert_includes res["errors"], expected_raised_error, "It turns raised errors into response errors"
283+
end
284+
285+
def test_it_applies_masking
286+
no_org = ->(member) { member.name == "Organization" }
287+
query_string = %|
288+
{
289+
node(id: "2001") {
290+
__typename
291+
}
292+
}|
293+
294+
assert_raises(GraphQL::UnresolvedTypeError) {
295+
execute_query(query_string, except: no_org)
296+
}
297+
298+
query_string = %|
299+
{
300+
organization(id: "2001") { name }
301+
}|
302+
303+
res = execute_query(query_string, except: no_org)
304+
305+
assert_equal nil, res["data"]
306+
assert_equal 1, res["errors"].length
307+
308+
query_string = %|
309+
{
310+
__type(name: "Organization") { name }
311+
}|
312+
313+
res = execute_query(query_string, except: no_org)
314+
315+
assert_equal nil, res["data"]["__type"]
316+
assert_equal nil, res["errors"]
317+
end
318+
319+
def test_it_provides_nodes_to_resolve
320+
query_string = %|
321+
{
322+
organization(id: "2001") {
323+
name
324+
nodePresence
325+
}
326+
}|
327+
328+
res = execute_query(query_string)
329+
assert_equal "SNCC", res["data"]["organization"]["name"]
330+
assert_equal [true, true, false], res["data"]["organization"]["nodePresence"]
331+
end
332+
end
333+
end
334+
end
335+
end
336+
end

spec/graphql/execution_spec.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require "spec_helper"
2+
require "graphql/compatibility"
3+
SerialExecutionSuite = GraphQL::Compatibility::ExecutionSpec.build_suite(GraphQL::Query::SerialExecution)

0 commit comments

Comments
 (0)