fix: input with invalid types should raise an error

This commit is contained in:
Thomas Leonard 2022-03-20 21:04:29 +01:00
parent efe4b89015
commit 3bdc67c6ae
11 changed files with 428 additions and 25 deletions

View File

@ -270,7 +270,7 @@ The following is an example for creating a DateTime scalar:
return dt.isoformat()
@staticmethod
def parse_literal(node):
def parse_literal(node, _variables=None):
if isinstance(node, ast.StringValue):
return datetime.datetime.strptime(
node.value, "%Y-%m-%dT%H:%M:%S.%f")

View File

@ -24,8 +24,8 @@ from ...types.uuid import UUID
(ID, "1"),
(DateTime, '"2022-02-02T11:11:11"'),
(UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'),
(Decimal, "1.1"),
(JSONString, '{key:"foo",value:"bar"}'),
(Decimal, '"1.1"'),
(JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'),
(Base64, '"Q2hlbG8gd29ycmxkCg=="'),
],
)

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
from decimal import Decimal as _Decimal
from graphql import Undefined
from graphql.language.ast import StringValueNode, IntValueNode
from .scalars import Scalar
@ -25,10 +26,11 @@ class Decimal(Scalar):
def parse_literal(cls, node, _variables=None):
if isinstance(node, (StringValueNode, IntValueNode)):
return cls.parse_value(node.value)
return Undefined
@staticmethod
def parse_value(value):
try:
return _Decimal(value)
except ValueError:
return None
except Exception:
return Undefined

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
import json
from graphql import Undefined
from graphql.language.ast import StringValueNode
from .scalars import Scalar
@ -22,7 +23,11 @@ class JSONString(Scalar):
@staticmethod
def parse_literal(node, _variables=None):
if isinstance(node, StringValueNode):
try:
return json.loads(node.value)
except Exception as error:
raise ValueError(f"Badly formed JSONString: {str(error)}")
return Undefined
@staticmethod
def parse_value(value):

View File

@ -1,5 +1,6 @@
from typing import Any
from graphql import Undefined
from graphql.language.ast import (
BooleanValueNode,
FloatValueNode,
@ -67,9 +68,10 @@ class Int(Scalar):
try:
num = int(float(value))
except ValueError:
return None
return Undefined
if MIN_INT <= num <= MAX_INT:
return num
return Undefined
serialize = coerce_int
parse_value = coerce_int
@ -80,6 +82,7 @@ class Int(Scalar):
num = int(ast.value)
if MIN_INT <= num <= MAX_INT:
return num
return Undefined
class BigInt(Scalar):
@ -97,7 +100,7 @@ class BigInt(Scalar):
try:
num = int(float(value))
except ValueError:
return None
return Undefined
return num
serialize = coerce_int
@ -107,6 +110,7 @@ class BigInt(Scalar):
def parse_literal(ast, _variables=None):
if isinstance(ast, IntValueNode):
return int(ast.value)
return Undefined
class Float(Scalar):
@ -122,7 +126,7 @@ class Float(Scalar):
try:
return float(value)
except ValueError:
return None
return Undefined
serialize = coerce_float
parse_value = coerce_float
@ -131,6 +135,7 @@ class Float(Scalar):
def parse_literal(ast, _variables=None):
if isinstance(ast, (FloatValueNode, IntValueNode)):
return float(ast.value)
return Undefined
class String(Scalar):
@ -153,6 +158,7 @@ class String(Scalar):
def parse_literal(ast, _variables=None):
if isinstance(ast, StringValueNode):
return ast.value
return Undefined
class Boolean(Scalar):
@ -167,6 +173,7 @@ class Boolean(Scalar):
def parse_literal(ast, _variables=None):
if isinstance(ast, BooleanValueNode):
return ast.value
return Undefined
class ID(Scalar):
@ -185,3 +192,4 @@ class ID(Scalar):
def parse_literal(ast, _variables=None):
if isinstance(ast, (StringValueNode, IntValueNode)):
return ast.value
return Undefined

View File

@ -39,8 +39,25 @@ def test_bad_decimal_query():
not_a_decimal = "Nobody expects the Spanish Inquisition!"
result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal)
assert result.errors
assert len(result.errors) == 1
assert result.data is None
assert (
result.errors[0].message
== "Expected value of type 'Decimal', found \"Nobody expects the Spanish Inquisition!\"."
)
result = schema.execute("{ decimal(input: true) }")
assert result.errors
assert len(result.errors) == 1
assert result.data is None
assert result.errors[0].message == "Expected value of type 'Decimal', found true."
result = schema.execute("{ decimal(input: 1.2) }")
assert result.errors
assert len(result.errors) == 1
assert result.data is None
assert result.errors[0].message == "Expected value of type 'Decimal', found 1.2."
def test_decimal_string_query_integer():

View File

@ -21,6 +21,10 @@ def test_jsonstring_query():
assert not result.errors
assert result.data == {"json": json_value}
result = schema.execute("""{ json(input: "{}") }""")
assert not result.errors
assert result.data == {"json": "{}"}
def test_jsonstring_query_variable():
json_value = '{"key": "value"}'
@ -31,3 +35,51 @@ def test_jsonstring_query_variable():
)
assert not result.errors
assert result.data == {"json": json_value}
def test_jsonstring_optional_uuid_input():
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ json(input: null) }")
assert not result.errors
assert result.data == {"json": None}
def test_jsonstring_invalid_query():
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute("{ json(input: 1) }")
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == "Expected value of type 'JSONString', found 1."
result = schema.execute("{ json(input: {}) }")
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == "Expected value of type 'JSONString', found {}."
result = schema.execute('{ json(input: "a") }')
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == (
"Expected value of type 'JSONString', found \"a\"; "
"Badly formed JSONString: Expecting value: line 1 column 1 (char 0)"
)
result = schema.execute("""{ json(input: "{\\'key\\': 0}") }""")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Syntax Error: Invalid character escape sequence: '\\''."
)
result = schema.execute("""{ json(input: "{\\"key\\": 0,}") }""")
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == (
'Expected value of type \'JSONString\', found "{\\"key\\": 0,}"; '
"Badly formed JSONString: Expecting property name enclosed in double quotes: line 1 column 11 (char 10)"
)

View File

@ -1,4 +1,7 @@
from ..scalars import Scalar, Int, BigInt
from ..objecttype import ObjectType, Field
from ..scalars import Scalar, Int, BigInt, Float, String, Boolean
from ..schema import Schema
from graphql import Undefined
from graphql.language.ast import IntValueNode
@ -11,19 +14,295 @@ def test_scalar():
def test_ints():
assert Int.parse_value(2**31 - 1) is not None
assert Int.parse_value("2.0") is not None
assert Int.parse_value(2**31) is None
assert Int.parse_value(2**31 - 1) is not Undefined
assert Int.parse_value("2.0") == 2
assert Int.parse_value(2**31) is Undefined
assert Int.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1
assert Int.parse_literal(IntValueNode(value=str(2**31))) is None
assert Int.parse_literal(IntValueNode(value=str(2**31))) is Undefined
assert Int.parse_value(-(2**31)) is not None
assert Int.parse_value(-(2**31) - 1) is None
assert Int.parse_value(-(2**31)) is not Undefined
assert Int.parse_value(-(2**31) - 1) is Undefined
assert BigInt.parse_value(2**31) is not None
assert BigInt.parse_value("2.0") is not None
assert BigInt.parse_value(-(2**31) - 1) is not None
assert BigInt.parse_value(2**31) is not Undefined
assert BigInt.parse_value("2.0") == 2
assert BigInt.parse_value(-(2**31) - 1) is not Undefined
assert BigInt.parse_literal(IntValueNode(value=str(2**31 - 1))) == 2**31 - 1
assert BigInt.parse_literal(IntValueNode(value=str(2**31))) == 2**31
def return_input(_parent, _info, input):
return input
class Optional(ObjectType):
int = Int(input=Int(), resolver=return_input)
big_int = BigInt(input=BigInt(), resolver=return_input)
float = Float(input=Float(), resolver=return_input)
bool = Boolean(input=Boolean(), resolver=return_input)
string = String(input=String(), resolver=return_input)
class Query(ObjectType):
optional = Field(Optional)
def resolve_optional(self, info):
return Optional()
def resolve_required(self, info, input):
return input
schema = Schema(query=Query)
class TestInt:
def test_query(self):
"""
Test that a normal query works.
"""
result = schema.execute("{ optional { int(input: 20) } }")
assert not result.errors
assert result.data == {"optional": {"int": 20}}
def test_optional_input(self):
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ optional { int(input: null) } }")
assert not result.errors
assert result.data == {"optional": {"int": None}}
def test_invalid_input(self):
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute('{ optional { int(input: "20") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == 'Int cannot represent non-integer value: "20"'
)
result = schema.execute('{ optional { int(input: "a") } }')
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == 'Int cannot represent non-integer value: "a"'
result = schema.execute("{ optional { int(input: true) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == "Int cannot represent non-integer value: true"
)
class TestBigInt:
def test_query(self):
"""
Test that a normal query works.
"""
value = 2**31
result = schema.execute("{ optional { bigInt(input: %s) } }" % value)
assert not result.errors
assert result.data == {"optional": {"bigInt": value}}
def test_optional_input(self):
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ optional { bigInt(input: null) } }")
assert not result.errors
assert result.data == {"optional": {"bigInt": None}}
def test_invalid_input(self):
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute('{ optional { bigInt(input: "20") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == "Expected value of type 'BigInt', found \"20\"."
)
result = schema.execute('{ optional { bigInt(input: "a") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == "Expected value of type 'BigInt', found \"a\"."
)
result = schema.execute("{ optional { bigInt(input: true) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == "Expected value of type 'BigInt', found true."
)
class TestFloat:
def test_query(self):
"""
Test that a normal query works.
"""
result = schema.execute("{ optional { float(input: 20) } }")
assert not result.errors
assert result.data == {"optional": {"float": 20.0}}
result = schema.execute("{ optional { float(input: 20.2) } }")
assert not result.errors
assert result.data == {"optional": {"float": 20.2}}
def test_optional_input(self):
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ optional { float(input: null) } }")
assert not result.errors
assert result.data == {"optional": {"float": None}}
def test_invalid_input(self):
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute('{ optional { float(input: "20") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == 'Float cannot represent non numeric value: "20"'
)
result = schema.execute('{ optional { float(input: "a") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == 'Float cannot represent non numeric value: "a"'
)
result = schema.execute("{ optional { float(input: true) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == "Float cannot represent non numeric value: true"
)
class TestBoolean:
def test_query(self):
"""
Test that a normal query works.
"""
result = schema.execute("{ optional { bool(input: true) } }")
assert not result.errors
assert result.data == {"optional": {"bool": True}}
result = schema.execute("{ optional { bool(input: false) } }")
assert not result.errors
assert result.data == {"optional": {"bool": False}}
def test_optional_input(self):
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ optional { bool(input: null) } }")
assert not result.errors
assert result.data == {"optional": {"bool": None}}
def test_invalid_input(self):
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute('{ optional { bool(input: "True") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== 'Boolean cannot represent a non boolean value: "True"'
)
result = schema.execute('{ optional { bool(input: "true") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== 'Boolean cannot represent a non boolean value: "true"'
)
result = schema.execute('{ optional { bool(input: "a") } }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== 'Boolean cannot represent a non boolean value: "a"'
)
result = schema.execute("{ optional { bool(input: 1) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Boolean cannot represent a non boolean value: 1"
)
result = schema.execute("{ optional { bool(input: 0) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Boolean cannot represent a non boolean value: 0"
)
class TestString:
def test_query(self):
"""
Test that a normal query works.
"""
result = schema.execute('{ optional { string(input: "something something") } }')
assert not result.errors
assert result.data == {"optional": {"string": "something something"}}
result = schema.execute('{ optional { string(input: "True") } }')
assert not result.errors
assert result.data == {"optional": {"string": "True"}}
result = schema.execute('{ optional { string(input: "0") } }')
assert not result.errors
assert result.data == {"optional": {"string": "0"}}
def test_optional_input(self):
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ optional { string(input: null) } }")
assert not result.errors
assert result.data == {"optional": {"string": None}}
def test_invalid_input(self):
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute("{ optional { string(input: 1) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message == "String cannot represent a non string value: 1"
)
result = schema.execute("{ optional { string(input: 3.2) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "String cannot represent a non string value: 3.2"
)
result = schema.execute("{ optional { string(input: true) } }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "String cannot represent a non string value: true"
)

View File

@ -1,3 +1,4 @@
from graphql import Undefined
from ..scalars import Boolean, Float, Int, String
@ -9,12 +10,12 @@ def test_serializes_output_int():
assert Int.serialize(1.1) == 1
assert Int.serialize(-1.1) == -1
assert Int.serialize(1e5) == 100000
assert Int.serialize(9876504321) is None
assert Int.serialize(-9876504321) is None
assert Int.serialize(1e100) is None
assert Int.serialize(-1e100) is None
assert Int.serialize(9876504321) is Undefined
assert Int.serialize(-9876504321) is Undefined
assert Int.serialize(1e100) is Undefined
assert Int.serialize(-1e100) is Undefined
assert Int.serialize("-1.1") == -1
assert Int.serialize("one") is None
assert Int.serialize("one") is Undefined
assert Int.serialize(False) == 0
assert Int.serialize(True) == 1
@ -27,7 +28,7 @@ def test_serializes_output_float():
assert Float.serialize(1.1) == 1.1
assert Float.serialize(-1.1) == -1.1
assert Float.serialize("-1.1") == -1.1
assert Float.serialize("one") is None
assert Float.serialize("one") is Undefined
assert Float.serialize(False) == 0
assert Float.serialize(True) == 1

View File

@ -1,14 +1,19 @@
from ..objecttype import ObjectType
from ..schema import Schema
from ..uuid import UUID
from ..structures import NonNull
class Query(ObjectType):
uuid = UUID(input=UUID())
required_uuid = UUID(input=NonNull(UUID), required=True)
def resolve_uuid(self, info, input):
return input
def resolve_required_uuid(self, info, input):
return input
schema = Schema(query=Query)
@ -29,3 +34,35 @@ def test_uuidstring_query_variable():
)
assert not result.errors
assert result.data == {"uuid": uuid_value}
def test_uuidstring_optional_uuid_input():
"""
Test that we can provide a null value to an optional input
"""
result = schema.execute("{ uuid(input: null) }")
assert not result.errors
assert result.data == {"uuid": None}
def test_uuidstring_invalid_query():
"""
Test that if an invalid type is provided we get an error
"""
result = schema.execute("{ uuid(input: 1) }")
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == "Expected value of type 'UUID', found 1."
result = schema.execute('{ uuid(input: "a") }')
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Expected value of type 'UUID', found \"a\"; badly formed hexadecimal UUID string"
)
result = schema.execute("{ requiredUuid(input: null) }")
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == "Expected value of type 'UUID!', found null."

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
from uuid import UUID as _UUID
from graphql.language.ast import StringValueNode
from graphql import Undefined
from .scalars import Scalar
@ -24,6 +25,7 @@ class UUID(Scalar):
def parse_literal(node, _variables=None):
if isinstance(node, StringValueNode):
return _UUID(node.value)
return Undefined
@staticmethod
def parse_value(value):