Skip to content

Commit

Permalink
feat: ASGI support for GraphQL schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
Stranger6667 committed Apr 1, 2021
1 parent 6d77911 commit 22354a9
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 10 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Changelog

- Custom keyword arguments to ``schemathesis.graphql.from_url`` that are proxied to ``requests.post``.
- ``from_wsgi`` loader for GraphQL apps. `#1097`_
- ``from_asgi`` loader for GraphQL apps. `#1100`_
- Support for ``app`` & ``base_url`` arguments for the ``from_pytest_fixture`` runner.

**Changed**
Expand Down Expand Up @@ -1766,6 +1767,7 @@ Deprecated
.. _0.3.0: https://github.com/schemathesis/schemathesis/compare/v0.2.0...v0.3.0
.. _0.2.0: https://github.com/schemathesis/schemathesis/compare/v0.1.0...v0.2.0

.. _#1100: https://github.com/schemathesis/schemathesis/issues/1100
.. _#1097: https://github.com/schemathesis/schemathesis/issues/1097
.. _#1093: https://github.com/schemathesis/schemathesis/issues/1093
.. _#1073: https://github.com/schemathesis/schemathesis/issues/1073
Expand Down
2 changes: 1 addition & 1 deletion src/schemathesis/specs/graphql/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .loaders import from_dict, from_url, from_wsgi
from .loaders import from_asgi, from_dict, from_url, from_wsgi
26 changes: 26 additions & 0 deletions src/schemathesis/specs/graphql/loaders.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, Dict, Optional

import requests
from starlette.testclient import TestClient as ASGIClient
from werkzeug import Client
from yarl import URL

Expand Down Expand Up @@ -147,3 +148,28 @@ def from_wsgi(schema_path: str, app: Any, base_url: Optional[str] = None, **kwar
response = client.post(schema_path, **kwargs)
HTTPError.check_response(response, schema_path)
return from_dict(raw_schema=response.json["data"], location=schema_path, base_url=base_url, app=app)


def from_asgi(
schema_path: str,
app: Any,
base_url: Optional[str] = None,
**kwargs: Any,
) -> GraphQLSchema:
"""Load GraphQL schema from an ASGI app.
:param str schema_path: An in-app relative URL to the schema.
:param app: An ASGI app instance.
"""
require_relative_url(schema_path)
setup_headers(kwargs)
kwargs.setdefault("json", {"query": INTROSPECTION_QUERY})
client = ASGIClient(app)
response = client.post(schema_path, **kwargs)
HTTPError.check_response(response, schema_path)
return from_dict(
response.json()["data"],
location=schema_path,
base_url=base_url,
app=app,
)
10 changes: 10 additions & 0 deletions src/schemathesis/specs/graphql/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import attr
import graphql
import requests
from hypothesis import strategies as st
from hypothesis.strategies import SearchStrategy
from hypothesis_graphql import strategies as gql_st
Expand Down Expand Up @@ -48,6 +49,15 @@ def validate_response(
checks += additional_checks
return super().validate_response(response, checks, code_sample_style=code_sample_style)

def call_asgi(
self,
app: Any = None,
base_url: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
**kwargs: Any,
) -> requests.Response:
return super().call_asgi(app=app, base_url=base_url, headers=headers, **kwargs)


@attr.s() # pragma: no mutate
class GraphQLSchema(BaseSchema):
Expand Down
2 changes: 1 addition & 1 deletion test/apps/_graphql/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from . import _flask
from . import _fastapi, _flask
10 changes: 10 additions & 0 deletions test/apps/_graphql/_fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fastapi import FastAPI
from starlette.graphql import GraphQLApp

from ..schema import schema


def create_app(path="/graphql"):
app = FastAPI()
app.add_route(path, GraphQLApp(schema=schema))
return app
3 changes: 3 additions & 0 deletions test/apps/_graphql/_fastapi/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import create_app

app = create_app()
21 changes: 13 additions & 8 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from schemathesis.extra._flask import run_server as run_flask_server
from schemathesis.specs.openapi import loaders as oas_loaders

from .apps import _graphql
from .apps.openapi import _aiohttp, _fastapi, _flask
from .apps import _graphql as graphql
from .apps import openapi
from .apps.openapi.schema import OpenAPIVersion, Operation
from .utils import get_schema_path, make_schema

Expand Down Expand Up @@ -51,7 +51,7 @@ def pytest_configure(config):
@pytest.fixture(scope="session")
def _app():
"""A global AioHTTP application with configurable API operations."""
return _aiohttp.create_app(("success", "failure"))
return openapi._aiohttp.create_app(("success", "failure"))


@pytest.fixture
Expand All @@ -70,7 +70,7 @@ def operations(request):
@pytest.fixture
def reset_app(_app, operations):
def inner(version):
_aiohttp.reset_app(_app, operations, version)
openapi._aiohttp.reset_app(_app, operations, version)

return inner

Expand Down Expand Up @@ -160,7 +160,7 @@ def graphql_path():

@pytest.fixture(scope="session")
def graphql_app(graphql_path):
return _graphql._flask.create_app(graphql_path)
return graphql._flask.create_app(graphql_path)


@pytest.fixture()
Expand Down Expand Up @@ -506,7 +506,7 @@ def openapi_30():

@pytest.fixture()
def app_schema(openapi_version, operations):
return _aiohttp.make_openapi_schema(operations=operations, version=openapi_version)
return openapi._aiohttp.make_openapi_schema(operations=operations, version=openapi_version)


@pytest.fixture()
Expand Down Expand Up @@ -583,7 +583,7 @@ def run_and_assert(*args, **kwargs):

@pytest.fixture
def wsgi_app_factory():
return _flask.create_app
return openapi._flask.create_app


@pytest.fixture()
Expand All @@ -593,14 +593,19 @@ def flask_app(wsgi_app_factory, operations):

@pytest.fixture
def asgi_app_factory():
return _fastapi.create_app
return openapi._fastapi.create_app


@pytest.fixture()
def fastapi_app(asgi_app_factory):
return asgi_app_factory()


@pytest.fixture()
def fastapi_graphql_app(graphql_path):
return graphql._fastapi.create_app(graphql_path)


def make_importable(module):
"""Make the package importable by the inline CLI execution."""
pkgroot = module.dirpath()
Expand Down
29 changes: 29 additions & 0 deletions test/test_loaders.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

import pytest
from hypothesis import given, settings
from requests.models import Response
from yarl import URL

Expand Down Expand Up @@ -150,3 +151,31 @@ def test_absolute_urls_for_apps(loader):
# Then it should be rejected
with pytest.raises(ValueError, match="Schema path should be relative for WSGI/ASGI loaders"):
loader("http://127.0.0.1:1/schema.json", app=None) # actual app doesn't matter here


def test_graphql_wsgi_loader(graphql_path, graphql_app):
schema = schemathesis.graphql.from_wsgi(graphql_path, app=graphql_app)
strategy = schema[graphql_path]["POST"].as_strategy()

@given(case=strategy)
@settings(max_examples=1)
def test(case):
response = case.call_wsgi()
assert response.status_code == 200
assert "data" in response.json

test()


def test_graphql_asgi_loader(graphql_path, fastapi_graphql_app):
schema = schemathesis.graphql.from_asgi(graphql_path, app=fastapi_graphql_app)
strategy = schema[graphql_path]["POST"].as_strategy()

@given(case=strategy)
@settings(max_examples=1)
def test(case):
response = case.call_asgi()
assert response.status_code == 200
assert "data" in response.json()

test()

0 comments on commit 22354a9

Please sign in to comment.