From 975c9fe3993bcc34066d40d87c1667090c0e6ac4 Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Thu, 29 Aug 2019 16:19:56 +0700 Subject: [PATCH] BREAKING/BUGFIX: Strict coercion of scalar types (#278) --- CHANGELOG.md | 1 + src/Type/Definition/BooleanType.php | 12 +- src/Type/Definition/FloatType.php | 35 +-- src/Type/Definition/IDType.php | 30 +- src/Type/Definition/IntType.php | 60 ++-- src/Type/Definition/StringType.php | 37 +-- tests/Executor/ExecutorSchemaTest.php | 2 +- tests/Executor/VariablesTest.php | 2 +- tests/Type/ScalarSerializationTest.php | 284 ++++++------------ tests/Type/TestClasses/CanCastToString.php | 21 ++ tests/Type/{ => TestClasses}/ObjectIdStub.php | 2 +- tests/Utils/AstFromValueTest.php | 3 +- tests/Utils/CoerceValueTest.php | 106 ++++--- 13 files changed, 252 insertions(+), 343 deletions(-) create mode 100644 tests/Type/TestClasses/CanCastToString.php rename tests/Type/{ => TestClasses}/ObjectIdStub.php (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 034b6eb30..c3f3a3310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- **BREAKING/BUGFIX:** Strict coercion of scalar types (#278) - **BREAKING:** Removed deprecated directive introspection fields (onOperation, onFragment, onField) - **BREAKING:** Removal of `VariablesDefaultValueAllowed` validation rule. All variables may now specify a default value. - **BREAKING:** renamed `ProvidedNonNullArguments` to `ProvidedRequiredArguments` (no longer require values to be provided to non-null arguments which provide a default value). diff --git a/src/Type/Definition/BooleanType.php b/src/Type/Definition/BooleanType.php index 478159cba..3ef882db6 100644 --- a/src/Type/Definition/BooleanType.php +++ b/src/Type/Definition/BooleanType.php @@ -21,23 +21,15 @@ class BooleanType extends ScalarType public $description = 'The `Boolean` scalar type represents `true` or `false`.'; /** - * Coerce the given value to a boolean. + * Serialize the given value to a boolean. * * The GraphQL spec leaves this up to the implementations, so we just do what * PHP does natively to make this intuitive for developers. * * @param mixed $value - * - * @throws Error */ public function serialize($value) : bool { - if (is_array($value)) { - throw new Error( - 'Boolean cannot represent an array value: ' . Utils::printSafe($value) - ); - } - return (bool) $value; } @@ -54,7 +46,7 @@ public function parseValue($value) return $value; } - throw new Error('Cannot represent value as boolean: ' . Utils::printSafe($value)); + throw new Error('Boolean cannot represent a non boolean value: ' . Utils::printSafe($value)); } /** diff --git a/src/Type/Definition/FloatType.php b/src/Type/Definition/FloatType.php index 78b3fb858..2335bcd50 100644 --- a/src/Type/Definition/FloatType.php +++ b/src/Type/Definition/FloatType.php @@ -10,9 +10,12 @@ use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; use GraphQL\Utils\Utils; +use function floatval; use function is_array; use function is_bool; use function is_finite; +use function is_float; +use function is_int; use function is_nan; use function is_numeric; use function sprintf; @@ -37,26 +40,9 @@ class FloatType extends ScalarType */ public function serialize($value) { - return $this->coerceFloat($value); - } - - private function coerceFloat($value) - { - if (is_array($value)) { - throw new Error( - sprintf('Float cannot represent an array value: %s', Utils::printSafe($value)) - ); - } + $float = is_numeric($value) || is_bool($value) ? floatval($value) : null; - if ($value === '') { - throw new Error( - 'Float cannot represent non numeric value: (empty string)' - ); - } - - $float = is_numeric($value) || is_bool($value) ? (float) $value : null; - - if ($float === null || ! is_finite($float) || is_nan($float)) { + if ($float === null || ! is_finite($float)) { throw new Error( 'Float cannot represent non numeric value: ' . Utils::printSafe($value) @@ -75,7 +61,16 @@ private function coerceFloat($value) */ public function parseValue($value) { - return $this->coerceFloat($value); + $float = is_float($value) || is_int($value) ? floatval($value) : null; + + if ($float === null || ! is_finite($float)) { + throw new Error( + 'Float cannot represent non numeric value: ' . + Utils::printSafe($value) + ); + } + + return $float; } /** diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index f4db03d31..9f7610811 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -39,22 +39,12 @@ class IDType extends ScalarType */ public function serialize($value) { - if ($value === true) { - return 'true'; - } - if ($value === false) { - return 'false'; - } - if ($value === null) { - return 'null'; - } - if (is_array($value)) { - throw new Error( - 'ID cannot represent an array value: ' . Utils::printSafe($value) - ); - } - if (! is_scalar($value) && (! is_object($value) || ! method_exists($value, '__toString'))) { - throw new Error('ID cannot represent non scalar value: ' . Utils::printSafe($value)); + $canCast = is_string($value) + || is_int($value) + || (is_object($value) && method_exists($value, '__toString')); + + if (! $canCast) { + throw new Error('ID cannot represent value: ' . Utils::printSafe($value)); } return (string) $value; @@ -72,13 +62,7 @@ public function parseValue($value) if (is_string($value) || is_int($value)) { return (string) $value; } - if (is_array($value)) { - throw new Error( - 'ID cannot represent an array value: ' . Utils::printSafe($value) - ); - } - - throw new Error('Cannot represent value as ID: ' . Utils::printSafe($value)); + throw new Error('ID cannot represent value: ' . Utils::printSafe($value)); } /** diff --git a/src/Type/Definition/IntType.php b/src/Type/Definition/IntType.php index df8e210c1..1fb96a468 100644 --- a/src/Type/Definition/IntType.php +++ b/src/Type/Definition/IntType.php @@ -10,9 +10,12 @@ use GraphQL\Language\AST\Node; use GraphQL\Utils\Utils; use function floatval; +use function floor; use function intval; use function is_array; use function is_bool; +use function is_float; +use function is_int; use function is_numeric; use function sprintf; @@ -43,53 +46,28 @@ class IntType extends ScalarType */ public function serialize($value) { - return $this->coerceInt($value); - } - - /** - * @param mixed $value - * - * @return int - */ - private function coerceInt($value) - { - if (is_array($value)) { - throw new Error( - sprintf('Int cannot represent an array value: %s', Utils::printSafe($value)) - ); + // Fast path for 90+% of cases: + if (is_int($value) && $value <= self::MAX_INT && $value >= self::MIN_INT) { + return $value; } - if ($value === '') { - throw new Error( - 'Int cannot represent non-integer value: (empty string)' - ); - } + $float = is_numeric($value) || is_bool($value) ? floatval($value) : null; - if (! is_numeric($value) && ! is_bool($value)) { + if ($float === null || floor($float) !== $float) { throw new Error( 'Int cannot represent non-integer value: ' . Utils::printSafe($value) ); } - $num = floatval($value); - if ($num > self::MAX_INT || $num < self::MIN_INT) { + if ($float > self::MAX_INT || $float < self::MIN_INT) { throw new Error( 'Int cannot represent non 32-bit signed integer value: ' . Utils::printSafe($value) ); } - $int = intval($num); - // int cast with == used for performance reasons - // phpcs:ignore - if ($int != $num) { - throw new Error( - 'Int cannot represent non-integer value: ' . - Utils::printSafe($value) - ); - } - return $int; + return intval($float); } /** @@ -101,7 +79,23 @@ private function coerceInt($value) */ public function parseValue($value) { - return $this->coerceInt($value); + $isInt = is_int($value) || (is_float($value) && floor($value) === $value); + + if (! $isInt) { + throw new Error( + 'Int cannot represent non-integer value: ' . + Utils::printSafe($value) + ); + } + + if ($value > self::MAX_INT || $value < self::MIN_INT) { + throw new Error( + 'Int cannot represent non 32-bit signed integer value: ' . + Utils::printSafe($value) + ); + } + + return intval($value); } /** diff --git a/src/Type/Definition/StringType.php b/src/Type/Definition/StringType.php index 79f67e113..7eafd0107 100644 --- a/src/Type/Definition/StringType.php +++ b/src/Type/Definition/StringType.php @@ -12,6 +12,7 @@ use function is_array; use function is_object; use function is_scalar; +use function is_string; use function method_exists; class StringType extends ScalarType @@ -34,31 +35,13 @@ class StringType extends ScalarType */ public function serialize($value) { - return $this->coerceString($value); - } + $canCast = is_scalar($value) + || (is_object($value) && method_exists($value, '__toString')) + || $value === null; - private function coerceString($value) - { - if ($value === true) { - return 'true'; - } - if ($value === false) { - return 'false'; - } - if ($value === null) { - return 'null'; - } - if (is_array($value)) { + if (! $canCast) { throw new Error( - 'String cannot represent an array value: ' . Utils::printSafe($value) - ); - } - if (is_object($value) && method_exists($value, '__toString')) { - return (string) $value; - } - if (! is_scalar($value)) { - throw new Error( - 'String cannot represent non scalar value: ' . Utils::printSafe($value) + 'String cannot represent value: ' . Utils::printSafe($value) ); } @@ -74,7 +57,13 @@ private function coerceString($value) */ public function parseValue($value) { - return $this->coerceString($value); + if (! is_string($value)) { + throw new Error( + 'String cannot represent a non string value: ' . Utils::printSafe($value) + ); + } + + return $value; } /** diff --git a/tests/Executor/ExecutorSchemaTest.php b/tests/Executor/ExecutorSchemaTest.php index ae5a3c180..42b5dc2e6 100644 --- a/tests/Executor/ExecutorSchemaTest.php +++ b/tests/Executor/ExecutorSchemaTest.php @@ -198,7 +198,7 @@ public function testExecutesUsingASchema() : void 'isPublished' => true, 'title' => 'My Article 1', 'body' => 'This is a post', - 'keywords' => ['foo', 'bar', '1', 'true', null], + 'keywords' => ['foo', 'bar', '1', '1', null], ], ], 'meta' => [ 'title' => 'My Article 1 | My Blog' ], diff --git a/tests/Executor/VariablesTest.php b/tests/Executor/VariablesTest.php index 8927a76d1..8c1750529 100644 --- a/tests/Executor/VariablesTest.php +++ b/tests/Executor/VariablesTest.php @@ -705,7 +705,7 @@ public function testReportsErrorForArrayPassedIntoStringInput() : void 'errors' => [[ 'message' => 'Variable "$value" got invalid value [1,2,3]; Expected type ' . - 'String; String cannot represent an array value: [1,2,3]', + 'String; String cannot represent a non string value: [1,2,3]', 'locations' => [ ['line' => 2, 'column' => 31], ], diff --git a/tests/Type/ScalarSerializationTest.php b/tests/Type/ScalarSerializationTest.php index 5310afdd9..f5d9ec546 100644 --- a/tests/Type/ScalarSerializationTest.php +++ b/tests/Type/ScalarSerializationTest.php @@ -5,14 +5,13 @@ namespace GraphQL\Tests\Type; use GraphQL\Error\Error; -use GraphQL\Type\Definition\IDType; -use GraphQL\Type\Definition\StringType; +use GraphQL\Tests\Type\TestClasses\CanCastToString; +use GraphQL\Tests\Type\TestClasses\ObjectIdStub; use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; use stdClass; use function acos; use function log; -use function sprintf; class ScalarSerializationTest extends TestCase { @@ -34,114 +33,38 @@ public function testSerializesOutputAsInt() : void self::assertSame(1, $intType->serialize(true)); } - public function testSerializesOutputIntCannotRepresentFloat1() : void + public function badIntValues() { - // The GraphQL specification does not allow serializing non-integer values - // as Int to avoid accidental data loss. - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: 0.1'); - $intType->serialize(0.1); - } - - public function testSerializesOutputIntCannotRepresentFloat2() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: 1.1'); - $intType->serialize(1.1); - } - - public function testSerializesOutputIntCannotRepresentNegativeFloat() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: -1.1'); - $intType->serialize(-1.1); - } - - public function testSerializesOutputIntCannotRepresentNumericString() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: -1.1'); - $intType->serialize('-1.1'); - } - - public function testSerializesOutputIntCannotRepresentBiggerThan32Bits() : void - { - // Maybe a safe PHP int, but bigger than 2^32, so not - // representable as a GraphQL Int - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: 9876504321'); - $intType->serialize(9876504321); - } - - public function testSerializesOutputIntCannotRepresentLowerThan32Bits() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: -9876504321'); - $intType->serialize(-9876504321); - } - - public function testSerializesOutputIntCannotRepresentBiggerThanSigned32Bits() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: 1.0E+100'); - $intType->serialize(1e100); - } - - public function testSerializesOutputIntCannotRepresentLowerThanSigned32Bits() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: -1.0E+100'); - $intType->serialize(-1e100); - } - - public function testSerializesOutputIntCannotRepresentInfinity() : void - { - $intType = Type::int(); - $infinity = log(0); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: -INF'); - $intType->serialize($infinity); - } - - public function testSerializesOutputIntCannotRepresentNaN() : void - { - $intType = Type::int(); - $nan = acos(8); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: NAN'); - $intType->serialize($nan); - } - - public function testSerializesOutputIntCannotRepresentString() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: one'); - $intType->serialize('one'); - } - - public function testSerializesOutputIntCannotRepresentEmptyString() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: (empty string)'); - $intType->serialize(''); + return [ + [0.1, 'Int cannot represent non-integer value: 0.1'], + [1.1, 'Int cannot represent non-integer value: 1.1'], + [-1.1, 'Int cannot represent non-integer value: -1.1'], + ['-1.1', 'Int cannot represent non-integer value: -1.1'], + [9876504321, 'Int cannot represent non 32-bit signed integer value: 9876504321'], + [-9876504321, 'Int cannot represent non 32-bit signed integer value: -9876504321'], + [1e100, 'Int cannot represent non 32-bit signed integer value: 1.0E+100'], + [-1e100, 'Int cannot represent non 32-bit signed integer value: -1.0E+100'], + [log(0), 'Int cannot represent non 32-bit signed integer value: -INF'], + [acos(8), 'Int cannot represent non-integer value: NAN'], + ['one', 'Int cannot represent non-integer value: one'], + ['', 'Int cannot represent non-integer value: (empty string)'], + [[5], 'Int cannot represent non-integer value: [5]'], + ]; } - public function testSerializesOutputIntCannotRepresentArray() : void + /** + * @throws Error + * + * @dataProvider badIntValues + */ + public function testSerializesOutputAsIntErrors($value, $expectedError) : void { + // The GraphQL specification does not allow serializing non-integer values + // as Int to avoid accidental data loss. $intType = Type::int(); $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent an array value: [5]'); - $intType->serialize([5]); + $this->expectExceptionMessage($expectedError); + $intType->serialize($value); } /** @@ -163,112 +86,64 @@ public function testSerializesOutputAsFloat() : void self::assertSame(1.0, $floatType->serialize(true)); } - public function testSerializesOutputFloatCannotRepresentString() : void - { - $floatType = Type::float(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent non numeric value: one'); - $floatType->serialize('one'); - } - - public function testSerializesOutputFloatCannotRepresentEmptyString() : void - { - $floatType = Type::float(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent non numeric value: (empty string)'); - $floatType->serialize(''); - } - - public function testSerializesOutputFloatCannotRepresentInfinity() : void - { - $floatType = Type::float(); - $infinity = log(0); - $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent non numeric value: -INF'); - $floatType->serialize($infinity); - } - - public function testSerializesOutputFloatCannotRepresentNaN() : void + public function badFloatValues() { - $floatType = Type::float(); - $nan = acos(8); - $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent non numeric value: NAN'); - $floatType->serialize($nan); + return [ + ['one', 'Float cannot represent non numeric value: one'], + ['', 'Float cannot represent non numeric value: (empty string)'], + [log(0), 'Float cannot represent non numeric value: -INF'], + [acos(8), 'Float cannot represent non numeric value: NAN'], + [[5], 'Float cannot represent non numeric value: [5]'], + ]; } - public function testSerializesOutputFloatCannotRepresentArray() : void + /** + * @throws Error + * + * @dataProvider badFloatValues + */ + public function testSerializesOutputFloatErrors($value, $expectedError) : void { $floatType = Type::float(); $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent an array value: [5]'); - $floatType->serialize([5]); - } - - public function stringLikeTypes() - { - return [ - [ Type::string() ], - [ Type::id() ], - ]; + $this->expectExceptionMessage($expectedError); + $floatType->serialize($value); } /** * @see it('serializes output as String') - * - * @param StringType|IDType $stringType - * - * @dataProvider stringLikeTypes */ - public function testSerializesOutputAsString($stringType) : void + public function testSerializesOutputAsString() : void { + $stringType = Type::string(); self::assertSame('string', $stringType->serialize('string')); self::assertSame('1', $stringType->serialize(1)); self::assertSame('-1.1', $stringType->serialize(-1.1)); - self::assertSame('true', $stringType->serialize(true)); - self::assertSame('false', $stringType->serialize(false)); - self::assertSame('null', $stringType->serialize(null)); - self::assertSame('2', $stringType->serialize(new ObjectIdStub(2))); - } - - /** - * @param StringType|IDType $stringType - * - * @throws Error - * - * @dataProvider stringLikeTypes - */ - public function testSerializesOutputStringsCannotRepresentArray($stringType) : void - { - $this->expectException(Error::class); - $this->expectExceptionMessage(sprintf('%s cannot represent an array value: [1]', $stringType->name)); - $stringType->serialize([1]); + self::assertSame('1', $stringType->serialize(true)); + self::assertSame('', $stringType->serialize(false)); + self::assertSame('', $stringType->serialize(null)); + self::assertSame('foo', $stringType->serialize(new CanCastToString('foo'))); } - /** - * @param StringType|IDType $stringType - * - * @dataProvider stringLikeTypes - */ - public function testSerializesOutputStringsCannotRepresentObject($stringType) : void + public function badStringValues() { - $this->expectException(Error::class); - $this->expectExceptionMessage(sprintf('%s cannot represent non scalar value: instance of stdClass', $stringType->name)); - $stringType->serialize(new stdClass()); + return [ + [[1], 'String cannot represent value: [1]'], + [new stdClass(), 'String cannot represent value: instance of stdClass'], + ]; } /** - * @param StringType|IDType $stringType - * * @throws Error * - * @dataProvider stringLikeTypes + * @dataProvider badStringValues */ - public function testSerializesOutputStringCannotRepresentArray($stringType) : void + public function testSerializesOutputStringErrors($value, $expectedError) : void { + $stringType = Type::string(); $this->expectException(Error::class); - $this->expectExceptionMessage(sprintf('%s cannot represent an array value: [5]', $stringType->name)); - $stringType->serialize([5]); + $this->expectExceptionMessage($expectedError); + $stringType->serialize($value); } /** @@ -289,11 +164,42 @@ public function testSerializesOutputAsBoolean() : void self::assertFalse($boolType->serialize('')); } - public function testSerializesOutputBooleanCannotRepresentArray() : void + /** + * @see it('serializes output as ID') + */ + public function testSerializesOutputAsID() : void { - $boolType = Type::boolean(); + $idType = Type::id(); + + self::assertSame('string', $idType->serialize('string')); + self::assertSame('false', $idType->serialize('false')); + self::assertSame('', $idType->serialize('')); + self::assertSame('1', $idType->serialize('1')); + self::assertSame('0', $idType->serialize('0')); + self::assertSame('1', $idType->serialize(1)); + self::assertSame('0', $idType->serialize(0)); + self::assertSame('2', $idType->serialize(new ObjectIdStub(2))); + } + + public function badIDValues() + { + return [ + [new stdClass(), 'ID cannot represent value: instance of stdClass'], + [true, 'ID cannot represent value: true'], + [false, 'ID cannot represent value: false'], + [-1.1, 'ID cannot represent value: -1.1'], + [['abc'], 'ID cannot represent value: ["abc"]'], + ]; + } + + /** + * @dataProvider badIDValues + */ + public function testSerializesOutputAsIDError($value, $expectedError) + { + $idType = Type::id(); $this->expectException(Error::class); - $this->expectExceptionMessage('Boolean cannot represent an array value: [5]'); - $boolType->serialize([5]); + $this->expectExceptionMessage($expectedError); + $idType->serialize($value); } } diff --git a/tests/Type/TestClasses/CanCastToString.php b/tests/Type/TestClasses/CanCastToString.php new file mode 100644 index 000000000..017b485f6 --- /dev/null +++ b/tests/Type/TestClasses/CanCastToString.php @@ -0,0 +1,21 @@ +str = $str; + } + + public function __toString() + { + return $this->str; + } +} diff --git a/tests/Type/ObjectIdStub.php b/tests/Type/TestClasses/ObjectIdStub.php similarity index 87% rename from tests/Type/ObjectIdStub.php rename to tests/Type/TestClasses/ObjectIdStub.php index 3049569f4..a6c2b64e7 100644 --- a/tests/Type/ObjectIdStub.php +++ b/tests/Type/TestClasses/ObjectIdStub.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace GraphQL\Tests\Type; +namespace GraphQL\Tests\Type\TestClasses; class ObjectIdStub { diff --git a/tests/Utils/AstFromValueTest.php b/tests/Utils/AstFromValueTest.php index 02e758289..86b62e00e 100644 --- a/tests/Utils/AstFromValueTest.php +++ b/tests/Utils/AstFromValueTest.php @@ -101,7 +101,7 @@ public function testConvertsStringValuesToASTs() : void self::assertEquals(new StringValueNode(['value' => 'VALUE']), AST::astFromValue('VALUE', Type::string())); self::assertEquals(new StringValueNode(['value' => "VA\nLUE"]), AST::astFromValue("VA\nLUE", Type::string())); self::assertEquals(new StringValueNode(['value' => '123']), AST::astFromValue(123, Type::string())); - self::assertEquals(new StringValueNode(['value' => 'false']), AST::astFromValue(false, Type::string())); + self::assertEquals(new StringValueNode(['value' => '']), AST::astFromValue(false, Type::string())); self::assertEquals(new NullValueNode([]), AST::astFromValue(null, Type::string())); self::assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::string()))); } @@ -118,7 +118,6 @@ public function testConvertIdValuesToIntOrStringASTs() : void self::assertEquals(new IntValueNode(['value' => '123']), AST::astFromValue(123, Type::id())); self::assertEquals(new IntValueNode(['value' => '123']), AST::astFromValue('123', Type::id())); self::assertEquals(new StringValueNode(['value' => '01']), AST::astFromValue('01', Type::id())); - self::assertEquals(new StringValueNode(['value' => 'false']), AST::astFromValue(false, Type::id())); self::assertEquals(new NullValueNode([]), AST::astFromValue(null, Type::id())); self::assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::id()))); } diff --git a/tests/Utils/CoerceValueTest.php b/tests/Utils/CoerceValueTest.php index 6b04eefc9..0cc92241e 100644 --- a/tests/Utils/CoerceValueTest.php +++ b/tests/Utils/CoerceValueTest.php @@ -44,37 +44,44 @@ public function setUp() ]); } - public function stringLikeTypes() + /** + * Describe: coerceValue + */ + + /** + * Describe: for GraphQLString + * + * @see it('returns error for array input as string') + */ + public function testCoercingAnArrayToGraphQLStringProducesAnError() : void { - return [ - [Type::string()], - [Type::id()], - ]; + $result = Value::coerceValue([1, 2, 3], Type::string()); + $this->expectError( + $result, + 'Expected type String; String cannot represent a non string value: [1,2,3]' + ); + + self::assertEquals( + 'String cannot represent a non string value: [1,2,3]', + $result['errors'][0]->getPrevious()->getMessage() + ); } /** - * Describe: coerceValue + * Describe: for GraphQLID * * @see it('returns error for array input as string') - * - * @param StringType|IDType $type - * - * @dataProvider stringLikeTypes */ - public function testCoercingAnArrayToGraphQLStringProducesAnError($type) : void + public function testCoercingAnArrayToGraphQLIDProducesAnError() : void { - $result = Value::coerceValue([1, 2, 3], $type); + $result = Value::coerceValue([1, 2, 3], Type::id()); $this->expectError( $result, - sprintf( - 'Expected type %s; %s cannot represent an array value: [1,2,3]', - $type->name, - $type->name - ) + 'Expected type ID; ID cannot represent value: [1,2,3]' ); self::assertEquals( - sprintf('%s cannot represent an array value: [1,2,3]', $type->name), + 'ID cannot represent value: [1,2,3]', $result['errors'][0]->getPrevious()->getMessage() ); } @@ -92,42 +99,51 @@ private function expectError($result, $expected) } /** - * @see it('returns no error for int input') + * @see it('returns value for integer') */ public function testIntReturnsNoErrorForIntInput() : void { - $result = Value::coerceValue('1', Type::int()); + $result = Value::coerceValue(1, Type::int()); $this->expectValue($result, 1); } + /** + * @see it('returns error for numeric looking string') + */ + public function testReturnsErrorForNumericLookingString() + { + $result = Value::coerceValue('1', Type::int()); + $this->expectError($result, 'Expected type Int; Int cannot represent non-integer value: 1'); + } + private function expectValue($result, $expected) { self::assertInternalType('array', $result); - self::assertNull($result['errors']); + self::assertEquals(null, $result['errors']); self::assertNotEquals(Utils::undefined(), $result['value']); self::assertEquals($expected, $result['value']); } /** - * @see it('returns no error for negative int input') + * @see it('returns value for negative int input') */ public function testIntReturnsNoErrorForNegativeIntInput() : void { - $result = Value::coerceValue('-1', Type::int()); + $result = Value::coerceValue(-1, Type::int()); $this->expectValue($result, -1); } /** - * @see it('returns no error for exponent input') + * @see it('returns value for exponent input') */ public function testIntReturnsNoErrorForExponentInput() : void { - $result = Value::coerceValue('1e3', Type::int()); + $result = Value::coerceValue(1e3, Type::int()); $this->expectValue($result, 1000); } /** - * @see it('returns no error for null') + * @see it('returns null for null value') */ public function testIntReturnsASingleErrorNull() : void { @@ -164,7 +180,7 @@ public function testReturnsASingleErrorFor2x32InputAsInt() */ public function testIntReturnsErrorForFloatInputAsInt() : void { - $result = Value::coerceValue('1.5', Type::int()); + $result = Value::coerceValue(1.5, Type::int()); $this->expectError( $result, 'Expected type Int; Int cannot represent non-integer value: 1.5' @@ -194,10 +210,8 @@ public function testReturnsASingleErrorForNaNInputAsInt() ); } - // Describe: for GraphQLFloat - /** - * @see it('returns a single error for char input') + * @see it('returns a single error for string input') */ public function testIntReturnsASingleErrorForCharInput() : void { @@ -220,35 +234,49 @@ public function testIntReturnsASingleErrorForMultiCharInput() : void ); } + // Describe: for GraphQLFloat + /** - * @see it('returns no error for int input') + * @see it('returns value for integer') */ public function testFloatReturnsNoErrorForIntInput() : void { - $result = Value::coerceValue('1', Type::float()); + $result = Value::coerceValue(1, Type::float()); $this->expectValue($result, 1); } /** - * @see it('returns no error for exponent input') + * @see it('returns value for decimal') + */ + public function testReturnsValueForDecimal() + { + $result = Value::coerceValue(1.1, Type::float()); + $this->expectValue($result, 1.1); + } + + /** + * @see it('returns value for exponent input') */ public function testFloatReturnsNoErrorForExponentInput() : void { - $result = Value::coerceValue('1e3', Type::float()); + $result = Value::coerceValue(1e3, Type::float()); $this->expectValue($result, 1000); } /** - * @see it('returns no error for float input') + * @see it('returns error for numeric looking string') */ - public function testFloatReturnsNoErrorForFloatInput() : void + public function testFloatReturnsErrorForNumericLookingString() { - $result = Value::coerceValue('1.5', Type::float()); - $this->expectValue($result, 1.5); + $result = Value::coerceValue('1', Type::float()); + $this->expectError( + $result, + 'Expected type Float; Float cannot represent non numeric value: 1' + ); } /** - * @see it('returns no error for null') + * @see it('returns null for null value') */ public function testFloatReturnsASingleErrorNull() : void {