Skip to content

Commit 1627744

Browse files
committed
tornado graphql handler testing
1 parent 47131f3 commit 1627744

File tree

2 files changed

+260
-4
lines changed

2 files changed

+260
-4
lines changed

cylc/uiserver/graphql/tornado.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def parse_body(self):
235235
try:
236236
body = self.request.body
237237
except Exception as e:
238-
raise ExecutionError(400, e)
238+
raise ExecutionError(400, [e])
239239

240240
try:
241241
request_json = json.loads(body)
@@ -337,7 +337,7 @@ async def execute_graphql_request(
337337

338338
return result
339339
except Exception as e:
340-
return ExecutionResult(errors=[e])
340+
return ExecutionResult(data=None, errors=[e])
341341

342342
async def execute(self, *args, **kwargs):
343343
return execute(*args, **kwargs)

cylc/uiserver/tests/test_handlers.py

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,273 @@
2020
from unittest.mock import MagicMock
2121
import pytest
2222

23+
from graphql import ExecutionResult, GraphQLError
24+
from graphql.execution.middleware import MiddlewareManager
2325
from tornado.httputil import HTTPServerRequest
24-
from tornado.testing import AsyncHTTPTestCase, get_async_test_timeout
26+
from tornado.testing import AsyncHTTPTestCase, get_async_test_timeout, gen_test
2527
from tornado.web import Application
28+
from tornado.web import HTTPError
2629

30+
from cylc.flow.network.graphql import (
31+
CylcExecutionContext, IgnoreFieldMiddleware
32+
)
33+
from cylc.uiserver.authorise import AuthorizationMiddleware
34+
from cylc.uiserver.graphql.tornado import TornadoGraphQLHandler, ExecutionError
2735
from cylc.uiserver.graphql.tornado_ws import GRAPHQL_WS
28-
from cylc.uiserver.handlers import SubscriptionHandler
36+
from cylc.uiserver.handlers import (
37+
SubscriptionHandler,
38+
UIServerGraphQLHandler,
39+
)
40+
from cylc.uiserver.schema import schema
2941

3042

3143
class MyApplication(Application):
3244
...
3345

46+
class GraphQLHandlersTest(AsyncHTTPTestCase):
47+
"""Test for TornadoGraphQLHandler/UIServerGraphQLHandler"""
48+
49+
def get_app(self, handler_class=UIServerGraphQLHandler) -> Application:
50+
return MyApplication(
51+
handlers=[
52+
(
53+
'graphql',
54+
handler_class,
55+
{
56+
'schema': schema,
57+
'middleware': [
58+
AuthorizationMiddleware,
59+
IgnoreFieldMiddleware
60+
],
61+
'execution_context_class': CylcExecutionContext,
62+
}
63+
),
64+
]
65+
)
66+
67+
def _create_handler(
68+
self,
69+
handler_class=UIServerGraphQLHandler,
70+
r_kwargs={},
71+
h_kwargs={},
72+
logged_in=True,
73+
):
74+
app = self.get_app(handler_class)
75+
r_params = {
76+
'uri': '/graphql',
77+
'method': 'GET',
78+
**r_kwargs,
79+
}
80+
request = HTTPServerRequest(**r_params)
81+
request.connection = MagicMock()
82+
h_params = {
83+
'middleware': [
84+
AuthorizationMiddleware,
85+
IgnoreFieldMiddleware
86+
],
87+
'execution_context_class': CylcExecutionContext,
88+
**h_kwargs,
89+
}
90+
handler = handler_class(
91+
application=app,
92+
request=request,
93+
schema=schema,
94+
**h_params,
95+
)
96+
97+
if logged_in:
98+
handler.get
99+
_current_user = lambda: {'name': getuser()}
100+
else:
101+
handler.current_user = None
102+
return handler
103+
104+
def test_setup(self):
105+
"""Test setup, attributes, and properties."""
106+
mid_manager = MiddlewareManager()
107+
handler = self._create_handler(
108+
handler_class=TornadoGraphQLHandler,
109+
h_kwargs={'middleware': mid_manager}
110+
)
111+
assert handler.middleware == mid_manager
112+
113+
handler = self._create_handler(
114+
handler_class=TornadoGraphQLHandler,
115+
h_kwargs={'middleware': None},
116+
)
117+
assert not handler.middleware
118+
assert handler.get_context().method == "GET"
119+
120+
def test_parse_body(self):
121+
"""Test parse_body for query doc."""
122+
handler = self._create_handler(
123+
r_kwargs={
124+
'headers': {'Content-Type': "application/json"},
125+
'method': "POST",
126+
}
127+
)
128+
assert not handler.get_parsed_body()
129+
130+
class FakeRequest:
131+
headers = {'Content-Type': "Application/json"}
132+
errors = None
133+
134+
# test request without body.
135+
orig_request = handler.request
136+
handler.request = FakeRequest()
137+
with pytest.raises(ExecutionError) as exc:
138+
handler.parse_body()
139+
assert 'FakeRequest' in str(exc)
140+
141+
ex_error = ExecutionError(123, None)
142+
assert ex_error.status_code == 123
143+
assert ex_error.message == ''
144+
145+
# test invalid JSON string
146+
handler.request = orig_request
147+
handler.request.body = '%&^ds:ea43#'
148+
with pytest.raises(HTTPError) as exc:
149+
handler.parse_body()
150+
assert 'invalid JSON' in str(exc.value)
151+
152+
# test invalid query
153+
handler.request.body = json.dumps([{'this': "that"}])
154+
with pytest.raises(HTTPError) as exc:
155+
handler.parse_body()
156+
assert 'not a valid JSON query.' in str(exc.value)
157+
158+
@gen_test
159+
@pytest.mark.usefixtures("mock_authentication_yossarian")
160+
async def test_get_response(self):
161+
"""Test get_response."""
162+
r_kwargs = {
163+
'headers': {'Content-Type': "application/json"},
164+
'method': "POST",
165+
'body': json.dumps({'query': "curry { massaman }"})
166+
}
167+
h_kwargs = {
168+
'execution_context_class': None,
169+
'parsed_body': 'cake',
170+
}
171+
handler = self._create_handler(r_kwargs=r_kwargs, h_kwargs=h_kwargs)
172+
assert handler.parsed_body == 'cake'
173+
174+
# Test bad query
175+
data = handler.parse_body()
176+
result, status_code = await handler.get_response(data)
177+
assert status_code == 400
178+
assert 'curry' in result
179+
180+
# Test good query
181+
doc = {
182+
'query': '''
183+
query stoke ($wFlows: [ID]){
184+
workflows (ids: $wFlows) { id status }
185+
}
186+
''',
187+
'variables': {'wFlows': ["*/run1"]},
188+
'operationName': 'stoke',
189+
}
190+
191+
handler.request.body = json.dumps(doc)
192+
data = handler.parse_body()
193+
handler.graphql_params = {}
194+
result, status_code = await handler.get_response(data)
195+
assert status_code == 200
196+
# caught by auth
197+
assert 'Forbidden' in str(result)
198+
assert handler.graphql_params[2] == 'stoke'
199+
# test the graphql params bypass
200+
result, status_code = await handler.get_response(data)
201+
assert status_code == 200
202+
assert 'Forbidden' in str(result)
203+
204+
# test bad schema
205+
orig_type = handler.schema.graphql_schema.query_type
206+
handler.schema.graphql_schema.query_type = None
207+
result, status_code = await handler.get_response(data)
208+
assert 'not configured to execute query' in result
209+
assert status_code == 400
210+
handler.schema.graphql_schema.query_type = orig_type
211+
handler.schema.graphql_schema._validation_errors = ['nasty one']
212+
result, status_code = await handler.get_response(data)
213+
assert 'nasty one' in results
214+
assert status_code == 400
215+
handler.schema.graphql_schema._validation_errors = []
216+
217+
# by-pass auth middleware
218+
h_kwargs['middleware'] = []
219+
doc['operationName'] = 'null'
220+
handler = self._create_handler(r_kwargs=r_kwargs, h_kwargs=h_kwargs)
221+
handler.request.body = json.dumps(doc)
222+
data = handler.parse_body()
223+
handler.graphql_params = {}
224+
result, status_code = await handler.get_response(data)
225+
assert status_code == 200
226+
# because no resolvers set:
227+
assert "object has no attribute 'get_workflows'" in str(result)
228+
# operationName should be None
229+
assert handler.graphql_params[2] is None
230+
231+
# test invalid variable
232+
doc['variables'] = 'Nope'
233+
handler.request.body = json.dumps(doc)
234+
data = handler.parse_body()
235+
handler.graphql_params = {}
236+
with pytest.raises(HTTPError) as exc:
237+
await handler.get_response(data)
238+
assert 'Variables are invalid JSON' in str(exc.value)
239+
240+
doc['variables'] = {'wFlows': ["*/run1"]},
241+
doc['operationName'] = 'stoke'
242+
handler.request.body = json.dumps(doc)
243+
data = handler.parse_body()
244+
handler.graphql_params = {}
245+
246+
# test execution error
247+
async def exec_error(schema, doc, **kwargs):
248+
raise ExecutionError(400, ['problems'])
249+
orig_exec = handler.execute
250+
handler.execute = exec_error
251+
result, status_code = await handler.get_response(data)
252+
assert status_code == 400
253+
handler.execute = orig_exec
254+
255+
# replicate the None result
256+
async def exe_gql_req(data, query, variables, operation_name):
257+
return None
258+
handler.execute_graphql_request = exe_gql_req
259+
result, status_code = await handler.get_response(data)
260+
assert status_code == 200
261+
assert result is None
262+
263+
# test an execution result with get method (i.e. other api link)
264+
async def exe_gql_req2(data, query, variables, operation_name):
265+
class FakeResult:
266+
def get(self):
267+
return ExecutionResult(data='other-api-response')
268+
return FakeResult()
269+
handler.execute_graphql_request = exe_gql_req2
270+
result, status_code = await handler.get_response(data)
271+
assert status_code == 200
272+
assert 'other-api-response' in result
273+
274+
# test an execution result with errors
275+
async def exe_gql_req3(data, query, variables, operation_name):
276+
return ExecutionResult(
277+
errors = [
278+
GraphQLError('GQL error'),
279+
ExecutionError(400, ['Some Exc error']),
280+
Exception('random other error')
281+
]
282+
)
283+
handler.execute_graphql_request = exe_gql_req3
284+
result, status_code = await handler.get_response(data)
285+
assert status_code == 400
286+
assert 'GQL error' in result
287+
assert 'Some Exc error' in result
288+
assert 'random other error' in result
289+
34290

35291
class SubscriptionHandlerTest(AsyncHTTPTestCase):
36292
"""Test for SubscriptionHandler"""

0 commit comments

Comments
 (0)