mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-13 19:33:55 +03:00
server/tests-py: Start helper services in the test harness.
This makes it possible for the test harness to start the test JWK server and the test remote schema server. In order to do this, we still generate the TLS certificates in the test script (because we need to install the generated CA certificate in the OS certificate store), and then pass the certificate and key paths into the test runner. Because we are still using _test-server.sh_ for now, we don't use the JWK server fixture in that case, as HGE needs the JWK server to be up and running when it starts. Instead, we keep running it outside (for now). This is also the case for the GraphQL server fixture when we are running the server upgrade/downgrade tests. I have also refactored _graphql_server.py_ so there isn't a global `HGE_URLS` value, but instead the value is passed through. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6303 GitOrigin-RevId: 06f05ff674372dc5d632e55d68e661f5c7a17c10
This commit is contained in:
parent
2e0cd9267b
commit
0d4d7e6b1e
@ -22,11 +22,9 @@ stop_services() {
|
||||
|
||||
[[ -n "${HGE_PIDS[*]}" ]] && kill -s INT "${HGE_PIDS[@]}" || true
|
||||
[[ -n "$WH_PID" ]] && kill "$WH_PID" || true
|
||||
[[ -n "$GQL_SERVER_PID" ]] && kill "$GQL_SERVER_PID" || true
|
||||
|
||||
[[ -n "${HGE_PIDS[*]}" ]] && wait "${HGE_PIDS[@]}" || true
|
||||
[[ -n "$WH_PID" ]] && wait "$WH_PID" || true
|
||||
[[ -n "$GQL_SERVER_PID" ]] && wait "$GQL_SERVER_PID" || true
|
||||
}
|
||||
|
||||
time_elapsed() {
|
||||
@ -229,7 +227,6 @@ PYTEST_PARALLEL_ARGS=(
|
||||
|
||||
HGE_PIDS=()
|
||||
WH_PID=""
|
||||
GQL_SERVER_PID=""
|
||||
|
||||
trap stop_services ERR
|
||||
trap stop_services INT
|
||||
@ -970,23 +967,18 @@ remote-schema-https)
|
||||
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH SECURE REMOTE SCHEMA #########################>\n"
|
||||
|
||||
OLD_REMOTE_SCHEMAS_WEBHOOK_DOMAIN="${REMOTE_SCHEMAS_WEBHOOK_DOMAIN}"
|
||||
export REMOTE_SCHEMAS_WEBHOOK_DOMAIN="https://localhost:5001"
|
||||
export REMOTE_SCHEMAS_WEBHOOK_DOMAIN="https://localhost:5000"
|
||||
init_ssl
|
||||
|
||||
run_hge_with_args serve
|
||||
|
||||
wait_for_port 8080
|
||||
|
||||
python3 graphql_server.py 5001 "$OUTPUT_FOLDER/ssl/webhook.pem" "$OUTPUT_FOLDER/ssl/webhook-key.pem" >"$OUTPUT_FOLDER/remote_gql_server.log" 2>&1 &
|
||||
GQL_SERVER_PID=$!
|
||||
|
||||
wait_for_port 5001
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" test_schema_stitching.py::TestRemoteSchemaBasic
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--tls-cert="$OUTPUT_FOLDER/ssl/webhook.pem" --tls-key="$OUTPUT_FOLDER/ssl/webhook-key.pem" \
|
||||
test_schema_stitching.py::TestRemoteSchemaBasic
|
||||
|
||||
export REMOTE_SCHEMAS_WEBHOOK_DOMAIN="${OLD_REMOTE_SCHEMAS_WEBHOOK_DOMAIN}"
|
||||
kill_hge_servers
|
||||
kill "$GQL_SERVER_PID"
|
||||
;;
|
||||
|
||||
|
||||
@ -1165,79 +1157,77 @@ jwk-url)
|
||||
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
|
||||
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH JWK URL ########> \n"
|
||||
|
||||
# start the JWK server
|
||||
export JWK_SERVER_URL='http://localhost:5001'
|
||||
|
||||
# Start the JWK server.
|
||||
# There is a fixture to do this, but when running in this fashion, we need to
|
||||
# start the JWK server first so the HGE server can communicate with it.
|
||||
python3 jwk_server.py >"$OUTPUT_FOLDER/jwk_server.log" 2>&1 &
|
||||
JWKS_PID=$!
|
||||
wait_for_port 5001
|
||||
|
||||
echo "Test: Cache-Control with max-age=3"
|
||||
export HASURA_GRAPHQL_JWT_SECRET='{"jwk_url": "http://localhost:5001/jwk-cache-control?max-age=3"}'
|
||||
export HASURA_GRAPHQL_JWT_SECRET="{\"jwk_url\": \"${JWK_SERVER_URL}/jwk-cache-control?max-age=3\"}"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--test-jwk-url \
|
||||
-k 'test_cache_control_header_max_age'
|
||||
-- 'test_jwk.py::test_cache_control_header_max_age'
|
||||
|
||||
kill_hge_servers
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
echo "Test: Cache-Control with must-revalidate, max-age=3"
|
||||
export HASURA_GRAPHQL_JWT_SECRET='{"jwk_url": "http://localhost:5001/jwk-cache-control?must-revalidate=true&must-revalidate=true"}'
|
||||
export HASURA_GRAPHQL_JWT_SECRET="{\"jwk_url\": \"${JWK_SERVER_URL}/jwk-cache-control?max-age=3&must-revalidate=true\"}"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--test-jwk-url \
|
||||
-k 'test_cache_control_header_max_age'
|
||||
-- 'test_jwk.py::test_cache_control_header_max_age_must_revalidate'
|
||||
|
||||
kill_hge_servers
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
echo "Test: Cache-Control with must-revalidate"
|
||||
export HASURA_GRAPHQL_JWT_SECRET='{"jwk_url": "http://localhost:5001/jwk-cache-control?must-revalidate=true"}'
|
||||
export HASURA_GRAPHQL_JWT_SECRET="{\"jwk_url\": \"${JWK_SERVER_URL}/jwk-cache-control?must-revalidate=true\"}"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--test-jwk-url \
|
||||
-k 'test_cache_control_header_no_caching'
|
||||
-- 'test_jwk.py::test_cache_control_header_must_revalidate'
|
||||
|
||||
kill_hge_servers
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
echo "Test: Cache-Control with no-cache, public"
|
||||
export HASURA_GRAPHQL_JWT_SECRET='{"jwk_url": "http://localhost:5001/jwk-cache-control?no-cache=true&public=true"}'
|
||||
export HASURA_GRAPHQL_JWT_SECRET="{\"jwk_url\": \"${JWK_SERVER_URL}/jwk-cache-control?no-cache=true&public=true\"}"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--test-jwk-url \
|
||||
-k 'test_cache_control_header_no_caching'
|
||||
-- 'test_jwk.py::test_cache_control_header_no_cache_public'
|
||||
|
||||
kill_hge_servers
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
echo "Test: Cache-Control with no-store, max-age=3"
|
||||
export HASURA_GRAPHQL_JWT_SECRET='{"jwk_url": "http://localhost:5001/jwk-cache-control?no-store=true&max-age=3"}'
|
||||
export HASURA_GRAPHQL_JWT_SECRET="{\"jwk_url\": \"${JWK_SERVER_URL}/jwk-cache-control?no-store=true&max-age=3\"}"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--test-jwk-url \
|
||||
-k 'test_cache_control_header_no_caching'
|
||||
-- 'test_jwk.py::test_cache_control_header_no_store_max_age'
|
||||
|
||||
kill_hge_servers
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
echo "Test: Expires with three second expiry"
|
||||
export HASURA_GRAPHQL_JWT_SECRET='{"jwk_url": "http://localhost:5001/jwk-expires?seconds=3"}'
|
||||
export HASURA_GRAPHQL_JWT_SECRET="{\"jwk_url\": \"${JWK_SERVER_URL}/jwk-expires?seconds=3\"}"
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest "${PYTEST_COMMON_ARGS[@]}" \
|
||||
--test-jwk-url \
|
||||
-k 'test_expires_header'
|
||||
-- 'test_jwk.py::test_expires_header'
|
||||
|
||||
kill_hge_servers
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
@ -2,11 +2,13 @@ import collections
|
||||
import os
|
||||
import pytest
|
||||
import re
|
||||
import socket
|
||||
import sqlalchemy
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
import auth_webhook_server
|
||||
@ -35,6 +37,10 @@ def pytest_addoption(parser):
|
||||
required=False,
|
||||
nargs='+'
|
||||
)
|
||||
|
||||
parser.addoption('--tls-cert', help='The TLS certificate used for helper services', required=False)
|
||||
parser.addoption('--tls-key', help='The TLS key used for helper services', required=False)
|
||||
|
||||
parser.addoption(
|
||||
"--hge-webhook", metavar="HGE_WEBHOOK", help="url for graphql-engine's access control webhook", required=False
|
||||
)
|
||||
@ -83,14 +89,6 @@ def pytest_addoption(parser):
|
||||
help="Run testcases for startup database calls"
|
||||
)
|
||||
|
||||
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",
|
||||
@ -297,23 +295,33 @@ def pg_version(request) -> int:
|
||||
def pg_url(request) -> str:
|
||||
return request.config.workerinput["pg-url"]
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def tls_configuration(request: pytest.FixtureRequest) -> Optional[Tuple[str, str]]:
|
||||
tls_cert: Optional[str] = request.config.getoption('--tls-cert') # type: ignore
|
||||
tls_key: Optional[str] = request.config.getoption('--tls-key') # type: ignore
|
||||
if tls_cert and tls_key:
|
||||
return (tls_cert, tls_key)
|
||||
else:
|
||||
return None
|
||||
|
||||
# Any test caught by this would also be caught by `hge_skip_function`, but
|
||||
# this is faster.
|
||||
@pytest.fixture(scope='class', autouse=True)
|
||||
def hge_skip_class(request: pytest.FixtureRequest, hge_server: Optional[str]):
|
||||
hge_skip(request, hge_server)
|
||||
def hge_skip_class(request: pytest.FixtureRequest, hge_server: Optional[str], hge_fixture_env: dict[str, str]):
|
||||
hge_skip(request, hge_server, hge_fixture_env)
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
def hge_skip_function(request: pytest.FixtureRequest, hge_server: Optional[str]):
|
||||
hge_skip(request, hge_server)
|
||||
def hge_skip_function(request: pytest.FixtureRequest, hge_server: Optional[str], hge_fixture_env: dict[str, str]):
|
||||
hge_skip(request, hge_server, hge_fixture_env)
|
||||
|
||||
def hge_skip(request: pytest.FixtureRequest, hge_server: Optional[str]):
|
||||
def hge_skip(request: pytest.FixtureRequest, hge_server: Optional[str], hge_fixture_env: dict[str, str]):
|
||||
# Let `hge_server` manage this stuff.
|
||||
if hge_server:
|
||||
return
|
||||
# Ensure that the correct environment variables have been set for the given test.
|
||||
hge_marker_env: dict[str, str] = {marker.args[0]: marker.args[1] for marker in request.node.iter_markers('hge_env')}
|
||||
incorrect_env = {name: value for name, value in hge_marker_env.items() if os.getenv(name) != value}
|
||||
hge_env = {**hge_marker_env, **hge_fixture_env}
|
||||
incorrect_env = {name: value for name, value in hge_env.items() if os.getenv(name) != value}
|
||||
if len(incorrect_env) > 0:
|
||||
pytest.skip(
|
||||
'This test expects the following environment variables: '
|
||||
@ -473,18 +481,39 @@ def scheduled_triggers_evts_webhook(hge_fixture_env: dict[str, str]):
|
||||
webhook_httpd.server_close()
|
||||
web_server.join()
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
@pytest.fixture(scope='class', params=[False, True])
|
||||
@pytest.mark.early
|
||||
def gql_server(request, hge_fixture_env: dict[str, str]):
|
||||
def gql_server(request: pytest.FixtureRequest, hge_bin: Optional[str], hge_url: str, hge_fixture_env: dict[str, str], tls_configuration: Optional[Tuple[str, str]]):
|
||||
port = 5000
|
||||
hge_urls: list[str] = request.config.getoption('--hge-urls')
|
||||
graphql_server.set_hge_urls(hge_urls)
|
||||
server = HGECtxGQLServer(port=port)
|
||||
if socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect_ex(('127.0.0.1', port)) == 0:
|
||||
# The server is already running. This might be the case if we're running the server upgrade/downgrade tests.
|
||||
# In this case, skip starting it and rely on the external service.
|
||||
url = f'http://127.0.0.1:{port}'
|
||||
hge_fixture_env['REMOTE_SCHEMAS_WEBHOOK_DOMAIN'] = url
|
||||
# Create an object with a field named "url", set to `url`.
|
||||
return collections.namedtuple('ExternalGraphQLServer', ['url'])(url)
|
||||
|
||||
hge_urls: list[str] = request.config.getoption('--hge-urls') or [hge_url] # type: ignore
|
||||
|
||||
use_tls: bool = request.param # type: ignore
|
||||
scheme = None
|
||||
if not hge_bin:
|
||||
scheme = str(urllib.parse.urlparse(os.getenv('REMOTE_SCHEMAS_WEBHOOK_DOMAIN')).scheme)
|
||||
if use_tls:
|
||||
if scheme is not None and scheme != 'https':
|
||||
pytest.skip(f'Cannot run the remote schema server with TLS; HGE is configured to talk to it over "{scheme}".')
|
||||
if not tls_configuration:
|
||||
pytest.skip('Cannot run the remote schema server with TLS; no certificate and key provided.')
|
||||
server = HGECtxGQLServer(port=port, tls_configuration=tls_configuration, hge_urls=hge_urls)
|
||||
else:
|
||||
if scheme is not None and scheme != 'http':
|
||||
pytest.skip(f'Cannot run the remote schema server without TLS; HGE is configured to talk to it over "{scheme}".')
|
||||
server = HGECtxGQLServer(port=port, hge_urls=hge_urls)
|
||||
server.start_server()
|
||||
hge_fixture_env['REMOTE_SCHEMAS_WEBHOOK_DOMAIN'] = server.url
|
||||
request.addfinalizer(server.stop_server)
|
||||
ports.wait_for_port(port)
|
||||
yield server
|
||||
server.stop_server()
|
||||
return server
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
def ws_client(request, hge_ctx):
|
||||
|
@ -19,7 +19,7 @@ import string
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
import websocket
|
||||
|
||||
@ -769,14 +769,16 @@ class EvtsWebhookServer(ThreadedHTTPServer):
|
||||
return f'http://{self.server_address[0]}:{self.server_address[1]}'
|
||||
|
||||
class HGECtxGQLServer:
|
||||
def __init__(self, port: int):
|
||||
def __init__(self, port: int, tls_configuration: Optional[Tuple[str, str]] = None, hge_urls: list[str] = []):
|
||||
# start the graphql server
|
||||
self.port = port
|
||||
self.tls_configuration = tls_configuration
|
||||
self.hge_urls = hge_urls
|
||||
self.is_running: bool = False
|
||||
|
||||
def start_server(self):
|
||||
if not self.is_running:
|
||||
self.server = graphql_server.create_server('localhost', self.port)
|
||||
self.server = graphql_server.create_server('localhost', self.port, self.tls_configuration, self.hge_urls)
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.start()
|
||||
self.is_running = True
|
||||
@ -789,7 +791,12 @@ class HGECtxGQLServer:
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f'http://{self.server.server_address[0]}:{self.server.server_address[1]}'
|
||||
scheme = 'https' if self.tls_configuration else 'http'
|
||||
# We must use 'localhost' and not `self.server.server_address[0]`
|
||||
# because when using TLS, we need a domain name, not an IP address.
|
||||
host = 'localhost'
|
||||
port = self.server.server_address[1]
|
||||
return f'{scheme}://{host}:{port}'
|
||||
|
||||
class HGECtx:
|
||||
|
||||
|
@ -8,12 +8,19 @@ from http import HTTPStatus
|
||||
import time
|
||||
import ssl
|
||||
import sys
|
||||
from typing import Callable, NamedTuple, Optional, Tuple, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from graphql import GraphQLError
|
||||
from webserver import MkHandlers, RequestHandler, Response
|
||||
|
||||
HGE_URLS=[]
|
||||
|
||||
DEFAULT_PORT = 5000
|
||||
|
||||
|
||||
class Context(NamedTuple):
|
||||
hge_urls: Optional[list[str]]
|
||||
|
||||
|
||||
def mkJSONResp(graphql_result, extensions={}):
|
||||
return Response(HTTPStatus.OK, {**graphql_result.to_dict(), **extensions},
|
||||
@ -749,21 +756,42 @@ class EchoGraphQL(RequestHandler):
|
||||
{'Content-Type': 'application/json'})
|
||||
|
||||
|
||||
def header_test_expected_headers(hge_urls: Optional[list[str]]) -> dict[str, Union[list[str], Callable[[list[str]], bool]]]:
|
||||
return {
|
||||
'authorization': ['Bearer abcdef'],
|
||||
'content-type': ['application/json'],
|
||||
'x-hasura-role': ['user'],
|
||||
'x-hasura-test': ['abcd'],
|
||||
'x-hasura-user-id': ['abcd1234'],
|
||||
'x-forwarded-host': lambda headers: all(header in hge_urls for header in headers) if hge_urls else True,
|
||||
'x-forwarded-user-agent': lambda headers: all(header.startswith('python-requests/') for header in headers),
|
||||
}
|
||||
|
||||
def header_test_expected_headers_str(hge_urls: Optional[list[str]]):
|
||||
expected_headers = header_test_expected_headers(hge_urls)
|
||||
return '\n'.join(f'{name}: {value}' for name, value in {
|
||||
'authorization': repr(expected_headers['authorization']),
|
||||
'content-type': repr(expected_headers['content-type']),
|
||||
'x-hasura-role': repr(expected_headers['x-hasura-role']),
|
||||
'x-hasura-test': repr(expected_headers['x-hasura-test']),
|
||||
'x-hasura-user-id': repr(expected_headers['x-hasura-user-id']),
|
||||
'x-forwarded-host': f'one of {hge_urls!r}' if hge_urls else 'ignored',
|
||||
'x-forwarded-user-agent': 'starts with "python-requests"',
|
||||
}.items())
|
||||
|
||||
class HeaderTest(graphene.ObjectType):
|
||||
wassup = graphene.String(arg=graphene.String(default_value='world'))
|
||||
|
||||
def resolve_wassup(self, info, arg):
|
||||
headers = info.context
|
||||
hosts = list(map(lambda o: urlparse(o).netloc, HGE_URLS))
|
||||
if not (headers.get_all('x-hasura-test') == ['abcd'] and
|
||||
headers.get_all('x-hasura-role') == ['user'] and
|
||||
headers.get_all('x-hasura-user-id') == ['abcd1234'] and
|
||||
headers.get_all('content-type') == ['application/json'] and
|
||||
headers.get_all('Authorization') == ['Bearer abcdef'] and
|
||||
len(headers.get_all('x-forwarded-host')) == 1 and
|
||||
all(host in headers.get_all('x-forwarded-host') for host in hosts) and
|
||||
headers.get_all('x-forwarded-user-agent')[0].startswith('python-requests')):
|
||||
raise Exception('headers dont match. Received: ' + str(headers))
|
||||
hge_urls = info.context.context.hge_urls
|
||||
actual_headers = info.context.headers
|
||||
for header_name, expected_header_values in header_test_expected_headers(hge_urls).items():
|
||||
actual_header_values: list[str] = actual_headers.get_all(header_name)
|
||||
if callable(expected_header_values):
|
||||
if not expected_header_values(actual_header_values):
|
||||
raise Exception(f'Header {header_name} doesn\'t match.\n\nActual headers:\n{actual_headers}Expected headers:\n{header_test_expected_headers_str(hge_urls)}')
|
||||
elif expected_header_values != actual_header_values:
|
||||
raise Exception(f'Header {header_name} doesn\'t match: {expected_header_values} != {actual_header_values}\n\nActual headers:\n{actual_headers}Expected headers:\n{header_test_expected_headers_str(hge_urls)}')
|
||||
|
||||
return "Hello " + arg
|
||||
|
||||
@ -776,8 +804,7 @@ class HeaderTestGraphQL(RequestHandler):
|
||||
def post(self, request):
|
||||
if not request.json:
|
||||
return Response(HTTPStatus.BAD_REQUEST)
|
||||
res = header_test_schema.execute(request.json['query'],
|
||||
context=request.headers)
|
||||
res = header_test_schema.execute(request.json['query'], context=request)
|
||||
return mkJSONResp(res)
|
||||
|
||||
|
||||
@ -849,61 +876,74 @@ class JsonScalarGraphQL(RequestHandler):
|
||||
res = json_schema.execute(request.json['query'])
|
||||
return mkJSONResp(res)
|
||||
|
||||
handlers = MkHandlers({
|
||||
'/hello': HelloWorldHandler,
|
||||
'/hello-graphql': HelloGraphQL,
|
||||
'/hello-echo-request-graphql': HelloGraphQLEchoRequest,
|
||||
'/hello-graphql-extensions': HelloGraphQLExtensions,
|
||||
'/user-graphql': UserGraphQL,
|
||||
'/country-graphql': CountryGraphQL,
|
||||
'/character-iface-graphql' : CharacterInterfaceGraphQL,
|
||||
'/iface-graphql-err-empty-field-list' : InterfaceGraphQLErrEmptyFieldList,
|
||||
'/iface-graphql-err-unknown-iface' : InterfaceGraphQLErrUnknownInterface,
|
||||
'/iface-graphql-err-missing-field' : InterfaceGraphQLErrMissingField,
|
||||
'/iface-graphql-err-wrong-field-type' : InterfaceGraphQLErrWrongFieldType,
|
||||
'/iface-graphql-err-missing-arg' : InterfaceGraphQLErrMissingArg,
|
||||
'/iface-graphql-err-wrong-arg-type' : InterfaceGraphQLErrWrongArgType,
|
||||
'/iface-graphql-err-extra-non-null-arg' : InterfaceGraphQLErrExtraNonNullArg,
|
||||
'/union-graphql' : UnionGraphQL,
|
||||
'/union-graphql-err-unknown-types' : UnionGraphQLSchemaErrUnknownTypes,
|
||||
'/union-graphql-err-subtype-iface' : UnionGraphQLSchemaErrSubTypeInterface,
|
||||
'/union-graphql-err-no-member-types' : UnionGraphQLSchemaErrNoMemberTypes,
|
||||
'/union-graphql-err-wrapped-type' : UnionGraphQLSchemaErrWrappedType,
|
||||
'/default-value-echo-graphql' : EchoGraphQL,
|
||||
'/person-graphql': PersonGraphQL,
|
||||
'/header-graphql': HeaderTestGraphQL,
|
||||
'/messages-graphql' : MessagesGraphQL,
|
||||
'/auth-graphql': SampleAuthGraphQL,
|
||||
'/json-scalar-graphql': JsonScalarGraphQL,
|
||||
'/big': BigGraphQL
|
||||
})
|
||||
def handlers(context):
|
||||
return MkHandlers({
|
||||
'/hello': HelloWorldHandler,
|
||||
'/hello-graphql': HelloGraphQL,
|
||||
'/hello-echo-request-graphql': HelloGraphQLEchoRequest,
|
||||
'/hello-graphql-extensions': HelloGraphQLExtensions,
|
||||
'/user-graphql': UserGraphQL,
|
||||
'/country-graphql': CountryGraphQL,
|
||||
'/character-iface-graphql' : CharacterInterfaceGraphQL,
|
||||
'/iface-graphql-err-empty-field-list' : InterfaceGraphQLErrEmptyFieldList,
|
||||
'/iface-graphql-err-unknown-iface' : InterfaceGraphQLErrUnknownInterface,
|
||||
'/iface-graphql-err-missing-field' : InterfaceGraphQLErrMissingField,
|
||||
'/iface-graphql-err-wrong-field-type' : InterfaceGraphQLErrWrongFieldType,
|
||||
'/iface-graphql-err-missing-arg' : InterfaceGraphQLErrMissingArg,
|
||||
'/iface-graphql-err-wrong-arg-type' : InterfaceGraphQLErrWrongArgType,
|
||||
'/iface-graphql-err-extra-non-null-arg' : InterfaceGraphQLErrExtraNonNullArg,
|
||||
'/union-graphql' : UnionGraphQL,
|
||||
'/union-graphql-err-unknown-types' : UnionGraphQLSchemaErrUnknownTypes,
|
||||
'/union-graphql-err-subtype-iface' : UnionGraphQLSchemaErrSubTypeInterface,
|
||||
'/union-graphql-err-no-member-types' : UnionGraphQLSchemaErrNoMemberTypes,
|
||||
'/union-graphql-err-wrapped-type' : UnionGraphQLSchemaErrWrappedType,
|
||||
'/default-value-echo-graphql' : EchoGraphQL,
|
||||
'/person-graphql': PersonGraphQL,
|
||||
'/header-graphql': HeaderTestGraphQL,
|
||||
'/messages-graphql' : MessagesGraphQL,
|
||||
'/auth-graphql': SampleAuthGraphQL,
|
||||
'/json-scalar-graphql': JsonScalarGraphQL,
|
||||
'/big': BigGraphQL
|
||||
}, context)
|
||||
|
||||
|
||||
def create_server(host='localhost', port=0):
|
||||
return http.server.HTTPServer((host, port), handlers)
|
||||
def create_server(
|
||||
host: str = 'localhost',
|
||||
port: int = 0,
|
||||
tls_configuration: Optional[Tuple[str, str]] = None,
|
||||
hge_urls: Optional[list[str]] = None,
|
||||
):
|
||||
context = Context([urlparse(url).netloc for url in hge_urls] if hge_urls else None)
|
||||
server = http.server.HTTPServer((host, port), handlers(context))
|
||||
if tls_configuration:
|
||||
server.socket = ssl.wrap_socket(
|
||||
server.socket,
|
||||
certfile=tls_configuration[0],
|
||||
keyfile=tls_configuration[1],
|
||||
server_side=True,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23,
|
||||
)
|
||||
return server
|
||||
|
||||
def stop_server(server):
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
def set_hge_urls(hge_urls = []):
|
||||
global HGE_URLS
|
||||
HGE_URLS=hge_urls
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = None
|
||||
certfile = None
|
||||
s = None
|
||||
port = DEFAULT_PORT
|
||||
if len(sys.argv) >= 2:
|
||||
port = int(sys.argv[1])
|
||||
server = create_server(port = port)
|
||||
|
||||
if len(sys.argv) == 4: # usage - python3 graphql-server.py <port> <certfile> <keyfile>
|
||||
port_ = int(sys.argv[1])
|
||||
certfile_ = sys.argv[2]
|
||||
keyfile_ = sys.argv[3]
|
||||
s = create_server(port = port_)
|
||||
s.socket = ssl.wrap_socket( s.socket,
|
||||
certfile=certfile_,
|
||||
keyfile=keyfile_,
|
||||
server_side=True,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23)
|
||||
else:
|
||||
s = create_server()
|
||||
s.serve_forever()
|
||||
certfile = sys.argv[2]
|
||||
keyfile = sys.argv[3]
|
||||
server.socket = ssl.wrap_socket(
|
||||
server.socket,
|
||||
certfile=certfile,
|
||||
keyfile=keyfile,
|
||||
server_side=True,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23,
|
||||
)
|
||||
|
||||
server.serve_forever()
|
||||
|
@ -95,7 +95,7 @@ handlers = MkHandlers({
|
||||
'/reset-state': ResetStateHandler
|
||||
})
|
||||
|
||||
def create_server(host='127.0.0.1', port=5001):
|
||||
def create_server(host, port):
|
||||
return http.server.HTTPServer((host, port), handlers)
|
||||
|
||||
def stop_server(server):
|
||||
@ -104,6 +104,6 @@ def stop_server(server):
|
||||
|
||||
# if you want to run this module to emulate a JWK server during development
|
||||
if __name__ == '__main__':
|
||||
s = create_server(port=5001)
|
||||
s = create_server(host='localhost', port=5001)
|
||||
s.serve_forever()
|
||||
stop_server(s)
|
||||
|
@ -8,5 +8,6 @@ markers =
|
||||
backend: The backends supported by the test case
|
||||
admin_secret: Generate and use an admin secret
|
||||
hge_env: Pass additional environment variables to the GraphQL Engine
|
||||
jwk_path: When running a JWK server, the URL path that HGE should use
|
||||
skip_server_upgrade_test: Tests with this marker should not be run as part of server upgrade test
|
||||
allow_server_upgrade_test: Add tests with this marker to server upgrade test, as far as they don't have the skip_server_upgarde_test market
|
||||
|
@ -39,7 +39,7 @@ docker compose pull citus mssql postgres
|
||||
|
||||
echo
|
||||
echo '*** Starting databases ***'
|
||||
docker compose rm -svf citus mssql postgres # tear down databases beforehand
|
||||
docker compose rm -svf citus mssql mssql-healthcheck postgres # tear down databases beforehand
|
||||
docker compose up -d --wait citus mssql-healthcheck postgres
|
||||
|
||||
HASURA_GRAPHQL_CITUS_SOURCE_URL="postgresql://postgres:hasura@localhost:$(docker compose port citus 5432 | sed -E 's/.*://')/postgres"
|
||||
|
@ -69,6 +69,6 @@ for SERVER_TEST_TO_RUN in "${SERVER_TESTS_TO_RUN[@]}"; do
|
||||
export SERVER_TEST_TO_RUN
|
||||
echo
|
||||
echo "*** Running test suite: ${SERVER_TEST_TO_RUN} ***"
|
||||
docker compose rm -svf citus mssql postgres # tear down databases beforehand
|
||||
docker compose rm -svf citus mssql mssql-healthcheck postgres # tear down databases beforehand
|
||||
docker compose run --rm tests-py
|
||||
done
|
||||
|
@ -1,43 +1,93 @@
|
||||
from time import sleep, perf_counter
|
||||
import requests
|
||||
import os
|
||||
import pytest
|
||||
from context import PytestConf
|
||||
import requests
|
||||
import threading
|
||||
from time import perf_counter, sleep
|
||||
|
||||
if not PytestConf.config.getoption("--test-jwk-url"):
|
||||
pytest.skip("--test-jwk-url flag is missing, skipping tests", allow_module_level=True)
|
||||
import jwk_server
|
||||
import ports
|
||||
|
||||
# assumes the JWK server is running on 127.0.0.1:5001
|
||||
pytestmark = [
|
||||
pytest.mark.admin_secret
|
||||
]
|
||||
|
||||
def wait_until_request_count_reaches(num_requests, state_key, max_wait_secs):
|
||||
@pytest.fixture(scope='class')
|
||||
@pytest.mark.early
|
||||
def jwk_server_url(request: pytest.FixtureRequest, hge_fixture_env: dict[str, str]):
|
||||
path_marker = request.node.get_closest_marker('jwk_path')
|
||||
assert path_marker is not None, 'The test must set the `jwk_path` marker.'
|
||||
path: str = path_marker.args[0]
|
||||
|
||||
# If the JWK server was started outside, just set the environment variable
|
||||
# so that the test is skipped if the value is wrong.
|
||||
env_var = os.getenv('JWK_SERVER_URL')
|
||||
if env_var:
|
||||
hge_fixture_env['HASURA_GRAPHQL_JWT_SECRET'] = '{"jwk_url": "' + env_var + path + '"}'
|
||||
return env_var
|
||||
|
||||
port = 5001
|
||||
server = jwk_server.create_server('localhost', port)
|
||||
thread = threading.Thread(target=server.serve_forever)
|
||||
thread.start()
|
||||
request.addfinalizer(server.shutdown)
|
||||
port = server.server_address[1]
|
||||
ports.wait_for_port(port)
|
||||
url = f'http://localhost:{port}'
|
||||
hge_fixture_env['HASURA_GRAPHQL_JWT_SECRET'] = '{"jwk_url": "' + url + path + '"}'
|
||||
return url
|
||||
|
||||
def wait_until_request_count_reaches(num_requests: int, state_key: str, timeout_secs: int, jwk_server_url: str) -> float:
|
||||
start_time = perf_counter()
|
||||
requests.post('http://localhost:5001/reset-state')
|
||||
requests.post(jwk_server_url + '/reset-state')
|
||||
request_count = 0
|
||||
time_elapsed = 0
|
||||
|
||||
while request_count < num_requests:
|
||||
time_elapsed = perf_counter() - start_time
|
||||
if time_elapsed > max_wait_secs:
|
||||
if time_elapsed > timeout_secs:
|
||||
raise Exception(f'Waited {time_elapsed} seconds for {state_key} JWK requests to reach {num_requests}. Only received {request_count}.')
|
||||
|
||||
sleep(0.2)
|
||||
state = requests.get('http://localhost:5001/state').json()
|
||||
state = requests.get(jwk_server_url + '/state').json()
|
||||
request_count = state[state_key]
|
||||
|
||||
return time_elapsed
|
||||
|
||||
def test_cache_control_header_max_age(hge_ctx):
|
||||
# The test uses max-age=3, so we are expecting one request (timing out after 6 seconds)
|
||||
time_elapsed = wait_until_request_count_reaches(1, 'cache-control', 6)
|
||||
@pytest.mark.jwk_path('/jwk-cache-control?max-age=3')
|
||||
def test_cache_control_header_max_age(jwk_server_url: str):
|
||||
# The test uses max-age=3, so we are expecting one request
|
||||
time_elapsed = wait_until_request_count_reaches(num_requests=1, state_key='cache-control', timeout_secs=6, jwk_server_url=jwk_server_url)
|
||||
print(f"time_elapsed: {time_elapsed}")
|
||||
|
||||
def test_cache_control_header_no_caching(hge_ctx):
|
||||
@pytest.mark.jwk_path('/jwk-cache-control?max-age=3&must-revalidate=true')
|
||||
def test_cache_control_header_max_age_must_revalidate(jwk_server_url: str):
|
||||
# The test uses max-age=3, so we are expecting one request
|
||||
time_elapsed = wait_until_request_count_reaches(num_requests=1, state_key='cache-control', timeout_secs=6, jwk_server_url=jwk_server_url)
|
||||
print(f"time_elapsed: {time_elapsed}")
|
||||
|
||||
@pytest.mark.jwk_path('/jwk-cache-control?must-revalidate=true')
|
||||
def test_cache_control_header_must_revalidate(jwk_server_url: str):
|
||||
# HGE should refresh the JWK once a second, so we are expecting three requests in at least two seconds
|
||||
# (timing out after 10 seconds)
|
||||
time_elapsed = wait_until_request_count_reaches(3, 'cache-control', 10)
|
||||
time_elapsed = wait_until_request_count_reaches(num_requests=3, state_key='cache-control', timeout_secs=10, jwk_server_url=jwk_server_url)
|
||||
print(f"time_elapsed: {time_elapsed}")
|
||||
assert(time_elapsed >= 2)
|
||||
|
||||
def test_expires_header(hge_ctx):
|
||||
# The test uses a three second jwk expiry so we are expecting one request (timing out after 6 seconds)
|
||||
time_elapsed = wait_until_request_count_reaches(1, 'expires', 6)
|
||||
@pytest.mark.jwk_path('/jwk-cache-control?no-cache=true&public=true')
|
||||
def test_cache_control_header_no_cache_public(jwk_server_url: str):
|
||||
# HGE should refresh the JWK once a second, so we are expecting three requests in at least two seconds
|
||||
time_elapsed = wait_until_request_count_reaches(num_requests=3, state_key='cache-control', timeout_secs=10, jwk_server_url=jwk_server_url)
|
||||
print(f"time_elapsed: {time_elapsed}")
|
||||
assert(time_elapsed >= 2)
|
||||
|
||||
@pytest.mark.jwk_path('/jwk-cache-control?no-store=true&max-age=3')
|
||||
def test_cache_control_header_no_store_max_age(jwk_server_url: str):
|
||||
# HGE should refresh the JWK once a second, so we are expecting three requests in at least two seconds
|
||||
time_elapsed = wait_until_request_count_reaches(num_requests=3, state_key='cache-control', timeout_secs=10, jwk_server_url=jwk_server_url)
|
||||
print(f"time_elapsed: {time_elapsed}")
|
||||
assert(time_elapsed >= 2)
|
||||
|
||||
@pytest.mark.jwk_path('/jwk-expires?seconds=3')
|
||||
def test_expires_header(jwk_server_url: str):
|
||||
# The test uses a three second jwk expiry so we are expecting one request
|
||||
time_elapsed = wait_until_request_count_reaches(num_requests=1, state_key='expires', timeout_secs=6, jwk_server_url=jwk_server_url)
|
||||
print(f"time_elapsed: {time_elapsed}")
|
||||
|
@ -1,20 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import graphql
|
||||
import json
|
||||
import requests
|
||||
from ruamel.yaml import YAML
|
||||
import pytest
|
||||
import graphql
|
||||
import requests
|
||||
import time
|
||||
|
||||
import graphql_server
|
||||
from validate import check_query_f, check_query
|
||||
import pytest
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.usefixtures('gql_server')
|
||||
pytest.mark.usefixtures('gql_server'),
|
||||
]
|
||||
|
||||
yaml = YAML(typ='safe', pure=True)
|
||||
yaml=YAML(typ='safe', pure=True)
|
||||
|
||||
from validate import check_query_f, check_query
|
||||
|
||||
def mk_add_remote_q(name, url, headers=None, client_hdrs=False, timeout=None, customization=None):
|
||||
return {
|
||||
@ -76,12 +75,12 @@ class TestRemoteSchemaBasic:
|
||||
dir = 'queries/remote_schemas'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
# This is needed for supporting server upgrade tests
|
||||
# Some marked tests in this class will be run as server upgrade tests
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
q = mk_add_remote_q('simple 1', 'http://localhost:5000/hello-graphql')
|
||||
q = mk_add_remote_q('simple 1', f'{gql_server.url}/hello-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -92,9 +91,9 @@ class TestRemoteSchemaBasic:
|
||||
resp = hge_ctx.v1q(export_metadata_q)
|
||||
assert resp['remote_schemas'][0]['name'] == "simple 1"
|
||||
|
||||
def test_update_schema_with_no_url_change(self, hge_ctx):
|
||||
def test_update_schema_with_no_url_change(self, hge_ctx, gql_server):
|
||||
""" call update_remote_schema API and check the details stored in metadata """
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, True, 120)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, True, 120)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
resp = hge_ctx.v1q(export_metadata_q)
|
||||
@ -103,35 +102,35 @@ class TestRemoteSchemaBasic:
|
||||
assert resp['remote_schemas'][0]['definition']['forward_client_headers'] == True
|
||||
|
||||
""" revert to original config for remote schema """
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, False, 60)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, False, 60)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
def test_update_schema_with_url_change(self, hge_ctx):
|
||||
def test_update_schema_with_url_change(self, hge_ctx, gql_server):
|
||||
""" call update_remote_schema API and check the details stored in metadata """
|
||||
# This should succeed since there isn't any conflicting relations or permissions set up
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/user-graphql', None, True, 80)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/user-graphql', None, True, 80)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
resp = hge_ctx.v1q(export_metadata_q)
|
||||
assert resp['remote_schemas'][0]['name'] == "simple 1"
|
||||
assert resp['remote_schemas'][0]['definition']['url'] == 'http://localhost:5000/user-graphql'
|
||||
assert resp['remote_schemas'][0]['definition']['url'] == f'{gql_server.url}/user-graphql'
|
||||
assert resp['remote_schemas'][0]['definition']['timeout_seconds'] == 80
|
||||
assert resp['remote_schemas'][0]['definition']['forward_client_headers'] == True
|
||||
|
||||
""" revert to original config for remote schema """
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, False, 60)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, False, 60)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
def test_update_schema_with_customization_change(self, hge_ctx):
|
||||
def test_update_schema_with_customization_change(self, hge_ctx, gql_server):
|
||||
""" call update_remote_schema API and check the details stored in metadata """
|
||||
# This should succeed since there isn't any conflicting relations or permissions set up
|
||||
customization = {'type_names': { 'prefix': 'Foo', 'mapping': {'String': 'MyString'}}, 'field_names': [{'parent_type': 'Hello', 'prefix': 'my_', 'mapping': {}}]}
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, False, 60, customization=customization)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, False, 60, customization=customization)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
resp = hge_ctx.v1q(export_metadata_q)
|
||||
assert resp['remote_schemas'][0]['name'] == "simple 1"
|
||||
assert resp['remote_schemas'][0]['definition']['url'] == 'http://localhost:5000/hello-graphql'
|
||||
assert resp['remote_schemas'][0]['definition']['url'] == f'{gql_server.url}/hello-graphql'
|
||||
assert resp['remote_schemas'][0]['definition']['timeout_seconds'] == 60
|
||||
assert resp['remote_schemas'][0]['definition']['customization'] == customization
|
||||
|
||||
@ -143,21 +142,21 @@ class TestRemoteSchemaBasic:
|
||||
check_query_f(hge_ctx, self.dir + '/basic_query_customized.yaml')
|
||||
|
||||
""" revert to original config for remote schema """
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, False, 60)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, False, 60)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
resp = hge_ctx.v1q(export_metadata_q)
|
||||
assert 'customization' not in resp['remote_schemas'][0]['definition']
|
||||
|
||||
def test_update_schema_with_customization_change_invalid(self, hge_ctx):
|
||||
def test_update_schema_with_customization_change_invalid(self, hge_ctx, gql_server):
|
||||
""" call update_remote_schema API and check the details stored in metadata """
|
||||
customization = {'type_names': { 'mapping': {'String': 'Foo', 'Hello': 'Foo'} } }
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, False, 60, customization=customization)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, False, 60, customization=customization)
|
||||
resp = hge_ctx.v1q(q, expected_status_code = 400)
|
||||
assert resp['error'] == 'Inconsistent object: Type name mappings are not distinct; the following types appear more than once: "Foo"'
|
||||
|
||||
""" revert to original config for remote schema """
|
||||
q = mk_update_remote_q('simple 1', 'http://localhost:5000/hello-graphql', None, False, 60)
|
||||
q = mk_update_remote_q('simple 1', f'{gql_server.url}/hello-graphql', None, False, 60)
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
@ -179,9 +178,9 @@ class TestRemoteSchemaBasic:
|
||||
def test_remote_subscription(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir + '/basic_subscription_not_supported.yaml')
|
||||
|
||||
def test_add_schema_conflicts(self, hge_ctx):
|
||||
def test_add_schema_conflicts(self, hge_ctx, gql_server):
|
||||
"""add 2 remote schemas with same node or types"""
|
||||
q = mk_add_remote_q('simple 2', 'http://localhost:5000/hello-graphql')
|
||||
q = mk_add_remote_q('simple 2', f'{gql_server.url}/hello-graphql')
|
||||
resp = hge_ctx.v1q(q, expected_status_code = 400)
|
||||
assert resp['code'] == 'unexpected'
|
||||
|
||||
@ -199,23 +198,23 @@ class TestRemoteSchemaBasic:
|
||||
hge_ctx.v1q(q)
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_add_second_remote_schema(self, hge_ctx):
|
||||
def test_add_second_remote_schema(self, hge_ctx, gql_server):
|
||||
"""add 2 remote schemas with different node and types"""
|
||||
q = mk_add_remote_q('my remote', 'http://localhost:5000/user-graphql')
|
||||
q = mk_add_remote_q('my remote', f'{gql_server.url}/user-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
hge_ctx.v1q(mk_delete_remote_q('my remote'))
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_json_scalar_dict(self, hge_ctx):
|
||||
q = mk_add_remote_q('my remote', 'http://localhost:5000/json-scalar-graphql')
|
||||
def test_json_scalar_dict(self, hge_ctx, gql_server):
|
||||
q = mk_add_remote_q('my remote', f'{gql_server.url}/json-scalar-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
check_query_f(hge_ctx, self.dir + '/json_scalar.yaml')
|
||||
hge_ctx.v1q(mk_delete_remote_q('my remote'))
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_add_remote_schema_with_interfaces(self, hge_ctx):
|
||||
def test_add_remote_schema_with_interfaces(self, hge_ctx, gql_server):
|
||||
"""add a remote schema with interfaces in it"""
|
||||
q = mk_add_remote_q('my remote interface one', 'http://localhost:5000/character-iface-graphql')
|
||||
q = mk_add_remote_q('my remote interface one', f'{gql_server.url}/character-iface-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
check_query_f(hge_ctx, self.dir + '/character_interface_query.yaml')
|
||||
hge_ctx.v1q(mk_delete_remote_q('my remote interface one'))
|
||||
@ -256,9 +255,9 @@ class TestRemoteSchemaBasic:
|
||||
check_query_f(hge_ctx, self.dir + '/add_remote_schema_with_iface_err_extra_non_null_arg.yaml')
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_add_remote_schema_with_union(self, hge_ctx):
|
||||
def test_add_remote_schema_with_union(self, hge_ctx, gql_server):
|
||||
"""add a remote schema with union in it"""
|
||||
q = mk_add_remote_q('my remote union one', 'http://localhost:5000/union-graphql')
|
||||
q = mk_add_remote_q('my remote union one', f'{gql_server.url}/union-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
check_query_f(hge_ctx, self.dir + '/search_union_type_query.yaml')
|
||||
hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "my remote union one"}})
|
||||
@ -289,12 +288,12 @@ class TestRemoteSchemaBasicExtensions:
|
||||
dir = 'queries/remote_schemas'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
# This is needed for supporting server upgrade tests
|
||||
# Some marked tests in this class will be run as server upgrade tests
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
q = mk_add_remote_q('simple 1', 'http://localhost:5000/hello-graphql-extensions')
|
||||
q = mk_add_remote_q('simple 1', f'{gql_server.url}/hello-graphql-extensions')
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -321,16 +320,16 @@ class TestAddRemoteSchemaTbls:
|
||||
resp = hge_ctx.v1q(export_metadata_q)
|
||||
assert resp['remote_schemas'][0]['name'] == "simple2-graphql"
|
||||
|
||||
def test_add_schema_conflicts_with_tables(self, hge_ctx):
|
||||
def test_add_schema_conflicts_with_tables(self, hge_ctx, gql_server):
|
||||
"""add remote schema which conflicts with hasura tables"""
|
||||
q = mk_add_remote_q('simple2', 'http://localhost:5000/hello-graphql')
|
||||
q = mk_add_remote_q('simple2', f'{gql_server.url}/hello-graphql')
|
||||
resp = hge_ctx.v1q(q, expected_status_code = 400)
|
||||
assert resp['code'] == 'invalid-configuration'
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_add_second_remote_schema(self, hge_ctx):
|
||||
def test_add_second_remote_schema(self, hge_ctx, gql_server):
|
||||
"""add 2 remote schemas with different node and types"""
|
||||
q = mk_add_remote_q('my remote2', 'http://localhost:5000/country-graphql')
|
||||
q = mk_add_remote_q('my remote2', f'{gql_server.url}/country-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "my remote2"}})
|
||||
|
||||
@ -353,37 +352,35 @@ class TestAddRemoteSchemaTbls:
|
||||
resp, _ = check_query(hge_ctx, query)
|
||||
assert check_introspection_result(resp, ['User', 'hello'], ['user', 'hello'])
|
||||
|
||||
def test_add_schema_duplicate_name(self, hge_ctx):
|
||||
q = mk_add_remote_q('simple2-graphql', 'http://localhost:5000/country-graphql')
|
||||
def test_add_schema_duplicate_name(self, hge_ctx, gql_server):
|
||||
q = mk_add_remote_q('simple2-graphql', f'{gql_server.url}/country-graphql')
|
||||
resp = hge_ctx.v1q(q, expected_status_code = 400)
|
||||
assert resp['code'] == 'already-exists'
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_add_schema_same_type_containing_same_scalar(self, hge_ctx):
|
||||
def test_add_schema_same_type_containing_same_scalar(self, hge_ctx, gql_server):
|
||||
"""
|
||||
test types get merged when remote schema has type with same name and
|
||||
same structure + a same custom scalar
|
||||
"""
|
||||
hge_ctx.v1q_f(self.dir + '/person_table.yaml')
|
||||
q = mk_add_remote_q('person-graphql', 'http://localhost:5000/person-graphql')
|
||||
q = mk_add_remote_q('person-graphql', f'{gql_server.url}/person-graphql')
|
||||
|
||||
hge_ctx.v1q(q)
|
||||
hge_ctx.v1q_f(self.dir + '/drop_person_table.yaml')
|
||||
hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "person-graphql"}})
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_remote_schema_forward_headers(self, hge_ctx):
|
||||
def test_remote_schema_forward_headers(self, hge_ctx, gql_server):
|
||||
"""
|
||||
test headers from client and conf and resolved info gets passed
|
||||
correctly to remote schema, and no duplicates are sent. this test just
|
||||
tests if the remote schema returns success or not. checking of header
|
||||
duplicate logic is in the remote schema server
|
||||
"""
|
||||
graphql_server.set_hge_urls([])
|
||||
|
||||
conf_hdrs = [{'name': 'x-hasura-test', 'value': 'abcd'}]
|
||||
add_remote = mk_add_remote_q('header-graphql',
|
||||
'http://localhost:5000/header-graphql',
|
||||
f'{gql_server.url}/header-graphql',
|
||||
headers=conf_hdrs, client_hdrs=True)
|
||||
hge_ctx.v1q(add_remote)
|
||||
q = {'query': '{ wassup }'}
|
||||
@ -402,8 +399,8 @@ class TestAddRemoteSchemaTbls:
|
||||
print(resp.status_code, resp.json())
|
||||
assert resp.status_code == 200
|
||||
res = resp.json()
|
||||
assert 'data' in res and res['data'], f'Response: {json.dumps(res)}'
|
||||
assert res['data']['wassup'] == 'Hello world', f'Response: {json.dumps(res)}'
|
||||
assert 'data' in res and res['data'] is not None, res
|
||||
assert res['data']['wassup'] == 'Hello world'
|
||||
|
||||
hge_ctx.v1q({'type': 'remove_remote_schema', 'args': {'name': 'header-graphql'}})
|
||||
|
||||
@ -493,8 +490,8 @@ class TestRemoteSchemaResponseHeaders():
|
||||
dir = 'queries/remote_schemas'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, hge_ctx):
|
||||
q = mk_add_remote_q('sample-auth', 'http://localhost:5000/auth-graphql')
|
||||
def transact(self, hge_ctx, gql_server):
|
||||
q = mk_add_remote_q('sample-auth', f'{gql_server.url}/auth-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
hge_ctx.v1q(self.teardown)
|
||||
@ -516,23 +513,20 @@ class TestRemoteSchemaResponseHeaders():
|
||||
|
||||
class TestAddRemoteSchemaCompareRootQueryFields:
|
||||
|
||||
remote = 'http://localhost:5000/default-value-echo-graphql'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, hge_ctx):
|
||||
hge_ctx.v1q(mk_add_remote_q('default_value_test', self.remote))
|
||||
def transact(self, hge_ctx, gql_server):
|
||||
remote = f'{gql_server.url}/default-value-echo-graphql'
|
||||
hge_ctx.v1q(mk_add_remote_q('default_value_test', remote))
|
||||
yield
|
||||
hge_ctx.v1q(mk_delete_remote_q('default_value_test'))
|
||||
|
||||
@pytest.mark.allow_server_upgrade_test
|
||||
def test_schema_check_arg_default_values_and_field_and_arg_types(self, hge_ctx):
|
||||
def test_schema_check_arg_default_values_and_field_and_arg_types(self, hge_ctx, gql_server):
|
||||
remote = f'{gql_server.url}/default-value-echo-graphql'
|
||||
with open('queries/graphql_introspection/introspection.yaml') as f:
|
||||
query = yaml.load(f)
|
||||
introspect_hasura, _ = check_query(hge_ctx, query)
|
||||
resp = requests.post(
|
||||
self.remote,
|
||||
json=query['query']
|
||||
)
|
||||
resp = requests.post(remote, json=query['query'])
|
||||
introspect_remote = resp.json()
|
||||
assert resp.status_code == 200, introspect_remote
|
||||
remote_root_ty_info = get_query_root_info(introspect_remote)
|
||||
@ -672,7 +666,7 @@ class TestRemoteSchemaReload:
|
||||
|
||||
def test_inconsistent_remote_schema_reload_metadata(self, gql_server, hge_ctx):
|
||||
# Add remote schema
|
||||
hge_ctx.v1q(mk_add_remote_q('simple 1', 'http://localhost:5000/hello-graphql'))
|
||||
hge_ctx.v1q(mk_add_remote_q('simple 1', f'{gql_server.url}/hello-graphql'))
|
||||
# stop remote graphql server
|
||||
gql_server.stop_server()
|
||||
# Reload metadata with remote schemas
|
||||
@ -714,12 +708,12 @@ class TestRemoteSchemaTypePrefix:
|
||||
dir = 'queries/remote_schemas'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
# This is needed for supporting server upgrade tests
|
||||
# Some marked tests in this class will be run as server upgrade tests
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
q = mk_add_remote_q('simple 2', 'http://localhost:5000/user-graphql', customization=type_prefix_customization("Foo"))
|
||||
q = mk_add_remote_q('simple 2', f'{gql_server.url}/user-graphql', customization=type_prefix_customization("Foo"))
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -744,10 +738,10 @@ class TestValidateRemoteSchemaTypePrefixQuery:
|
||||
teardown = {"type": "clear_metadata", "args": {}}
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
q = mk_add_remote_q('character-foo', 'http://localhost:5000/character-iface-graphql', customization=type_prefix_customization("Foo"))
|
||||
q = mk_add_remote_q('character-foo', f'{gql_server.url}/character-iface-graphql', customization=type_prefix_customization("Foo"))
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -765,11 +759,11 @@ class TestValidateRemoteSchemaFieldPrefixQuery:
|
||||
teardown = {"type": "clear_metadata", "args": {}}
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
customization = { "field_names": [{"parent_type": "Character", "prefix": "foo_"},{"parent_type": "Human", "prefix": "foo_"},{"parent_type": "Droid", "prefix": "foo_"}] }
|
||||
q = mk_add_remote_q('character-foo', 'http://localhost:5000/character-iface-graphql', customization=customization)
|
||||
q = mk_add_remote_q('character-foo', f'{gql_server.url}/character-iface-graphql', customization=customization)
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -795,11 +789,11 @@ class TestValidateRemoteSchemaNamespaceQuery:
|
||||
teardown = {"type": "clear_metadata", "args": {}}
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
customization = { "root_fields_namespace": "foo" }
|
||||
q = mk_add_remote_q('character-foo', 'http://localhost:5000/character-iface-graphql', customization=customization)
|
||||
q = mk_add_remote_q('character-foo', f'{gql_server.url}/character-iface-graphql', customization=customization)
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -820,7 +814,7 @@ class TestValidateRemoteSchemaCustomizeAllTheThings:
|
||||
teardown = {"type": "clear_metadata", "args": {}}
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, request, hge_ctx):
|
||||
def transact(self, request, hge_ctx, gql_server):
|
||||
config = request.config
|
||||
if not config.getoption('--skip-schema-setup'):
|
||||
customization = {
|
||||
@ -833,7 +827,7 @@ class TestValidateRemoteSchemaCustomizeAllTheThings:
|
||||
{"parent_type": "CharacterIFaceQuery", "prefix": "super_" }
|
||||
]
|
||||
}
|
||||
q = mk_add_remote_q('character-foo', 'http://localhost:5000/character-iface-graphql', customization=customization)
|
||||
q = mk_add_remote_q('character-foo', f'{gql_server.url}/character-iface-graphql', customization=customization)
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
if request.session.testsfailed > 0 or not config.getoption('--skip-schema-teardown'):
|
||||
@ -851,8 +845,8 @@ class TestRemoteSchemaRequestPayload:
|
||||
teardown = {"type": "clear_metadata", "args": {}}
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, hge_ctx):
|
||||
q = mk_add_remote_q('echo request', 'http://localhost:5000/hello-echo-request-graphql')
|
||||
def transact(self, hge_ctx, gql_server):
|
||||
q = mk_add_remote_q('echo request', f'{gql_server.url}/hello-echo-request-graphql')
|
||||
hge_ctx.v1q(q)
|
||||
yield
|
||||
hge_ctx.v1q(self.teardown)
|
||||
|
@ -33,12 +33,13 @@ class Response():
|
||||
|
||||
class Request():
|
||||
""" Represents a HTTP `Request` object """
|
||||
def __init__(self, path, qs=None, body=None, json=None, headers=None):
|
||||
def __init__(self, path, qs=None, body=None, json=None, headers=None, context=None):
|
||||
self.path = path
|
||||
self.qs = qs
|
||||
self.body = body
|
||||
self.json = json
|
||||
self.headers = headers
|
||||
self.context = context
|
||||
|
||||
|
||||
class RequestHandler(ABC):
|
||||
@ -54,7 +55,7 @@ class RequestHandler(ABC):
|
||||
pass
|
||||
|
||||
|
||||
def MkHandlers(handlers):
|
||||
def MkHandlers(handlers, context = None):
|
||||
class HTTPHandler(http.BaseHTTPRequestHandler):
|
||||
def not_found(self):
|
||||
self.send_response(HTTPStatus.NOT_FOUND)
|
||||
@ -79,7 +80,7 @@ def MkHandlers(handlers):
|
||||
path = raw_path.path
|
||||
handler = handlers[path]()
|
||||
qs = parse_qs(raw_path.query)
|
||||
req = Request(path, qs, None, None, self.headers)
|
||||
req = Request(path, qs, None, None, self.headers, context)
|
||||
resp = handler.get(req)
|
||||
self.send_response(resp.status)
|
||||
if resp.headers:
|
||||
@ -100,7 +101,7 @@ def MkHandlers(handlers):
|
||||
req_json = None
|
||||
if self.headers.get('Content-Type') == 'application/json':
|
||||
req_json = json.loads(req_body)
|
||||
req = Request(self.path, qs, req_body, req_json, self.headers)
|
||||
req = Request(self.path, qs, req_body, req_json, self.headers, context)
|
||||
resp = handler.post(req)
|
||||
self.send_response(resp.status)
|
||||
if resp.headers:
|
||||
|
Loading…
Reference in New Issue
Block a user