Repository: graphql-python/graphene
Pull request #1414 (2022-03-20 20:06:44)

Issue #1413 fix invalid input type
The failure in the CI exists on master as well.
You can find the fix on the PR #1412


Patch:
diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst
index db20a52..f47fffe 100644
--- a/docs/types/scalars.rst
+++ b/docs/types/scalars.rst
@@ -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")
diff --git a/graphene/types/decimal.py b/graphene/types/decimal.py
index 94968f4..0c6ccc9 100644
--- a/graphene/types/decimal.py
+++ b/graphene/types/decimal.py
@@ -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
diff --git a/graphene/types/json.py b/graphene/types/json.py
index 7e60de7..ca55836 100644
--- a/graphene/types/json.py
+++ b/graphene/types/json.py
@@ -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):
-            return json.loads(node.value)
+            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):
diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py
index 80cfcc8..a468bb3 100644
--- a/graphene/types/scalars.py
+++ b/graphene/types/scalars.py
@@ -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
diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py
index 4714a67..f2ba1fc 100644
--- a/graphene/types/uuid.py
+++ b/graphene/types/uuid.py
@@ -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):


Test patch:
diff --git a/graphene/tests/issues/test_1419.py b/graphene/tests/issues/test_1419.py
index 243645f..a08374d 100644
--- a/graphene/tests/issues/test_1419.py
+++ b/graphene/tests/issues/test_1419.py
@@ -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=="'),
     ],
 )
diff --git a/graphene/types/tests/test_decimal.py b/graphene/types/tests/test_decimal.py
index 9757e82..1ba48bd 100644
--- a/graphene/types/tests/test_decimal.py
+++ b/graphene/types/tests/test_decimal.py
@@ -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():
diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py
index b553718..bb754b3 100644
--- a/graphene/types/tests/test_json.py
+++ b/graphene/types/tests/test_json.py
@@ -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)"
+    )
diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py
index 9dce6c3..cbdfd8c 100644
--- a/graphene/types/tests/test_scalar.py
+++ b/graphene/types/tests/test_scalar.py
@@ -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"
+        )
diff --git a/graphene/types/tests/test_scalars_serialization.py b/graphene/types/tests/test_scalars_serialization.py
index a91efe2..a0028c8 100644
--- a/graphene/types/tests/test_scalars_serialization.py
+++ b/graphene/types/tests/test_scalars_serialization.py
@@ -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
 
diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py
index 2280b41..d34f166 100644
--- a/graphene/types/tests/test_uuid.py
+++ b/graphene/types/tests/test_uuid.py
@@ -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."