mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 17:31:56 +03:00
b84db36ebb
* basic doc for actions * custom_types, sync and async actions * switch to graphql-parser-hs on github * update docs * metadata import/export * webhook calls are now supported * relationships in sync actions * initialise.sql is now in sync with the migration file * fix metadata tests * allow specifying arguments of actions * fix blacklist check on check_build_worthiness job * track custom_types and actions related tables * handlers are now triggered on async actions * default to pgjson unless a field is involved in relationships, for generating definition list * use 'true' for action filter for non admin role * fix create_action_permission sql query * drop permissions when dropping an action * add a hdb_role view (and relationships) to fetch all roles in the system * rename 'webhook' key in action definition to 'handler' * allow templating actions wehook URLs with env vars * add 'update_action' /v1/query type * allow forwarding client headers by setting `forward_client_headers` in action definition * add 'headers' configuration in action definition * handle webhook error response based on status codes * support array relationships for custom types * implement single row mutation, see https://github.com/hasura/graphql-engine/issues/3731 * single row mutation: rename 'pk_columns' -> 'columns' and no-op refactor * use top level primary key inputs for delete_by_pk & account select permissions for single row mutations * use only REST semantics to resolve the webhook response * use 'pk_columns' instead of 'columns' for update_by_pk input * add python basic tests for single row mutations * add action context (name) in webhook payload * Async action response is accessible for non admin roles only if the request session vars equals to action's * clean nulls, empty arrays for actions, custom types in export metadata * async action mutation returns only the UUID of the action * unit tests for URL template parser * Basic sync actions python tests * fix output in async query & add async tests * add admin secret header in async actions python test * document async action architecture in Resolve/Action.hs file * support actions returning array of objects * tests for list type response actions * update docs with actions and custom types metadata API reference * update actions python tests as per #f8e1330 Co-authored-by: Tirumarai Selvan <tirumarai.selvan@gmail.com> Co-authored-by: Aravind Shankar <face11301@gmail.com> Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com>
395 lines
14 KiB
Python
395 lines
14 KiB
Python
import pytest
|
|
import time
|
|
from context import HGECtx, HGECtxError, ActionsWebhookServer, EvtsWebhookServer, HGECtxGQLServer, GQLWsClient, PytestConf
|
|
import threading
|
|
import random
|
|
from datetime import datetime
|
|
import sys
|
|
import os
|
|
from collections import OrderedDict
|
|
|
|
def pytest_addoption(parser):
|
|
parser.addoption(
|
|
"--hge-urls",
|
|
metavar="HGE_URLS",
|
|
help="csv list of urls for graphql-engine",
|
|
required=False,
|
|
nargs='+'
|
|
)
|
|
parser.addoption(
|
|
"--pg-urls", metavar="PG_URLS",
|
|
help="csv list of urls for connecting to Postgres directly",
|
|
required=False,
|
|
nargs='+'
|
|
)
|
|
parser.addoption(
|
|
"--hge-key", metavar="HGE_KEY", help="admin secret key for graphql-engine", required=False
|
|
)
|
|
parser.addoption(
|
|
"--hge-webhook", metavar="HGE_WEBHOOK", help="url for graphql-engine's access control webhook", required=False
|
|
)
|
|
parser.addoption(
|
|
"--test-webhook-insecure", action="store_true",
|
|
help="Run Test cases for insecure https webhook"
|
|
)
|
|
parser.addoption(
|
|
"--hge-jwt-key-file", metavar="HGE_JWT_KEY_FILE", help="File containting the private key used to encode jwt tokens using RS512 algorithm", required=False
|
|
)
|
|
parser.addoption(
|
|
"--hge-jwt-conf", metavar="HGE_JWT_CONF", help="The JWT conf", required=False
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-cors", action="store_true",
|
|
required=False,
|
|
help="Run testcases for CORS configuration"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-ws-init-cookie",
|
|
metavar="read|noread",
|
|
required=False,
|
|
help="Run testcases for testing cookie sending over websockets"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-metadata-disabled", action="store_true",
|
|
help="Run Test cases with metadata queries being disabled"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-graphql-disabled", action="store_true",
|
|
help="Run Test cases with GraphQL queries being disabled"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-hge-scale-url",
|
|
metavar="<url>",
|
|
required=False,
|
|
help="Run testcases for horizontal scaling"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-allowlist-queries", action="store_true",
|
|
help="Run Test cases with allowlist queries enabled"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-logging",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="Run testcases for logging"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--test-jwk-url",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="Run testcases for JWK url behaviour"
|
|
)
|
|
|
|
parser.addoption(
|
|
"--accept",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="Accept any failing test cases from YAML files as correct, and write the new files out to disk."
|
|
)
|
|
parser.addoption(
|
|
"--skip-schema-teardown",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="""
|
|
Skip tearing down the schema/Hasura metadata after tests. This option may result in test failures if the schema
|
|
has to change between the list of tests to be run
|
|
"""
|
|
)
|
|
parser.addoption(
|
|
"--skip-schema-setup",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="""
|
|
Skip setting up schema/Hasura metadata before tests.
|
|
This option may result in test failures if the schema has to change between the list of tests to be run
|
|
"""
|
|
)
|
|
|
|
parser.addoption(
|
|
"--avoid-error-message-checks",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="""
|
|
This option when set will ignore disparity in error messages between expected and response outputs.
|
|
Used basically in version upgrade/downgrade tests where the error messages may change
|
|
"""
|
|
)
|
|
|
|
parser.addoption(
|
|
"--collect-upgrade-tests-to-file",
|
|
metavar="<path>",
|
|
required=False,
|
|
help="When used along with collect-only, it will write the list of upgrade tests into the file specified"
|
|
)
|
|
|
|
#By default,
|
|
#1) Set default parallelism to one
|
|
#2) Set test grouping to by filename (--dist=loadfile)
|
|
def pytest_cmdline_preparse(config, args):
|
|
worker = os.environ.get('PYTEST_XDIST_WORKER')
|
|
if 'xdist' in sys.modules and not worker: # pytest-xdist plugin
|
|
num = 1
|
|
args[:] = ["-n" + str(num),"--dist=loadfile"] + args
|
|
|
|
def pytest_configure(config):
|
|
# Pytest has removed the global pytest.config
|
|
# As a solution we are going to store it in PytestConf.config
|
|
PytestConf.config = config
|
|
if is_help_option_present(config):
|
|
return
|
|
if is_master(config):
|
|
if not config.getoption('--hge-urls'):
|
|
print("hge-urls should be specified")
|
|
if not config.getoption('--pg-urls'):
|
|
print("pg-urls should be specified")
|
|
config.hge_url_list = config.getoption('--hge-urls')
|
|
config.pg_url_list = config.getoption('--pg-urls')
|
|
config.hge_ctx_gql_server = HGECtxGQLServer(config.hge_url_list)
|
|
if config.getoption('-n', default=None):
|
|
xdist_threads = config.getoption('-n')
|
|
assert xdist_threads <= len(config.hge_url_list), "Not enough hge_urls specified, Required " + str(xdist_threads) + ", got " + str(len(config.hge_url_list))
|
|
assert xdist_threads <= len(config.pg_url_list), "Not enough pg_urls specified, Required " + str(xdist_threads) + ", got " + str(len(config.pg_url_list))
|
|
|
|
random.seed(datetime.now())
|
|
|
|
|
|
@pytest.hookimpl()
|
|
def pytest_report_collectionfinish(config, startdir, items):
|
|
"""
|
|
Collect server upgrade tests to the given file
|
|
"""
|
|
tests_file = config.getoption('--collect-upgrade-tests-to-file')
|
|
sep=''
|
|
tests=OrderedDict()
|
|
if tests_file:
|
|
def is_upgrade_test(item):
|
|
# Check if allow_server_upgrade_tests marker are present
|
|
# skip_server_upgrade_tests marker is not present
|
|
return item.get_closest_marker('allow_server_upgrade_test') \
|
|
and not item.get_closest_marker('skip_server_upgrade_test')
|
|
with open(tests_file,'w') as f:
|
|
upgrade_items = filter(is_upgrade_test, items)
|
|
for item in upgrade_items:
|
|
# This test should be run separately,
|
|
# since its schema setup has function scope
|
|
if 'per_method_tests_db_state' in item.fixturenames:
|
|
tests[item.nodeid] = True
|
|
elif any([ (x in item.fixturenames)
|
|
for x in
|
|
[ 'per_class_tests_db_state',
|
|
'per_class_db_schema_for_mutation_tests'
|
|
]
|
|
]):
|
|
# For this test, schema setup has class scope
|
|
# We can run a class of these tests at a time
|
|
tests[item.parent.nodeid] = True
|
|
# Assume tests can only be run separately
|
|
else:
|
|
tests[item.nodeid] = True
|
|
for test in tests.keys():
|
|
f.write(test + '\n')
|
|
return ''
|
|
|
|
|
|
|
|
@pytest.hookimpl(optionalhook=True)
|
|
def pytest_configure_node(node):
|
|
if is_help_option_present(node.config):
|
|
return
|
|
# Pytest has removed the global pytest.config
|
|
node.slaveinput["hge-url"] = node.config.hge_url_list.pop()
|
|
node.slaveinput["pg-url"] = node.config.pg_url_list.pop()
|
|
|
|
def pytest_unconfigure(config):
|
|
if is_help_option_present(config):
|
|
return
|
|
config.hge_ctx_gql_server.teardown()
|
|
|
|
@pytest.fixture(scope='module')
|
|
def hge_ctx(request):
|
|
config = request.config
|
|
print("create hge_ctx")
|
|
if is_master(config):
|
|
hge_url = config.hge_url_list[0]
|
|
else:
|
|
hge_url = config.slaveinput["hge-url"]
|
|
|
|
if is_master(config):
|
|
pg_url = config.pg_url_list[0]
|
|
else:
|
|
pg_url = config.slaveinput["pg-url"]
|
|
|
|
try:
|
|
hge_ctx = HGECtx(hge_url, pg_url, config)
|
|
except HGECtxError as e:
|
|
assert False, "Error from hge_cxt: " + str(e)
|
|
# TODO this breaks things (https://github.com/pytest-dev/pytest-xdist/issues/86)
|
|
# so at least make sure the real error gets printed (above)
|
|
pytest.exit(str(e))
|
|
|
|
yield hge_ctx # provide the fixture value
|
|
print("teardown hge_ctx")
|
|
hge_ctx.teardown()
|
|
time.sleep(1)
|
|
|
|
@pytest.fixture(scope='class')
|
|
def evts_webhook(request):
|
|
webhook_httpd = EvtsWebhookServer(server_address=('127.0.0.1', 5592))
|
|
web_server = threading.Thread(target=webhook_httpd.serve_forever)
|
|
web_server.start()
|
|
yield webhook_httpd
|
|
webhook_httpd.shutdown()
|
|
webhook_httpd.server_close()
|
|
web_server.join()
|
|
|
|
@pytest.fixture(scope='module')
|
|
def actions_webhook(hge_ctx):
|
|
webhook_httpd = ActionsWebhookServer(hge_ctx, server_address=('127.0.0.1', 5593))
|
|
web_server = threading.Thread(target=webhook_httpd.serve_forever)
|
|
web_server.start()
|
|
yield webhook_httpd
|
|
webhook_httpd.shutdown()
|
|
webhook_httpd.server_close()
|
|
web_server.join()
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ws_client(request, hge_ctx):
|
|
"""
|
|
This fixture provides an Apollo GraphQL websockets client
|
|
"""
|
|
client = GQLWsClient(hge_ctx, '/v1/graphql')
|
|
time.sleep(0.1)
|
|
yield client
|
|
client.teardown()
|
|
|
|
@pytest.fixture(scope='class')
|
|
def per_class_tests_db_state(request, hge_ctx):
|
|
"""
|
|
Set up the database state for select queries.
|
|
Has a class level scope, since select queries does not change database state
|
|
Expects either `dir()` method which provides the directory
|
|
with `setup.yaml` and `teardown.yaml` files
|
|
Or class variables `setup_files` and `teardown_files` that provides
|
|
the list of setup and teardown files respectively
|
|
"""
|
|
yield from db_state_context(request, hge_ctx)
|
|
|
|
@pytest.fixture(scope='function')
|
|
def per_method_tests_db_state(request, hge_ctx):
|
|
"""
|
|
This fixture sets up the database state for metadata operations
|
|
Has a function level scope, since metadata operations may change both the schema and data
|
|
Class method/variable requirements are similar to that of per_class_tests_db_state fixture
|
|
"""
|
|
yield from db_state_context(request, hge_ctx)
|
|
|
|
@pytest.fixture(scope='class')
|
|
def per_class_db_schema_for_mutation_tests(request, hge_ctx):
|
|
"""
|
|
This fixture sets up the database schema for mutations.
|
|
It has a class level scope, since mutations does not change schema.
|
|
Expects either `dir()` class method which provides the directory with `schema_setup.yaml` and `schema_teardown.yaml` files,
|
|
or variables `schema_setup_files` and `schema_teardown_files`
|
|
that provides the list of setup and teardown files respectively
|
|
"""
|
|
yield from db_context_with_schema_common(
|
|
request, hge_ctx, 'schema_setup_files', 'schema_setup.yaml', 'schema_teardown_files', 'schema_teardown.yaml', True
|
|
)
|
|
|
|
@pytest.fixture(scope='function')
|
|
def per_method_db_data_for_mutation_tests(request, hge_ctx, per_class_db_schema_for_mutation_tests):
|
|
"""
|
|
This fixture sets up the data for mutations.
|
|
Has a function level scope, since mutations may change data.
|
|
Having just the setup file(s), or the teardown file(s) is allowed.
|
|
Expects either `dir()` class method which provides the directory with `values_setup.yaml` and / or `values_teardown.yaml` files.
|
|
The class may provide `values_setup_files` variables which contains the list of data setup files,
|
|
Or the `values_teardown_files` variable which provides the list of data teardown files.
|
|
"""
|
|
yield from db_context_common(
|
|
request, hge_ctx, 'values_setup_files', 'values_setup.yaml',
|
|
'values_teardown_files', 'values_teardown.yaml',
|
|
False, False, False
|
|
)
|
|
|
|
def db_state_context(request, hge_ctx):
|
|
yield from db_context_with_schema_common(
|
|
request, hge_ctx, 'setup_files', 'setup.yaml', 'teardown_files',
|
|
'teardown.yaml', True
|
|
)
|
|
|
|
def db_context_with_schema_common(
|
|
request, hge_ctx, setup_files_attr, setup_default_file,
|
|
teardown_files_attr, teardown_default_file, check_file_exists=True):
|
|
(skip_setup, skip_teardown) = [
|
|
request.config.getoption('--' + x)
|
|
for x in ['skip-schema-setup', 'skip-schema-teardown']
|
|
]
|
|
yield from db_context_common(
|
|
request, hge_ctx, setup_files_attr, setup_default_file,
|
|
teardown_files_attr, teardown_default_file,
|
|
check_file_exists, skip_setup, skip_teardown
|
|
)
|
|
|
|
def db_context_common(
|
|
request, hge_ctx, setup_files_attr, setup_default_file,
|
|
teardown_files_attr, teardown_default_file,
|
|
check_file_exists=True, skip_setup=True, skip_teardown=True ):
|
|
def get_files(attr, default_file):
|
|
files = getattr(request.cls, attr, None)
|
|
if not files:
|
|
files = os.path.join(request.cls.dir(), default_file)
|
|
return files
|
|
setup = get_files(setup_files_attr, setup_default_file)
|
|
teardown = get_files(teardown_files_attr, teardown_default_file)
|
|
yield from setup_and_teardown(request, hge_ctx, setup, teardown, check_file_exists, skip_setup, skip_teardown)
|
|
|
|
def setup_and_teardown(request, hge_ctx, setup_files, teardown_files, check_file_exists=True, skip_setup=False, skip_teardown=False):
|
|
def assert_file_exists(f):
|
|
assert os.path.isfile(f), 'Could not find file ' + f
|
|
if check_file_exists:
|
|
for o in [setup_files, teardown_files]:
|
|
run_on_elem_or_list(assert_file_exists, o)
|
|
def v1q_f(f):
|
|
if os.path.isfile(f):
|
|
st_code, resp = hge_ctx.v1q_f(f)
|
|
assert st_code == 200, resp
|
|
if not skip_setup:
|
|
run_on_elem_or_list(v1q_f, setup_files)
|
|
yield
|
|
# Teardown anyway if any of the tests have failed
|
|
if request.session.testsfailed > 0 or not skip_teardown:
|
|
run_on_elem_or_list(v1q_f, teardown_files)
|
|
|
|
def run_on_elem_or_list(f, x):
|
|
if isinstance(x, str):
|
|
return [f(x)]
|
|
elif isinstance(x, list):
|
|
return [f(e) for e in x]
|
|
|
|
def is_help_option_present(config):
|
|
return any([
|
|
config.getoption(x)
|
|
for x in ['--fixtures','--help', '--collect-only']
|
|
])
|
|
|
|
def is_master(config):
|
|
"""True if the code running the given pytest.config object is running in a xdist master
|
|
node or not running xdist at all.
|
|
"""
|
|
return not hasattr(config, 'slaveinput')
|