graphql-engine/server/tests-py/test_actions.py
Rakesh Emmadi 362230e8d0 server: async action query subscription
Multi source support had limited the availability of async action queries in subscriptions. This PR
adds support for async action query subscriptions with new implementation. Also addresses https://github.com/hasura/graphql-engine/issues/6460.

GitOrigin-RevId: 5ddc321073d224f287dc4b86ce2239ff55190b36
2021-03-31 10:40:15 +00:00

571 lines
18 KiB
Python

#!/usr/bin/env python3
import pytest
import time
import subprocess
from validate import check_query_f, check_query, get_conf_f
from remote_server import NodeGraphQL
"""
TODO:- Test Actions metadata
"""
@pytest.fixture(scope="module")
def graphql_service():
svc = NodeGraphQL(["node", "remote_schemas/nodejs/actions_remote_join_schema.js"])
svc.start()
yield svc
svc.stop()
use_action_fixtures = pytest.mark.usefixtures(
"actions_fixture",
'per_class_db_schema_for_mutation_tests',
'per_method_db_data_for_mutation_tests'
)
use_action_fixtures_with_remote_joins = pytest.mark.usefixtures(
"graphql_service",
"actions_fixture",
"per_class_db_schema_for_mutation_tests",
"per_method_db_data_for_mutation_tests"
)
@pytest.mark.parametrize("transport", ['websocket'])
@use_action_fixtures
class TestActionsSyncWebsocket:
@classmethod
def dir(cls):
return 'queries/actions/sync'
def test_create_user_fail(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_fail.yaml', transport)
def test_create_user_success(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_success.yaml', transport)
def test_create_user_relationship(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_relationship.yaml', transport)
def test_create_user_relationship(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_relationship_fail.yaml', transport)
def test_create_users_fail(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_users_fail.yaml', transport)
def test_create_users_success(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_users_success.yaml', transport)
@use_action_fixtures
class TestActionsSync:
@classmethod
def dir(cls):
return 'queries/actions/sync'
def test_invalid_webhook_response(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/invalid_webhook_response.yaml')
def test_expecting_object_response(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/expecting_object_response.yaml')
def test_expecting_array_response(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/expecting_array_response.yaml')
# Webhook response validation tests. See https://github.com/hasura/graphql-engine/issues/3977
def test_mirror_action_not_null(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/mirror_action_not_null.yaml')
def test_mirror_action_unexpected_field(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/mirror_action_unexpected_field.yaml')
def test_mirror_action_no_field(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/mirror_action_no_field.yaml')
def test_mirror_action_success(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/mirror_action_success.yaml')
#https://github.com/hasura/graphql-engine/issues/6631
def test_create_users_output_type(self, hge_ctx):
gql_query = '''
query {
__type(name: "mutation_root"){
fields {
name
type{
kind
}
}
}
}
'''
query = {
'query': gql_query
}
headers = {}
admin_secret = hge_ctx.hge_key
if admin_secret is not None:
headers['X-Hasura-Admin-Secret'] = admin_secret
code, resp, _ = hge_ctx.anyq('/v1/graphql', query, headers)
assert code == 200, resp
resp_data = resp['data']
mutation_root_fields = resp_data['__type']['fields']
# check type for create_users root field
for root_field in mutation_root_fields:
if root_field['name'] == 'create_users':
assert root_field['type']['kind'] == 'LIST', root_field
@use_action_fixtures_with_remote_joins
class TestActionsSyncWithRemoteJoins:
@classmethod
def dir(cls):
return 'queries/actions/sync/remote_joins'
def test_action_with_remote_joins(self,hge_ctx):
check_query_f(hge_ctx,self.dir() + '/action_with_remote_joins.yaml')
# Check query with admin secret tokens
def check_query_secret(hge_ctx, f):
conf = get_conf_f(f)
admin_secret = hge_ctx.hge_key
def add_secret(c):
if admin_secret is not None:
if 'headers' in c:
c['headers']['x-hasura-admin-secret'] = admin_secret
else:
c['headers'] = {
'x-hasura-admin-secret': admin_secret
}
return c
if isinstance(conf, list):
for _, sconf in enumerate(conf):
check_query(hge_ctx, add_secret(sconf), add_auth = False)
else:
check_query(hge_ctx, add_secret(conf), add_auth = False)
@use_action_fixtures
class TestQueryActions:
@classmethod
def dir(cls):
return 'queries/actions/sync'
def test_query_action_fail(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/get_user_by_email_fail.yaml')
def test_query_action_success_output_object(self, hge_ctx):
gql_query = '''
mutation {
insert_user_one(object: {email: "clarke@gmail.com", name:"Clarke"}){
id
}
}
'''
query = {
'query': gql_query
}
headers = {}
admin_secret = hge_ctx.hge_key
if admin_secret is not None:
headers['X-Hasura-Admin-Secret'] = admin_secret
code, resp, _ = hge_ctx.anyq('/v1/graphql', query, headers)
assert code == 200,resp
check_query_f(hge_ctx, self.dir() + '/get_user_by_email_success.yaml')
def test_query_action_success_output_list(self, hge_ctx):
gql_query = '''
mutation {
insert_user(objects:
[{id:1,email: "clarke@gmail.com", name:"Clarke 1"},
{id:2,email: "clarke@gmail.com", name:"Clarke 2"}])
{
returning {
id
}
}
}
'''
query = {
'query': gql_query
}
headers = {}
admin_secret = hge_ctx.hge_key
if admin_secret is not None:
headers['X-Hasura-Admin-Secret'] = admin_secret
code, resp, _ = hge_ctx.anyq('/v1/graphql', query, headers)
assert code == 200,resp
check_query_f(hge_ctx, self.dir() + '/get_users_by_email_success.yaml')
# This test is to make sure that query actions work well with variables.
# Earlier the HGE used to add the query action to the plan cache, which
# results in interrmittent validation errors, like:
# {
# "errors": [
# {
# "extensions": {
# "path": "$.variableValues",
# "code": "validation-failed"
# },
# "message": "unexpected variables: email"
# }
# ]
# }
def test_query_action_should_not_throw_validation_error(self, hge_ctx):
for _ in range(25):
self.test_query_action_success_output_object(hge_ctx)
def test_query_action_with_relationship(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/query_action_relationship_with_permission.yaml')
def mk_headers_with_secret(hge_ctx, headers={}):
admin_secret = hge_ctx.hge_key
if admin_secret:
headers['X-Hasura-Admin-Secret'] = admin_secret
return headers
@use_action_fixtures
class TestActionsSyncResponseHeaders:
@classmethod
def dir(cls):
return 'queries/actions/sync'
# See https://github.com/hasura/graphql-engine/issues/4021
def test_set_cookie_header(self, hge_ctx):
mutation = '''
mutation {
create_user(email: "clarke@gmail.com", name: "Clarke"){
id
}
}
'''
query = {
'query': mutation,
'variables': {}
}
status, resp, resp_headers = hge_ctx.anyq('/v1/graphql', query, mk_headers_with_secret(hge_ctx))
assert status == 200, resp
assert 'data' in resp, resp
assert ('Set-Cookie' in resp_headers and
resp_headers['Set-Cookie'] == 'abcd'), resp_headers
@use_action_fixtures
class TestActionsAsync:
@classmethod
def dir(cls):
return 'queries/actions/async'
def test_create_user_fail(self, hge_ctx):
graphql_mutation = '''
mutation {
create_user(email: "random-email", name: "Clarke")
}
'''
query = {
'query': graphql_mutation,
'variables': {}
}
status, resp, _ = hge_ctx.anyq('/v1/graphql', query, mk_headers_with_secret(hge_ctx))
assert status == 200, resp
assert 'data' in resp
action_id = resp['data']['create_user']
query_async = '''
query ($action_id: uuid!){
create_user(id: $action_id){
id
errors
}
}
'''
query = {
'query': query_async,
'variables': {
'action_id': action_id
}
}
response = {
'data': {
'create_user': {
'id': action_id,
'errors': {
'code': 'invalid-email',
'path': '$',
'error': 'Given email address is not valid'
}
}
}
}
conf = {
'url': '/v1/graphql',
'headers': {},
'query': query,
'status': 200,
'response': response
}
check_query_timeout(hge_ctx, conf, True, 10)
def test_create_user_success(self, hge_ctx):
graphql_mutation = '''
mutation {
create_user(email: "clarke@hasura.io", name: "Clarke")
}
'''
query = {
'query': graphql_mutation,
'variables': {}
}
status, resp, _ = hge_ctx.anyq('/v1/graphql', query, mk_headers_with_secret(hge_ctx))
assert status == 200, resp
assert 'data' in resp
action_id = resp['data']['create_user']
query_async = '''
query ($action_id: uuid!){
create_user(id: $action_id){
__typename
id
output {
__typename
id
user {
__typename
name
email
is_admin
}
}
}
}
'''
query = {
'query': query_async,
'variables': {
'action_id': action_id
}
}
response = {
'data': {
'create_user': {
'__typename': 'create_user',
'id': action_id,
'output': {
'__typename': 'UserId',
'id': 1,
'user': {
'__typename': 'user',
'name': 'Clarke',
'email': 'clarke@hasura.io',
'is_admin': False
}
}
}
}
}
conf = {
'url': '/v1/graphql',
'headers': {},
'query': query,
'status': 200,
'response': response
}
check_query_timeout(hge_ctx, conf, True, 10)
def test_create_user_roles(self, hge_ctx):
graphql_mutation = '''
mutation {
create_user(email: "blake@hasura.io", name: "Blake")
}
'''
query = {
'query': graphql_mutation,
'variables': {}
}
headers_user_1 = mk_headers_with_secret(hge_ctx, {
'X-Hasura-Role': 'user',
'X-Hasura-User-Id': '1'
})
# create action with user-id 1
status, resp, headers = hge_ctx.anyq('/v1/graphql', query, headers_user_1)
assert status == 200, resp
assert 'data' in resp
action_id = resp['data']['create_user']
query_async = '''
query ($action_id: uuid!){
create_user(id: $action_id){
id
output {
id
}
}
}
'''
query = {
'query': query_async,
'variables': {
'action_id': action_id
}
}
headers_user_2 = mk_headers_with_secret(hge_ctx, {
'X-Hasura-Role': 'user',
'X-Hasura-User-Id': '2'
})
conf_user_2 = {
'url': '/v1/graphql',
'headers': headers_user_2,
'query': query,
'status': 200,
'response': {
'data': {
'create_user': None # User 2 shouldn't able to access the action
}
}
}
# Query the action as user-id 2
# Make request without auth using admin_secret
check_query_timeout(hge_ctx, conf_user_2, add_auth = False, timeout = 10)
conf_user_1 = {
'url': '/v1/graphql',
'headers': headers_user_1,
'query': query,
'status': 200,
'response': {
'data': {
'create_user': {
'id': action_id,
'output': {
'id': 1
}
}
}
}
}
# Query the action as user-id 1
# Make request without auth using admin_secret
check_query_timeout(hge_ctx, conf_user_1, add_auth = False, timeout = 10)
def check_query_timeout(hge_ctx, conf, add_auth, timeout):
wait_until = time.time() + timeout
while True:
time.sleep(2)
try:
check_query(hge_ctx, conf, add_auth = add_auth)
except AssertionError:
if time.time() > wait_until:
raise
else:
continue
break
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestSetCustomTypes:
@classmethod
def dir(cls):
return 'queries/actions/custom-types'
def test_reuse_pgscalars(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/reuse_pgscalars.yaml')
def test_reuse_unknown_pgscalar(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/reuse_unknown_pgscalar.yaml')
def test_create_action_pg_scalar(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/create_action_pg_scalar.yaml')
def test_list_type_relationship(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/list_type_relationship.yaml')
def test_drop_relationship(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/drop_relationship.yaml')
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestActionsMetadata:
@classmethod
def dir(cls):
return 'queries/actions/metadata'
def test_recreate_permission(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/recreate_permission.yaml')
def test_create_with_headers(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/create_with_headers.yaml')
# Test case for bug reported at https://github.com/hasura/graphql-engine/issues/5166
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestActionIntrospection:
@classmethod
def dir(cls):
return 'queries/actions/introspection'
def test_introspection_query(self, hge_ctx):
conf = get_conf_f(self.dir() + '/introspection_query.yaml')
headers = {}
admin_secret = hge_ctx.hge_key
if admin_secret:
headers['X-Hasura-Admin-Secret'] = admin_secret
code, resp, _ = hge_ctx.anyq(conf['url'], conf['query'], headers)
assert code == 200, resp
assert 'data' in resp, resp
@use_action_fixtures
class TestActionTimeout:
@classmethod
def dir(cls):
return 'queries/actions/timeout'
def test_action_timeout_fail(self, hge_ctx):
graphql_mutation = '''
mutation {
create_user(email: "random-email", name: "Clarke")
}
'''
query = {
'query': graphql_mutation,
'variables': {}
}
status, resp, _ = hge_ctx.anyq('/v1/graphql', query, mk_headers_with_secret(hge_ctx))
assert status == 200, resp
assert 'data' in resp
action_id = resp['data']['create_user']
query_async = '''
query ($action_id: uuid!){
create_user(id: $action_id){
id
errors
}
}
'''
query = {
'query': query_async,
'variables': {
'action_id': action_id
}
}
conf = {
'url': '/v1/graphql',
'headers': {},
'query': query,
'status': 200,
}
# Since, the above is an async action, we don't wait for the execution of the webhook.
# We need this sleep of 4 seconds here because only after 3 seconds (sleep duration in the handler)
# we will be getting the result, otherwise the following asserts will fail because the
# response will be empty. This 4 seconds sleep will be concurrent with the sleep duration
# of the handler's execution. So, total time taken for this test will be 4 seconds.
time.sleep(4)
response, _ = check_query(hge_ctx, conf)
assert 'errors' in response['data']['create_user']
assert 'ResponseTimeout' == response['data']['create_user']['errors']['internal']['error']['message']