Giter VIP home page Giter VIP logo

hypothesis-graphql's Introduction

hypothesis-graphql

Build Coverage Version Python versions Chat License

Generate queries matching your GraphQL schema, and use them to verify your backend implementation

It is a Python library that provides a set of Hypothesis strategies that let you write tests parametrized by a source of examples. Generated queries have arbitrary depth and may contain any subset of GraphQL types defined in the input schema. They expose edge cases in your code that are unlikely to be found otherwise.

Schemathesis provides a higher-level interface around this library and finds server crashes automatically.

Usage

hypothesis-graphql provides the from_schema function, which takes a GraphQL schema and returns a Hypothesis strategy for GraphQL queries matching the schema:

from hypothesis import given
from hypothesis_graphql import from_schema
import requests

# Strings and `graphql.GraphQLSchema` are supported
SCHEMA = """
type Book {
  title: String
  author: Author
}

type Author {
  name: String
  books: [Book]
}

type Query {
  getBooks: [Book]
  getAuthors: [Author]
}

type Mutation {
  addBook(title: String!, author: String!): Book!
  addAuthor(name: String!): Author!
}
"""


@given(from_schema(SCHEMA))
def test_graphql(query):
    # Will generate samples like these:
    #
    # {
    #   getBooks {
    #     title
    #   }
    # }
    #
    # mutation {
    #   addBook(title: "H4Z\u7869", author: "\u00d2"){
    #     title
    #   }
    # }
    response = requests.post("http://127.0.0.1/graphql", json={"query": query})
    assert response.status_code == 200
    assert response.json().get("errors") is None

It is also possible to generate queries or mutations separately with hypothesis_graphql.queries and hypothesis_graphql.mutations.

Customization

To restrict the set of fields in generated operations use the fields argument:

@given(from_schema(SCHEMA, fields=["getAuthors"]))
def test_graphql(query):
    # Only `getAuthors` will be generated
    ...

You can customize the string generation with these arguments to from_schema:

  • allow_x00 (default True): Determines whether to allow the generation of \x00 bytes within strings. It is useful to avoid rejecting tests as invalid by some web servers.
  • codec (default utf-8): Specifies the codec used for generating strings. It helps if you need to restrict the inputs to, for example, the ASCII range.
@given(from_schema(SCHEMA, allow_x00=False, codec="ascii"))
def test_graphql(query):
    assert "\0" not in query
    query.encode("ascii")

It is also possible to generate custom scalars. For example, Date:

from hypothesis import strategies as st, given
from hypothesis_graphql import from_schema, nodes

SCHEMA = """
scalar Date

type Query {
  getByDate(created: Date!): Int
}
"""


@given(
    from_schema(
        SCHEMA,
        custom_scalars={
            # Standard scalars work out of the box, for custom ones you need
            # to pass custom strategies that generate proper AST nodes
            "Date": st.dates().map(nodes.String)
        },
    )
)
def test_graphql(query):
    # Example:
    #
    #  { getByDate(created: "2000-01-01") }
    #
    ...

The hypothesis_graphql.nodes module includes a few helpers to generate various node types:

  • String -> graphql.StringValueNode
  • Float -> graphql.FloatValueNode
  • Int -> graphql.IntValueNode
  • Object -> graphql.ObjectValueNode
  • List -> graphql.ListValueNode
  • Boolean -> graphql.BooleanValueNode
  • Enum -> graphql.EnumValueNode
  • Null -> graphql.NullValueNode (a constant, not a function)

They exist because classes like graphql.StringValueNode can't be directly used in map calls due to kwarg-only arguments.

License

The code in this project is licensed under MIT license. By contributing to hypothesis-graphql, you agree that your contributions will be licensed under its MIT license.

hypothesis-graphql's People

Contributors

branchvincent avatar dependabot[bot] avatar stranger6667 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

hypothesis-graphql's Issues

[FEATURE] Tests against real schema corpus

Is your feature request related to a problem? Please describe.
The current test suite is quite limited to simple schemas that I was able to come up with. It will be nice to have tests against real-life GraphQL schemas, similar to what hypothesis-jsonschema does.

Describe the solution you'd like
Build a test corpus from something like https://github.com/APIs-guru/graphql-apis and run query generation tests against them.

[BUG] Invalid queries when different inline fragments have the same fields of different type

Describe the bug
When a GraphQL query type has multiple possible types on the same level (e.g via union) and those types have fields with the same name but different types, then hypothesis-graphql generates invalid queries

To Reproduce
Schema:

interface Conflict {
  id: ID
}

type FloatModel implements Conflict {
  id: ID,
  query: Float!
}

type StringModel implements Conflict {
  id: ID,
  query: String!
}

type Query {
  getData: Conflict
}

Generated query:

{
  getData {
    ... on FloatModel {
      query
    }
    ... on StringModel {
      query
    }
  }
}

Error: Fields 'query' conflict because they return conflicting types 'Float!' and 'String!'. Use different aliases on the fields to fetch both if this was intentional.

Additional context
Comes from this job

Option to skip user-defined types

If there are no strategies provided (as #22 is implemented), skip fields with user-defined types rather than raise errors.

It could be a config option passed to the query function

Improve performance

At the moment, data generation is relatively slow. On my machine (i7-8700K), generation of 100 queries for this schema takes on average 0.8 seconds:

type Book {
  title: String
  author: Author
}

type Author {
  name: String
  books: [Book]
}

enum Color {
  RED
  GREEN
  BLUE
}

input QueryInput {
  eq: String
  ne: String
}

input NestedQueryInput {
  code: QueryInput
}

type Model {
  int: Int,
  float: Float,
  string: String
  id: ID,
  boolean: Boolean
  color: Color
}

type Query {
      getModel(int: Int): Model
}

The version without AST nodes was ~3 times faster. Not a priority right now definitely, but we can investigate the cause in the future and improve performance

[BUG] Invalid queries when arguments have required fields

Describe the bug
If arguments in the input query type contain required fields, they should be present in generated queries. Otherwise, such a query is invalid.

Example:

input RequiredInput {
  eq: Float!,
}

type Query { 
  getData(query: RequiredInput): Float! 
}

Generated query:

{
  getData(query: {})
}

Error: Field 'RequiredInput.eq' of required type 'Float!' was not provided.

Generate arguments for queries

Currently query doesn't generate arguments. E.g.

type Query {
    getBooksByAuthor(name: String) [Book]
}

name will not be generated.

Can be done by adding arguments to a field node

Add a test server

It will be really helpful to verify generated queries against a real GraphQL server

Build Query AST in one pass

At the moment, when a query is generated here a dictionary is created on the first pass, then it is traversed again and converted into an AST tree. It could be done in one pass, which will be more efficient

Handle all possible field types in query generation

At the moment only GraphQLScalarType, GraphQLList and GraphQLObjectType are handled. But there could be more different types which are listed in a union type GraphQLOutputType:

  • - GraphQLScalarType
  • - GraphQLObjectType
  • - GraphQLInterfaceType
  • - GraphQLUnionType
  • - GraphQLEnumType
  • - GraphQLWrappingType

[FEATURE] Generate `null` for nullable custom scalars

Is your feature request related to a problem? Please describe.
At the moment, if a custom scalar is nullable, it is skipped from arguments. But null is a valid value, which is not generated. Example query:

{
  countries(filter: {code: {eq: null}}) {
    languages {
      name
      native
    }
  }
}

Describe the solution you'd like
Generate NullValueNode for such cases

Decrease the number of non-printable characters in generated queries

Even though that on the network layer non-printable chars might be compressed, etc it may decrease debuggability. Consider:

Falsifying example: test(
    case=GraphQLCase(body='{\n  search {\n    operationLocations {\n      UIC\n    }\n  }\n}\n'),
)

vs. (some minimal whitespaces):

Falsifying example: test(
    case=GraphQLCase(body='{ search { operationLocations { UIC } } }'),
)

Of course, it might be pretty-printed, but Hypothesis output is the primary source of description, and it displays a raw string. Therefore it is crucial to have it as readable as possible.

Implementation notes:

  • Create a subclass of graphql.language.printer.PrintAstVisitor;
  • Redefine methods that do pretty-printing and make them return a more compact definition

Mostly it should be enough to replace some "\n" with " "

[BUG] Do not generate out-of range integers

Describe the bug
The GraphQL spec defines Int! as an integer value in [-2 ** 31:2 ** 31 - 1] range. The current implementation may generate queries that will be rejected by graphql.validate with Int cannot represent non 32-bit signed integer value: -2147483649.

[FEATURE] Support interfaces

Is your feature request related to a problem? Please describe.
Interfaces are ignored now which leads to invalid queries and cases when fields from implementers are not used at all

[FEATURE] Handle union types

Is your feature request related to a problem? Please describe.
Unit types are completely ignored at the moment, which leads to invalid queries.

[BUG] When an input object has an optional enum argument, one of the generated values is "EnumValueNode"

There is a bug that occurs when a GraphQL schema has an optional enum argument on an input object. One of the values that hypothesis-graphql generates is: EnumValueNode. I'm guessing that this is happening during the case when the enum value is supposed to be null.

To demonstrate, run the following with pytest -s:

import schemathesis

schema = schemathesis.graphql.from_file(
    """
    enum MyEnum {
        FOO
        BAR
        BAZ
    }
    
    input MyInput {
        value: MyEnum
    }
    
    type Query {
        hello(i: MyInput!): String
    }
    """
)

@schema.parametrize()
def test_schema(case):
    print(case.body)

The printed output is the following:

{
  hello(i: {value: FOO})
}

{
  hello(i: {value: EnumValueNode})
}

{
  hello(i: {value: BAZ})
}

{
  hello(i: {value: FOO})
}

{
  hello(i: {value: BAR})
}

I am using:
schemathesis 3.6.3
hypothesis-graphql 0.4.1
graphql-core 3.1.4

[BUG] Text-generation strategy for string arguments includes surrogate characters

I'm trying to figure out why I keep getting the following error: UnicodeEncodeError: 'utf-8' codec can't encode characters in position 0-1: surrogates not allowed

For some reason, the text strategy for arguments of type String is generating unicode data that cannot be encoded as UTF-8, because it includes surrogate characters.

From what I can tell, hypothesis-graphql is using hypothesis.strategies.text() for String-based arguments: https://github.com/Stranger6667/hypothesis-graphql/blob/master/src/hypothesis_graphql/_strategies/primitives.py#L46

The documentation for hypothesis.strategies.text() explicitly states:

The default alphabet strategy can generate the full unicode range but excludes surrogate characters because they are invalid in the UTF-8 encoding.

https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.text

So I'm very confused why surrogate characters are being produced.

To Reproduce
Run this test using pytest. You should get a UnicodeEncodeError. You can include --log-cli-level=INFO to see all the values being generated for the user argument.

import logging
import graphql
import schemathesis

schema = schemathesis.graphql.from_file(
    """
    type Query {
        hello(user: String!): String
    }
    """
)

logger = logging.getLogger()

@schema.parametrize()
def test_schema(case):
    document = graphql.parse(case.body)
    argument_node = document.definitions[0].selection_set.selections[0].arguments[0]
    if argument_node.name.value == "user":
        value = argument_node.value.value
        logger.info(f"User argument = {value!r}")
        encoded = value.encode("utf8")

Expected behavior
The text strategy used for String-based arguments includes only characters that can be UTF-8 encoded.

Environment (please complete the following information):

  • OS: Linux
  • Python version: 3.8.5
  • schemathesis version: 3.6.1
  • hypothesis-graphql version: 0.4.0
  • hypothesis version: 6.8.9
  • graphql-core version: 3.1.4
  • pytest version: 6.2.3

Support user-defined types

For example, if there is a custom scalar type:

scalar Date

Then hypothesis-graphql doesn't know how to generate data for it, but it would be nice to have a way to generate it via custom strategies provided by the user. Something like this:

from hypothesis_graphql import strategies as gql_st
from hypothesis import strategies as st

@given(case=gql_st.query(schema, custom_types={"Date": st.dates().map(str)}
def test_something(case):
    ...

Implementation notes:

  • Add custom_types argument to the hypothesis_graphql._strategies.queries.query function. Its type is Dict[str, SearchStrategy];
  • Pass it down to the hypothesis_graphql._strategies.primitives.scalar function;
  • At the end of the function, add a check if the scalar name is in the custom_types dictionary. Then use a strategy from this there;
  • If type is nullable, then combine that strategy with st.none() and return it to the caller

Support graphql-core<2

The current Graphene version does not support graphql-core>=3 and it will take some more time ... It seems like the AST part we're using is not much difference between these versions and it should be easy to support both versions of graphql-core

[BUG] Missing aliases

Still there. The TMDB schema from the corpus and the following query is generated:

{
  people {
    trending(
      last: 370363096
      after: ""
      timeWindow: Day
      first: null
      before: "\u001bh\u0015"
    ) {
      totalCount
    }
    person(id: 1427696479) {
      profilePicture(size: W45)
      knownForDepartment
      isAdult
      alsoKnownAs
      name
      imdbID
    }
    popular(
      last: 10124
      after: "\u0087\u00d4\uc03d\ub8da\u0011\u00ae;\u22fd\u0084\u008f)\uf674\u1fd2\u00899"
      first: null
      before: null
    ) {
      pageInfo {
        startCursor
      }
      totalCount
      edges {
        cursor
      }
    }
    search(last: -249, after: "\u00ceh\u8341", term: "", first: null, before: null) {
      totalCount
    }
  }
  movies {
    topRated(
      last: -126
      after: "\u00a9\u00c5\u0092j\u00e3(*PLE"
      first: 149
      before: "\u0084\u00c4i\u00b8\u6364\u754dw"
    ) {
      edges {
        cursor
      }
      totalCount
    }
    trending(
      last: -132
      after: "\u0099"
      timeWindow: Day
      first: -61425
      before: null
    ) {
      totalCount
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
    }
    popular(last: null, after: null, first: -102257250, before: null) {
      pageInfo {
        hasNextPage
        hasPreviousPage
        endCursor
      }
    }
    upcoming(last: -77, after: null, first: null, before: null) {
      edges {
        cursor
        node {
          title
          releaseDate
          id
          videos {
            type
          }
          translations {
            iso639_1
          }
          ... on DetailedMovie {
            runtime
            images {
              posters {
                width
                voteCount
                image(size: W92)
              }
            }
            poster(size: W92)
            translations {
              iso3166_1
              info {
                overview
                title
              }
              iso639_1
            }
            externalIds {
              instagram
              facebook
              twitter
              imdb
            }
            backdrop(size: W1280)
            productionCompanies {
              id
              name
              logo(size: W154)
              originCountry
            }
            releaseDate
            homepage
            imdbID
          }
          ... on MovieResult {
            reviews(first: null, before: null, last: 197, after: null) {
              pageInfo {
                startCursor
              }
            }
            originalTitle
            alternativeTitles {
              type
              title
            }
          }
        }
      }
      pageInfo {
        hasNextPage
      }
      totalCount
    }
    nowPlaying(last: null, after: null, first: null, before: null) {
      pageInfo {
        endCursor
        startCursor
        hasNextPage
        hasPreviousPage
      }
    }
    search(
      last: null
      after: "\u001c\u0013"
      term: "\u009b\u0089\t\u3068"
      first: null
      before: "\u249b\u0094\ue856\u00fa`\u9847\u00d8Ql\u0080\u009fU"
    ) {
      totalCount
      pageInfo {
        endCursor
      }
      edges {
        cursor
      }
    }
  }
  trending(
    last: null
    after: "\u00adh\uebd3"
    timeWindow: Week
    first: null
    before: "\u001c"
  ) {
    pageInfo {
      startCursor
      endCursor
      hasNextPage
      hasPreviousPage
    }
    edges {
      node {
        ... on PersonListResult {
          isAdult
        }
        ... on MovieResult {
          originalTitle
        }
        ... on PersonListResult {
          details {
            alsoKnownAs
            birthday
            isAdult
            externalIds {
              imdb
            }
            details {
              details {
                placeOfBirth
              }
            }
            images {
              voteCount
              height
              aspectRatio
              width
              iso639_1
              image(size: Original)
              voteAverage
            }
          }
          popularityIndex
        }
        ... on PersonListResult {
          externalIds {
            twitter
            imdb
          }
          images {
            width
            height
          }
        }
        ... on PersonListResult {
          profilePicture(size: Original)
          images {
            width
          }
        }
        ... on MovieResult {
          title
        }
        ... on TVShowResult {
          firstAirDate
          translations {
            iso639_1
            localizedLanguage
            iso3166_1
          }
          poster(size: W185)
          videos {
            type
            thumbnail
            links {
              fireTV
              androidTV
              tvOS
            }
            iso3166_1
            site
          }
          numberOfRatings
          details {
            id
            productionCompanies {
              id
            }
            rating
            firstAirDate
            backdrop(size: W300)
            homepage
          }
          externalIds {
            twitter
          }
          overview
          originalName
          rating
          recommendations(first: null, before: null, last: 44267, after: null) {
            totalCount
            pageInfo {
              endCursor
            }
          }
          popularityIndex
          originalLanguage
          streamingOptions {
            bestOffering {
              links {
                web
                androidTV
                tvOS
                fireTV
              }
              resolution
              type
            }
            offerings {
              links {
                fireTV
                androidTV
              }
            }
          }
        }
        ... on PersonListResult {
          profilePicture_1: profilePicture(size: Original)
          images {
            width
          }
        }
        ... on MovieResult {
          title
        }
        ... on TVShowResult {
          firstAirDate
          translations {
            iso639_1
            localizedLanguage
            iso3166_1
          }
          poster_1: poster(size: W185)
          videos {
            type
            thumbnail
            links {
              fireTV
              androidTV
              tvOS
            }
            iso3166_1
            site
          }
          numberOfRatings
          details {
            id
            productionCompanies {
              id
            }
            rating
            firstAirDate
            backdrop_1: backdrop(size: W300)
            homepage
          }
          externalIds {
            twitter
          }
          overview
          originalName
          rating
          recommendations_1: recommendations(
            first: null
            before: null
            last: 44267
            after: null
          ) {
            totalCount
            pageInfo {
              endCursor
            }
          }
          popularityIndex
          originalLanguage
          streamingOptions {
            bestOffering {
              links {
                web
                androidTV
                tvOS
                fireTV
              }
              resolution
              type
            }
            offerings {
              links {
                fireTV
                androidTV
              }
            }
          }
        }
      }
    }
  }

Error:

Fields 'popularityIndex' conflict because they return conflicting types 'Float!' and 'Float'. Use different aliases on the fields to fetch both if this was intentional.

GraphQL request:207:11
206 |           }
207 |           popularityIndex
    |           ^
208 |         }

GraphQL request:347:11
346 |           }
347 |           popularityIndex
    |           ^
348 |           originalLanguage

Verify shrinking behavior

I am not sure how good shrinking works with the current implementation and if it could be improved. We can try to build some app first, add a target error and check how good it will be in finding a minimal example.

There is no concrete plan on my mind now since I don't have much experience with Hypothesis internals, maybe we can get some advice from Hypothesis devs

Unable to import from_schema from hypothesis_graphql

Describe the bug
Unable to import from_schema from hypothesis_graphql

To Reproduce
Steps to reproduce the behaviour:

  1. My GraphQL schema is - the sample schema provided in the README
  2. Run this command - python3 hypothesis_graphql.py
  3. See error -
    ImportError: cannot import name 'from_schema' from partially initialized module 'hypothesis_graphql' (most likely due to a circular import) (file-path/hypothesis_graphql.py)

Expected behavior
Imports to go through correctly

Environment (please complete the following information):

  • OS: Mac
  • Python version: 3.9.6 and also noticed with 3.11.1
  • hypothesis-graphql version: 0.11.0
  • hypothesis version: 6.100.0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.