From 58e0c7a178154203c7aec9108102805ae8db23c2 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Thu, 15 Feb 2018 21:29:14 +0100 Subject: [PATCH] Validate literals in a single rule with finer precision This generalizes the "arguments of correct type" and "default values of correct type" to a single rule "values of correct type" which has been re-written to rely on a traversal rather than the utility function `isValidLiteralValue`. To reduce breaking scope, this does not remove that utility even though it's no longer used directly within the library. Since the default values rule included another validation rule that rule was renamed to a more apt "variable default value allowed". This also includes the original errors from custom scalars in the validation error output, solving the remainder of graphql/graphql-js#821. ref: graphql/graphql-js#1144 --- src/Executor/Values.php | 10 +- src/Utils/TypeInfo.php | 36 +- src/Validator/DocumentValidator.php | 142 +----- .../Rules/ArgumentsOfCorrectType.php | 39 -- .../Rules/DefaultValuesOfCorrectType.php | 59 --- src/Validator/Rules/ValuesOfCorrectType.php | 226 +++++++++ .../Rules/VariablesDefaultValueAllowed.php | 60 +++ src/Validator/ValidationContext.php | 10 +- tests/Executor/VariablesTest.php | 10 +- tests/Type/EnumTypeTest.php | 24 +- tests/Utils/IsValidLiteralValueTest.php | 37 ++ .../DefaultValuesOfCorrectTypeTest.php | 188 -------- tests/Validator/TestCase.php | 24 +- tests/Validator/ValidationTest.php | 7 +- ...peTest.php => ValuesOfCorrectTypeTest.php} | 446 ++++++++++++------ .../VariablesDefaultValueAllowedTest.php | 109 +++++ 16 files changed, 834 insertions(+), 593 deletions(-) delete mode 100644 src/Validator/Rules/ArgumentsOfCorrectType.php delete mode 100644 src/Validator/Rules/DefaultValuesOfCorrectType.php create mode 100644 src/Validator/Rules/ValuesOfCorrectType.php create mode 100644 src/Validator/Rules/VariablesDefaultValueAllowed.php create mode 100644 tests/Utils/IsValidLiteralValueTest.php delete mode 100644 tests/Validator/DefaultValuesOfCorrectTypeTest.php rename tests/Validator/{ArgumentsOfCorrectTypeTest.php => ValuesOfCorrectTypeTest.php} (58%) create mode 100644 tests/Validator/VariablesDefaultValueAllowedTest.php diff --git a/src/Executor/Values.php b/src/Executor/Values.php index ef6a8cf..c8353c4 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -115,7 +115,6 @@ class Values } $coercedValues = []; - $undefined = Utils::undefined(); /** @var ArgumentNode[] $argNodeMap */ $argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) { @@ -158,11 +157,12 @@ class Values } else { $valueNode = $argumentNode->value; $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues); - if ($coercedValue === $undefined) { - $errors = DocumentValidator::isValidLiteralValue($argType, $valueNode); - $message = !empty($errors) ? ("\n" . implode("\n", $errors)) : ''; + if (Utils::isInvalid($coercedValue)) { + // Note: ValuesOfCorrectType validation should catch this before + // execution. This is a runtime check to ensure execution does not + // continue with an invalid argument value. throw new Error( - 'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message, + 'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.', [ $argumentNode->value ] ); } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 843a433..a211c5a 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -4,7 +4,6 @@ namespace GraphQL\Utils; use GraphQL\Error\InvariantViolation; use GraphQL\Error\Warning; use GraphQL\Language\AST\FieldNode; -use GraphQL\Language\AST\ListType; use GraphQL\Language\AST\ListTypeNode; use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\Node; @@ -20,7 +19,6 @@ use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; -use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; @@ -217,14 +215,26 @@ class TypeInfo /** * TypeInfo constructor. * @param Schema $schema + * @param Type|null $initialType */ - public function __construct(Schema $schema) + public function __construct(Schema $schema, $initialType = null) { $this->schema = $schema; $this->typeStack = []; $this->parentTypeStack = []; $this->inputTypeStack = []; $this->fieldDefStack = []; + if ($initialType) { + if (Type::isInputType($initialType)) { + $this->inputTypeStack[] = $initialType; + } + if (Type::isCompositeType($initialType)) { + $this->parentTypeStack[] = $initialType; + } + if (Type::isOutputType($initialType)) { + $this->typeStack[] = $initialType; + } + } } /** @@ -239,7 +249,7 @@ class TypeInfo } /** - * @return Type + * @return CompositeType */ function getParentType() { @@ -260,6 +270,17 @@ class TypeInfo return null; } + /** + * @return InputType|null + */ + public function getParentInputType() + { + $inputTypeStackLength = count($this->inputTypeStack); + if ($inputTypeStackLength > 1) { + return $this->inputTypeStack[$inputTypeStackLength - 2]; + } + } + /** * @return FieldDefinition */ @@ -369,10 +390,9 @@ class TypeInfo case NodeKind::LST: $listType = Type::getNullableType($this->getInputType()); - $itemType = null; - if ($itemType instanceof ListType) { - $itemType = $listType->getWrappedType(); - } + $itemType = $listType instanceof ListOfType + ? $listType->getWrappedType() + : $listType; $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null; break; diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 1a454f7..3efd992 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -2,26 +2,13 @@ namespace GraphQL\Validator; use GraphQL\Error\Error; -use GraphQL\Language\AST\EnumValueNode; -use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\DocumentNode; -use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\AST\NullValueNode; -use GraphQL\Language\AST\VariableNode; -use GraphQL\Language\Printer; use GraphQL\Language\Visitor; use GraphQL\Type\Schema; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\ListOfType; -use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ScalarType; -use GraphQL\Utils\Utils; use GraphQL\Utils\TypeInfo; use GraphQL\Validator\Rules\AbstractValidationRule; -use GraphQL\Validator\Rules\ArgumentsOfCorrectType; -use GraphQL\Validator\Rules\DefaultValuesOfCorrectType; +use GraphQL\Validator\Rules\ValuesOfCorrectType; use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\ExecutableDefinitions; use GraphQL\Validator\Rules\FieldsOnCorrectType; @@ -48,6 +35,7 @@ use GraphQL\Validator\Rules\UniqueInputFieldNames; use GraphQL\Validator\Rules\UniqueOperationNames; use GraphQL\Validator\Rules\UniqueVariableNames; use GraphQL\Validator\Rules\VariablesAreInputTypes; +use GraphQL\Validator\Rules\VariablesDefaultValueAllowed; use GraphQL\Validator\Rules\VariablesInAllowedPosition; /** @@ -144,9 +132,9 @@ class DocumentValidator UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), KnownArgumentNames::class => new KnownArgumentNames(), UniqueArgumentNames::class => new UniqueArgumentNames(), - ArgumentsOfCorrectType::class => new ArgumentsOfCorrectType(), + ValuesOfCorrectType::class => new ValuesOfCorrectType(), ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), - DefaultValuesOfCorrectType::class => new DefaultValuesOfCorrectType(), + VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(), VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), UniqueInputFieldNames::class => new UniqueInputFieldNames(), @@ -226,121 +214,23 @@ class DocumentValidator } /** - * Utility for validators which determines if a value literal AST is valid given - * an input type. + * Utility which determines if a value literal node is valid for an input type. * - * Note that this only validates literal values, variables are assumed to - * provide values of the correct type. + * Deprecated. Rely on validation for documents containing literal values. * - * @return array + * @deprecated + * @return Error[] */ public static function isValidLiteralValue(Type $type, $valueNode) { - // A value must be provided if the type is non-null. - if ($type instanceof NonNull) { - if (!$valueNode || $valueNode instanceof NullValueNode) { - return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ]; - } - return static::isValidLiteralValue($type->getWrappedType(), $valueNode); - } - - if (!$valueNode || $valueNode instanceof NullValueNode) { - return []; - } - - // This function only tests literals, and assumes variables will provide - // values of the correct type. - if ($valueNode instanceof VariableNode) { - return []; - } - - // Lists accept a non-list value as a list of one. - if ($type instanceof ListOfType) { - $itemType = $type->getWrappedType(); - if ($valueNode instanceof ListValueNode) { - $errors = []; - foreach($valueNode->values as $index => $itemNode) { - $tmp = static::isValidLiteralValue($itemType, $itemNode); - - if ($tmp) { - $errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) { - return "In element #$index: $error"; - })); - } - } - return $errors; - } - - return static::isValidLiteralValue($itemType, $valueNode); - } - - // Input objects check each defined field and look for undefined fields. - if ($type instanceof InputObjectType) { - if ($valueNode->kind !== NodeKind::OBJECT) { - return [ "Expected \"{$type->name}\", found not an object." ]; - } - - $fields = $type->getFields(); - - $errors = []; - - // Ensure every provided field is defined. - $fieldNodes = $valueNode->fields; - - foreach ($fieldNodes as $providedFieldNode) { - if (empty($fields[$providedFieldNode->name->value])) { - $errors[] = "In field \"{$providedFieldNode->name->value}\": Unknown field."; - } - } - - // Ensure every defined field is valid. - $fieldNodeMap = Utils::keyMap($fieldNodes, function($fieldNode) {return $fieldNode->name->value;}); - foreach ($fields as $fieldName => $field) { - $result = static::isValidLiteralValue( - $field->getType(), - isset($fieldNodeMap[$fieldName]) ? $fieldNodeMap[$fieldName]->value : null - ); - if ($result) { - $errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) { - return "In field \"$fieldName\": $error"; - })); - } - } - - return $errors; - } - - if ($type instanceof EnumType) { - if (!$valueNode instanceof EnumValueNode || !$type->getValue($valueNode->value)) { - $printed = Printer::doPrint($valueNode); - return ["Expected type \"{$type->name}\", found $printed."]; - } - - return []; - } - - if ($type instanceof ScalarType) { - // Scalars determine if a literal values is valid via parseLiteral(). - try { - $parseResult = $type->parseLiteral($valueNode); - if (Utils::isInvalid($parseResult)) { - $printed = Printer::doPrint($valueNode); - return ["Expected type \"{$type->name}\", found $printed."]; - } - } catch (\Exception $error) { - $printed = Printer::doPrint($valueNode); - $message = $error->getMessage(); - return ["Expected type \"{$type->name}\", found $printed; $message"]; - } catch (\Throwable $error) { - $printed = Printer::doPrint($valueNode); - $message = $error->getMessage(); - return ["Expected type \"{$type->name}\", found $printed; $message"]; - } - - return []; - } - - throw new Error('Unknown type: ' . Utils::printSafe($type) . '.'); + $emptySchema = new Schema([]); + $emptyDoc = new DocumentNode(['definitions' => []]); + $typeInfo = new TypeInfo($emptySchema, $type); + $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo); + $validator = new ValuesOfCorrectType(); + $visitor = $validator->getVisitor($context); + Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor)); + return $context->getErrors(); } /** diff --git a/src/Validator/Rules/ArgumentsOfCorrectType.php b/src/Validator/Rules/ArgumentsOfCorrectType.php deleted file mode 100644 index 3e37322..0000000 --- a/src/Validator/Rules/ArgumentsOfCorrectType.php +++ /dev/null @@ -1,39 +0,0 @@ - function(ArgumentNode $argNode) use ($context) { - $argDef = $context->getArgument(); - if ($argDef) { - $errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argNode->value); - - if (!empty($errors)) { - $context->reportError(new Error( - self::badValueMessage($argNode->name->value, $argDef->getType(), Printer::doPrint($argNode->value), $errors), - [$argNode->value] - )); - } - } - return Visitor::skipNode(); - } - ]; - } -} diff --git a/src/Validator/Rules/DefaultValuesOfCorrectType.php b/src/Validator/Rules/DefaultValuesOfCorrectType.php deleted file mode 100644 index 792acd7..0000000 --- a/src/Validator/Rules/DefaultValuesOfCorrectType.php +++ /dev/null @@ -1,59 +0,0 @@ - function(VariableDefinitionNode $varDefNode) use ($context) { - $name = $varDefNode->variable->name->value; - $defaultValue = $varDefNode->defaultValue; - $type = $context->getInputType(); - - if ($type instanceof NonNull && $defaultValue) { - $context->reportError(new Error( - static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()), - [$defaultValue] - )); - } - if ($type && $defaultValue) { - $errors = DocumentValidator::isValidLiteralValue($type, $defaultValue); - if (!empty($errors)) { - $context->reportError(new Error( - static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors), - [$defaultValue] - )); - } - } - return Visitor::skipNode(); - }, - NodeKind::SELECTION_SET => function() {return Visitor::skipNode();}, - NodeKind::FRAGMENT_DEFINITION => function() {return Visitor::skipNode();} - ]; - } -} diff --git a/src/Validator/Rules/ValuesOfCorrectType.php b/src/Validator/Rules/ValuesOfCorrectType.php new file mode 100644 index 0000000..a70de1f --- /dev/null +++ b/src/Validator/Rules/ValuesOfCorrectType.php @@ -0,0 +1,226 @@ + function(NullValueNode $node) use ($context) { + $type = $context->getInputType(); + if ($type instanceof NonNull) { + $context->reportError( + new Error( + self::badValueMessage((string) $type, Printer::doPrint($node)), + $node + ) + ); + } + }, + NodeKind::LST => function(ListValueNode $node) use ($context) { + // Note: TypeInfo will traverse into a list's item type, so look to the + // parent input type to check if it is a list. + $type = Type::getNullableType($context->getParentInputType()); + if (!$type instanceof ListOfType) { + $this->isValidScalar($context, $node); + return Visitor::skipNode(); + } + }, + NodeKind::OBJECT => function(ObjectValueNode $node) use ($context) { + // Note: TypeInfo will traverse into a list's item type, so look to the + // parent input type to check if it is a list. + $type = Type::getNamedType($context->getInputType()); + if (!$type instanceof InputObjectType) { + $this->isValidScalar($context, $node); + return Visitor::skipNode(); + } + // Ensure every required field exists. + $inputFields = $type->getFields(); + $nodeFields = iterator_to_array($node->fields); + $fieldNodeMap = array_combine( + array_map(function ($field) { return $field->name->value; }, $nodeFields), + array_values($nodeFields) + ); + foreach ($inputFields as $fieldName => $fieldDef) { + $fieldType = $fieldDef->getType(); + if (!isset($fieldNodeMap[$fieldName]) && $fieldType instanceof NonNull) { + $context->reportError( + new Error( + self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType), + $node + ) + ); + } + } + }, + NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) { + $parentType = Type::getNamedType($context->getParentInputType()); + $fieldType = $context->getInputType(); + if (!$fieldType && $parentType) { + $context->reportError( + new Error( + self::unknownFieldMessage($parentType->name, $node->name->value), + $node + ) + ); + } + }, + NodeKind::ENUM => function(EnumValueNode $node) use ($context) { + $type = Type::getNamedType($context->getInputType()); + if (!$type instanceof EnumType) { + $this->isValidScalar($context, $node); + } else if (!$type->getValue($node->value)) { + $context->reportError( + new Error( + self::badValueMessage( + $type->name, + Printer::doPrint($node), + $this->enumTypeSuggestion($type, $node) + ), + $node + ) + ); + } + }, + NodeKind::INT => function (IntValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, + NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, + NodeKind::STRING => function (StringValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, + NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, + ]; + } + + private function isValidScalar(ValidationContext $context, ValueNode $node) + { + // Report any error at the full type expected by the location. + $locationType = $context->getInputType(); + + if (!$locationType) { + return; + } + + $type = Type::getNamedType($locationType); + + if (!$type instanceof ScalarType) { + $suggestions = $type instanceof EnumType + ? $this->enumTypeSuggestion($type, $node) + : null; + $context->reportError( + new Error( + self::badValueMessage( + (string) $locationType, + Printer::doPrint($node), + $suggestions + ), + $node + ) + ); + return; + } + + // Scalars determine if a literal value is valid via parseLiteral() which + // may throw or return an invalid value to indicate failure. + try { + $parseResult = $type->parseLiteral($node); + if (Utils::isInvalid($parseResult)) { + $context->reportError( + new Error( + self::badValueMessage( + (string) $locationType, + Printer::doPrint($node) + ), + $node + ) + ); + } + } catch (\Exception $error) { + // Ensure a reference to the original error is maintained. + $context->reportError( + new Error( + self::badValueMessage( + (string) $locationType, + Printer::doPrint($node), + $error->getMessage() + ), + $node, + null, + null, + null, + $error + ) + ); + } catch (\Throwable $error) { + // Ensure a reference to the original error is maintained. + $context->reportError( + new Error( + self::badValueMessage( + (string) $locationType, + Printer::doPrint($node), + $error->getMessage() + ), + $node, + null, + null, + null, + $error + ) + ); + } + } + + private function enumTypeSuggestion(EnumType $type, ValueNode $node) + { + $suggestions = Utils::suggestionList( + Printer::doPrint($node), + array_map(function (EnumValueDefinition $value) { return $value->name; }, $type->getValues()) + ); + + return $suggestions ? 'Did you mean the enum value: ' . Utils::orList($suggestions) . '?' : ''; + } +} diff --git a/src/Validator/Rules/VariablesDefaultValueAllowed.php b/src/Validator/Rules/VariablesDefaultValueAllowed.php new file mode 100644 index 0000000..fcbbef4 --- /dev/null +++ b/src/Validator/Rules/VariablesDefaultValueAllowed.php @@ -0,0 +1,60 @@ + function(VariableDefinitionNode $node) use ($context) { + $name = $node->variable->name->value; + $defaultValue = $node->defaultValue; + $type = $context->getInputType(); + if ($type instanceof NonNull && $defaultValue) { + $context->reportError( + new Error( + self::defaultForRequiredVarMessage( + $name, + $type, + $type->getWrappedType() + ), + [$defaultValue] + ) + ); + } + + return Visitor::skipNode(); + }, + NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) { + return Visitor::skipNode(); + }, + NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) { + return Visitor::skipNode(); + }, + ]; + } +} diff --git a/src/Validator/ValidationContext.php b/src/Validator/ValidationContext.php index 51ea8d1..4d82ce4 100644 --- a/src/Validator/ValidationContext.php +++ b/src/Validator/ValidationContext.php @@ -12,11 +12,9 @@ use GraphQL\Error\Error; use GraphQL\Type\Schema; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\FragmentDefinitionNode; -use GraphQL\Language\AST\Node; use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputType; -use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use GraphQL\Utils\TypeInfo; @@ -275,6 +273,14 @@ class ValidationContext return $this->typeInfo->getInputType(); } + /** + * @return InputType + */ + function getParentInputType() + { + return $this->typeInfo->getParentInputType(); + } + /** * @return FieldDefinition */ diff --git a/tests/Executor/VariablesTest.php b/tests/Executor/VariablesTest.php index 0180bbd..ccb16bb 100644 --- a/tests/Executor/VariablesTest.php +++ b/tests/Executor/VariablesTest.php @@ -4,7 +4,6 @@ namespace GraphQL\Tests\Executor; require_once __DIR__ . '/TestClasses.php'; use GraphQL\Error\Error; -use GraphQL\Error\InvariantViolation; use GraphQL\Executor\Executor; use GraphQL\Language\Parser; use GraphQL\Type\Schema; @@ -82,9 +81,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $expected = [ 'data' => ['fieldWithObjectInput' => null], 'errors' => [[ - 'message' => 'Argument "input" got invalid value ["foo", "bar", "baz"].' . "\n" . - 'Expected "TestInputObject", found not an object.', - 'path' => ['fieldWithObjectInput'] + 'message' => 'Argument "input" has invalid value ["foo", "bar", "baz"].', + 'path' => ['fieldWithObjectInput'], + 'locations' => [['line' => 3, 'column' => 39]] ]] ]; $this->assertArraySubset($expected, $result); @@ -877,8 +876,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'data' => ['fieldWithDefaultArgumentValue' => null], 'errors' => [[ 'message' => - 'Argument "input" got invalid value WRONG_TYPE.' . "\n" . - 'Expected type "String", found WRONG_TYPE.', + 'Argument "input" has invalid value WRONG_TYPE.', 'locations' => [ [ 'line' => 2, 'column' => 50 ] ], 'path' => [ 'fieldWithDefaultArgumentValue' ], 'category' => 'graphql', diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php index 0761cb1..9c6910d 100644 --- a/tests/Type/EnumTypeTest.php +++ b/tests/Type/EnumTypeTest.php @@ -220,7 +220,22 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase '{ colorEnum(fromEnum: "GREEN") }', null, [ - 'message' => "Argument \"fromEnum\" has invalid value \"GREEN\".\nExpected type \"Color\", found \"GREEN\".", + 'message' => "Expected type Color, found \"GREEN\"; Did you mean the enum value: GREEN?", + 'locations' => [new SourceLocation(1, 23)] + ] + ); + } + + /** + * @it does not accept valuesNotInTheEnum + */ + public function testDoesNotAcceptValuesNotInTheEnum() + { + $this->expectFailure( + '{ colorEnum(fromEnum: GREENISH) }', + null, + [ + 'message' => "Expected type Color, found GREENISH; Did you mean the enum value: GREEN?", 'locations' => [new SourceLocation(1, 23)] ] ); @@ -236,7 +251,8 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase null, [ 'message' => 'Expected a value of type "Color" but received: GREEN', - 'locations' => [new SourceLocation(1, 3)] + 'locations' => [new SourceLocation(1, 3)], + 'path' => ['colorEnum'], ] ); } @@ -249,7 +265,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase $this->expectFailure( '{ colorEnum(fromEnum: 1) }', null, - "Argument \"fromEnum\" has invalid value 1.\nExpected type \"Color\", found 1." + "Expected type Color, found 1." ); } @@ -261,7 +277,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase $this->expectFailure( '{ colorEnum(fromInt: GREEN) }', null, - "Argument \"fromInt\" has invalid value GREEN.\nExpected type \"Int\", found GREEN." + "Expected type Int, found GREEN." ); } diff --git a/tests/Utils/IsValidLiteralValueTest.php b/tests/Utils/IsValidLiteralValueTest.php new file mode 100644 index 0000000..33b0592 --- /dev/null +++ b/tests/Utils/IsValidLiteralValueTest.php @@ -0,0 +1,37 @@ +assertEquals( + [], + DocumentValidator::isValidLiteralValue(Type::int(), Parser::parseValue('123')) + ); + } + + /** + * @it Returns errors for an invalid value + */ + public function testReturnsErrorsForForInvalidValue() + { + $errors = DocumentValidator::isValidLiteralValue(Type::int(), Parser::parseValue('"abc"')); + + $this->assertCount(1, $errors); + $this->assertEquals('Expected type Int, found "abc".', $errors[0]->getMessage()); + $this->assertEquals([new SourceLocation(1, 1)], $errors[0]->getLocations()); + $this->assertEquals(null, $errors[0]->getPath()); + } +} diff --git a/tests/Validator/DefaultValuesOfCorrectTypeTest.php b/tests/Validator/DefaultValuesOfCorrectTypeTest.php deleted file mode 100644 index a0ac412..0000000 --- a/tests/Validator/DefaultValuesOfCorrectTypeTest.php +++ /dev/null @@ -1,188 +0,0 @@ -expectPassesRule(new DefaultValuesOfCorrectType, ' - query NullableValues($a: Int, $b: String, $c: ComplexInput) { - dog { name } - } - '); - } - - /** - * @it required variables without default values - */ - public function testRequiredVariablesWithoutDefaultValues() - { - $this->expectPassesRule(new DefaultValuesOfCorrectType, ' - query RequiredValues($a: Int!, $b: String!) { - dog { name } - } - '); - } - - /** - * @it variables with valid default values - */ - public function testVariablesWithValidDefaultValues() - { - $this->expectPassesRule(new DefaultValuesOfCorrectType, ' - query WithDefaultValues( - $a: Int = 1, - $b: String = "ok", - $c: ComplexInput = { requiredField: true, intField: 3 } - ) { - dog { name } - } - '); - } - - /** - * @it variables with valid default null values - */ - public function testVariablesWithValidDefaultNullValues() - { - $this->expectPassesRule(new DefaultValuesOfCorrectType(), ' - query WithDefaultValues( - $a: Int = null, - $b: String = null, - $c: ComplexInput = { requiredField: true, intField: null } - ) { - dog { name } - } - '); - } - - /** - * @it variables with invalid default null values - */ - public function testVariablesWithInvalidDefaultNullValues() - { - $this->expectFailsRule(new DefaultValuesOfCorrectType(), ' - query WithDefaultValues( - $a: Int! = null, - $b: String! = null, - $c: ComplexInput = { requiredField: null, intField: null } - ) { - dog { name } - } - ', [ - $this->defaultForNonNullArg('a', 'Int!', 'Int', 3, 20), - $this->badValue('a', 'Int!', 'null', 3, 20, [ - 'Expected "Int!", found null.' - ]), - $this->defaultForNonNullArg('b', 'String!', 'String', 4, 23), - $this->badValue('b', 'String!', 'null', 4, 23, [ - 'Expected "String!", found null.' - ]), - $this->badValue('c', 'ComplexInput', '{requiredField: null, intField: null}', - 5, 28, [ - 'In field "requiredField": Expected "Boolean!", found null.' - ] - ), - ]); - } - - /** - * @it no required variables with default values - */ - public function testNoRequiredVariablesWithDefaultValues() - { - $this->expectFailsRule(new DefaultValuesOfCorrectType, ' - query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { - dog { name } - } - ', [ - $this->defaultForNonNullArg('a', 'Int!', 'Int', 2, 49), - $this->defaultForNonNullArg('b', 'String!', 'String', 2, 66) - ]); - } - - /** - * @it variables with invalid default values - */ - public function testVariablesWithInvalidDefaultValues() - { - $this->expectFailsRule(new DefaultValuesOfCorrectType, ' - query InvalidDefaultValues( - $a: Int = "one", - $b: String = 4, - $c: ComplexInput = "notverycomplex" - ) { - dog { name } - } - ', [ - $this->badValue('a', 'Int', '"one"', 3, 19, [ - 'Expected type "Int", found "one".' - ]), - $this->badValue('b', 'String', '4', 4, 22, [ - 'Expected type "String", found 4.' - ]), - $this->badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28, [ - 'Expected "ComplexInput", found not an object.' - ]) - ]); - } - - /** - * @it complex variables missing required field - */ - public function testComplexVariablesMissingRequiredField() - { - $this->expectFailsRule(new DefaultValuesOfCorrectType, ' - query MissingRequiredField($a: ComplexInput = {intField: 3}) { - dog { name } - } - ', [ - $this->badValue('a', 'ComplexInput', '{intField: 3}', 2, 53, [ - 'In field "requiredField": Expected "Boolean!", found null.' - ]) - ]); - } - - /** - * @it list variables with invalid item - */ - public function testListVariablesWithInvalidItem() - { - $this->expectFailsRule(new DefaultValuesOfCorrectType, ' - query InvalidItem($a: [String] = ["one", 2]) { - dog { name } - } - ', [ - $this->badValue('a', '[String]', '["one", 2]', 2, 40, [ - 'In element #1: Expected type "String", found 2.' - ]) - ]); - } - - private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column) - { - return FormattedError::create( - DefaultValuesOfCorrectType::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName), - [ new SourceLocation($line, $column) ] - ); - } - - private function badValue($varName, $typeName, $val, $line, $column, $errors = null) - { - $realErrors = !$errors ? ["Expected type \"$typeName\", found $val."] : $errors; - - return FormattedError::create( - DefaultValuesOfCorrectType::badValueForDefaultArgMessage($varName, $typeName, $val, $realErrors), - [ new SourceLocation($line, $column) ] - ); - } -} diff --git a/tests/Validator/TestCase.php b/tests/Validator/TestCase.php index 770e650..164effa 100644 --- a/tests/Validator/TestCase.php +++ b/tests/Validator/TestCase.php @@ -260,13 +260,6 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase ] ]); - $anyScalar = new CustomScalarType([ - 'name' => 'Any', - 'serialize' => function ($value) { return $value; }, - 'parseLiteral' => function ($node) { return $node; }, // Allows any value - 'parseValue' => function ($value) { return $value; }, // Allows any value - ]); - $invalidScalar = new CustomScalarType([ 'name' => 'Invalid', 'serialize' => function ($value) { @@ -280,6 +273,13 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase }, ]); + $anyScalar = new CustomScalarType([ + 'name' => 'Any', + 'serialize' => function ($value) { return $value; }, + 'parseLiteral' => function ($node) { return $node; }, // Allows any value + 'parseValue' => function ($value) { return $value; }, // Allows any value + ]); + $queryRoot = new ObjectType([ 'name' => 'QueryRoot', 'fields' => [ @@ -295,16 +295,16 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase 'dogOrHuman' => ['type' => $DogOrHuman], 'humanOrAlien' => ['type' => $HumanOrAlien], 'complicatedArgs' => ['type' => $ComplicatedArgs], - 'anyArg' => [ - 'args' => ['arg' => ['type' => $anyScalar]], - 'type' => Type::string(), - ], 'invalidArg' => [ 'args' => [ 'arg' => ['type' => $invalidScalar] ], 'type' => Type::string(), - ] + ], + 'anyArg' => [ + 'args' => ['arg' => ['type' => $anyScalar]], + 'type' => Type::string(), + ], ] ]); diff --git a/tests/Validator/ValidationTest.php b/tests/Validator/ValidationTest.php index 6cab583..51c7dbf 100644 --- a/tests/Validator/ValidationTest.php +++ b/tests/Validator/ValidationTest.php @@ -1,10 +1,6 @@ "Argument \"arg\" has invalid value \"bad value\". -Expected type \"Invalid\", found \"bad value\"; Invalid scalar is always invalid: bad value", + 'message' => "Expected type Invalid, found \"bad value\"; Invalid scalar is always invalid: bad value", 'locations' => [ ['line' => 3, 'column' => 25] ] ]; diff --git a/tests/Validator/ArgumentsOfCorrectTypeTest.php b/tests/Validator/ValuesOfCorrectTypeTest.php similarity index 58% rename from tests/Validator/ArgumentsOfCorrectTypeTest.php rename to tests/Validator/ValuesOfCorrectTypeTest.php index 1f1c9df..dd62ebd 100644 --- a/tests/Validator/ArgumentsOfCorrectTypeTest.php +++ b/tests/Validator/ValuesOfCorrectTypeTest.php @@ -3,21 +3,44 @@ namespace GraphQL\Tests\Validator; use GraphQL\Error\FormattedError; use GraphQL\Language\SourceLocation; -use GraphQL\Validator\Rules\ArgumentsOfCorrectType; +use GraphQL\Validator\Rules\ValuesOfCorrectType; -class ArgumentsOfCorrectTypeTest extends TestCase +class ValuesOfCorrectTypeTest extends TestCase { - function badValue($argName, $typeName, $value, $line, $column, $errors = null) + private function badValue($typeName, $value, $line, $column, $message = null) { - $realErrors = !$errors ? ["Expected type \"$typeName\", found $value."] : $errors; - return FormattedError::create( - ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value, $realErrors), + ValuesOfCorrectType::badValueMessage( + $typeName, + $value, + $message + ), [new SourceLocation($line, $column)] ); } - // Validate: Argument values of correct type + private function requiredField($typeName, $fieldName, $fieldTypeName, $line, $column) { + return FormattedError::create( + ValuesOfCorrectType::requiredFieldMessage( + $typeName, + $fieldName, + $fieldTypeName + ), + [new SourceLocation($line, $column)] + ); + } + + private function unknownField($typeName, $fieldName, $line, $column) { + return FormattedError::create( + ValuesOfCorrectType::unknownFieldMessage( + $typeName, + $fieldName + ), + [new SourceLocation($line, $column)] + ); + } + + // Validate: Values of correct type // Valid values /** @@ -25,7 +48,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodIntValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: 2) @@ -39,7 +62,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodNegativeIntValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: -2) @@ -53,7 +76,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodBooleanValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { booleanArgField(booleanArg: true) @@ -67,7 +90,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodStringValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { stringArgField(stringArg: "foo") @@ -81,7 +104,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodFloatValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { floatArgField(floatArg: 1.1) @@ -92,7 +115,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase public function testGoodNegativeFloatValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { floatArgField(floatArg: -1.1) @@ -106,7 +129,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIntIntoFloat() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { floatArgField(floatArg: 1) @@ -120,7 +143,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIntIntoID() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { idArgField(idArg: 1) @@ -134,7 +157,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testStringIntoID() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { idArgField(idArg: "someIdString") @@ -148,7 +171,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodEnumValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: SIT) @@ -162,7 +185,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testEnumWithNullValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { enumArgField(enumArg: NO_FUR) @@ -176,7 +199,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testNullIntoNullableType() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: null) @@ -184,7 +207,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase } '); - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { dog(a: null, b: null, c:{ requiredField: true, intField: null }) { name @@ -200,14 +223,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIntIntoString() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { stringArgField(stringArg: 1) } } ', [ - $this->badValue('stringArg', 'String', '1', 4, 39) + $this->badValue('String', '1', 4, 39) ]); } @@ -216,14 +239,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFloatIntoString() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { stringArgField(stringArg: 1.0) } } ', [ - $this->badValue('stringArg', 'String', '1.0', 4, 39) + $this->badValue('String', '1.0', 4, 39) ]); } @@ -232,14 +255,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testBooleanIntoString() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { stringArgField(stringArg: true) } } ', [ - $this->badValue('stringArg', 'String', 'true', 4, 39) + $this->badValue('String', 'true', 4, 39) ]); } @@ -248,14 +271,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testUnquotedStringIntoString() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { stringArgField(stringArg: BAR) } } ', [ - $this->badValue('stringArg', 'String', 'BAR', 4, 39) + $this->badValue('String', 'BAR', 4, 39) ]); } @@ -266,14 +289,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testStringIntoInt() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: "3") } } ', [ - $this->badValue('intArg', 'Int', '"3"', 4, 33) + $this->badValue('Int', '"3"', 4, 33) ]); } @@ -282,14 +305,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testBigIntIntoInt() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: 829384293849283498239482938) } } ', [ - $this->badValue('intArg', 'Int', '829384293849283498239482938', 4, 33) + $this->badValue('Int', '829384293849283498239482938', 4, 33) ]); } @@ -298,14 +321,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testUnquotedStringIntoInt() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: FOO) } } ', [ - $this->badValue('intArg', 'Int', 'FOO', 4, 33) + $this->badValue('Int', 'FOO', 4, 33) ]); } @@ -314,14 +337,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testSimpleFloatIntoInt() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: 3.0) } } ', [ - $this->badValue('intArg', 'Int', '3.0', 4, 33) + $this->badValue('Int', '3.0', 4, 33) ]); } @@ -330,14 +353,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFloatIntoInt() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { intArgField(intArg: 3.333) } } ', [ - $this->badValue('intArg', 'Int', '3.333', 4, 33) + $this->badValue('Int', '3.333', 4, 33) ]); } @@ -348,14 +371,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testStringIntoFloat() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { floatArgField(floatArg: "3.333") } } ', [ - $this->badValue('floatArg', 'Float', '"3.333"', 4, 37) + $this->badValue('Float', '"3.333"', 4, 37) ]); } @@ -364,14 +387,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testBooleanIntoFloat() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { floatArgField(floatArg: true) } } ', [ - $this->badValue('floatArg', 'Float', 'true', 4, 37) + $this->badValue('Float', 'true', 4, 37) ]); } @@ -380,14 +403,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testUnquotedIntoFloat() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { floatArgField(floatArg: FOO) } } ', [ - $this->badValue('floatArg', 'Float', 'FOO', 4, 37) + $this->badValue('Float', 'FOO', 4, 37) ]); } @@ -398,14 +421,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIntIntoBoolean() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { booleanArgField(booleanArg: 2) } } ', [ - $this->badValue('booleanArg', 'Boolean', '2', 4, 41) + $this->badValue('Boolean', '2', 4, 41) ]); } @@ -414,14 +437,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFloatIntoBoolean() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { booleanArgField(booleanArg: 1.0) } } ', [ - $this->badValue('booleanArg', 'Boolean', '1.0', 4, 41) + $this->badValue('Boolean', '1.0', 4, 41) ]); } @@ -430,14 +453,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testStringIntoBoolean() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { booleanArgField(booleanArg: "true") } } ', [ - $this->badValue('booleanArg', 'Boolean', '"true"', 4, 41) + $this->badValue('Boolean', '"true"', 4, 41) ]); } @@ -446,14 +469,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testUnquotedIntoBoolean() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { booleanArgField(booleanArg: TRUE) } } ', [ - $this->badValue('booleanArg', 'Boolean', 'TRUE', 4, 41) + $this->badValue('Boolean', 'TRUE', 4, 41) ]); } @@ -464,14 +487,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFloatIntoID() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { idArgField(idArg: 1.0) } } ', [ - $this->badValue('idArg', 'ID', '1.0', 4, 31) + $this->badValue('ID', '1.0', 4, 31) ]); } @@ -480,14 +503,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testBooleanIntoID() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { idArgField(idArg: true) } } ', [ - $this->badValue('idArg', 'ID', 'true', 4, 31) + $this->badValue('ID', 'true', 4, 31) ]); } @@ -496,14 +519,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testUnquotedIntoID() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { idArgField(idArg: SOMETHING) } } ', [ - $this->badValue('idArg', 'ID', 'SOMETHING', 4, 31) + $this->badValue('ID', 'SOMETHING', 4, 31) ]); } @@ -514,14 +537,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIntIntoEnum() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: 2) } } ', [ - $this->badValue('dogCommand', 'DogCommand', '2', 4, 41) + $this->badValue('DogCommand', '2', 4, 41) ]); } @@ -530,14 +553,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFloatIntoEnum() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: 1.0) } } ', [ - $this->badValue('dogCommand', 'DogCommand', '1.0', 4, 41) + $this->badValue('DogCommand', '1.0', 4, 41) ]); } @@ -546,14 +569,20 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testStringIntoEnum() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: "SIT") } } ', [ - $this->badValue('dogCommand', 'DogCommand', '"SIT"', 4, 41) + $this->badValue( + 'DogCommand', + '"SIT"', + 4, + 41, + 'Did you mean the enum value: SIT?' + ) ]); } @@ -562,14 +591,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testBooleanIntoEnum() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: true) } } ', [ - $this->badValue('dogCommand', 'DogCommand', 'true', 4, 41) + $this->badValue('DogCommand', 'true', 4, 41) ]); } @@ -578,14 +607,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testUnknownEnumValueIntoEnum() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: JUGGLE) } } ', [ - $this->badValue('dogCommand', 'DogCommand', 'JUGGLE', 4, 41) + $this->badValue('DogCommand', 'JUGGLE', 4, 41) ]); } @@ -594,14 +623,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testDifferentCaseEnumValueIntoEnum() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog { doesKnowCommand(dogCommand: sit) } } ', [ - $this->badValue('dogCommand', 'DogCommand', 'sit', 4, 41) + $this->badValue('DogCommand', 'sit', 4, 41) ]); } @@ -612,7 +641,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testGoodListValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { stringListArgField(stringListArg: ["one", null, "two"]) @@ -626,7 +655,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testEmptyListValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { stringListArgField(stringListArg: []) @@ -640,7 +669,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testNullValue() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { stringListArgField(stringListArg: null) @@ -654,7 +683,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testSingleValueIntoList() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { stringListArgField(stringListArg: "one") @@ -670,16 +699,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIncorrectItemtype() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { stringListArgField(stringListArg: ["one", 2]) } } ', [ - $this->badValue('stringListArg', '[String]', '["one", 2]', 4, 47, [ - 'In element #1: Expected type "String", found 2.' - ]), + $this->badValue('String', '2', 4, 55), ]); } @@ -688,14 +715,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testSingleValueOfIncorrectType() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { stringListArgField(stringListArg: 1) } } ', [ - $this->badValue('stringListArg', 'String', '1', 4, 47), + $this->badValue('[String]', '1', 4, 47), ]); } @@ -706,7 +733,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testArgOnOptionalArg() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { dog { isHousetrained(atOtherHomes: true) @@ -720,7 +747,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testNoArgOnOptionalArg() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { dog { isHousetrained @@ -734,7 +761,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testMultipleArgs() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleReqs(req1: 1, req2: 2) @@ -748,7 +775,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testMultipleArgsReverseOrder() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleReqs(req2: 2, req1: 1) @@ -762,7 +789,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testNoArgsOnMultipleOptional() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleOpts @@ -776,7 +803,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testOneArgOnMultipleOptional() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleOpts(opt1: 1) @@ -790,7 +817,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testSecondArgOnMultipleOptional() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleOpts(opt2: 1) @@ -804,7 +831,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testMultipleReqsOnMixedList() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4) @@ -818,7 +845,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testMultipleReqsAndOneOptOnMixedList() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4, opt1: 5) @@ -832,7 +859,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testAllReqsAndOptsOnMixedList() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) @@ -848,31 +875,31 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testIncorrectValueType() { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleReqs(req2: "two", req1: "one") } } ', [ - $this->badValue('req2', 'Int', '"two"', 4, 32), - $this->badValue('req1', 'Int', '"one"', 4, 45), + $this->badValue('Int!', '"two"', 4, 32), + $this->badValue('Int!', '"one"', 4, 45), ]); } /** - * @it Incorrect value and missing argument + * @it Incorrect value and missing argument (ProvidedNonNullArguments) */ - public function testIncorrectValueAndMissingArgument() + public function testIncorrectValueAndMissingArgumentProvidedNonNullArguments() { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleReqs(req1: "one") } } ', [ - $this->badValue('req1', 'Int', '"one"', 4, 32), + $this->badValue('Int!', '"one"', 4, 32), ]); } @@ -881,28 +908,26 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testNullValue2() { - $this->expectFailsRule(new ArgumentsOfCorrectType(), ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { multipleReqs(req1: null) } } ', [ - $this->badValue('req1', 'Int!', 'null', 4, 32, [ - 'Expected "Int!", found null.' - ]), + $this->badValue('Int!', 'null', 4, 32), ]); } - // Valid input object value + // DESCRIBE: Valid input object value /** * @it Optional arg, despite required field in type */ public function testOptionalArgDespiteRequiredFieldInType() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField @@ -916,7 +941,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testPartialObjectOnlyRequired() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { requiredField: true }) @@ -930,7 +955,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testPartialObjectRequiredFieldCanBeFalsey() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { requiredField: false }) @@ -944,7 +969,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testPartialObjectIncludingRequired() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { requiredField: true, intField: 4 }) @@ -958,7 +983,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFullObject() { - $this->expectPassesRule(new ArgumentsOfCorrectType, ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { @@ -978,7 +1003,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testFullObjectWithFieldsInDifferentOrder() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { @@ -993,23 +1018,21 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } - // Invalid input object value + // DESCRIBE: Invalid input object value /** * @it Partial object, missing required */ public function testPartialObjectMissingRequired() { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { intField: 4 }) } } ', [ - $this->badValue('complexArg', 'ComplexInput', '{intField: 4}', 4, 41, [ - 'In field "requiredField": Expected "Boolean!", found null.' - ]), + $this->requiredField('ComplexInput', 'requiredField', 'Boolean!', 4, 41), ]); } @@ -1018,7 +1041,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testPartialObjectInvalidFieldType() { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { @@ -1028,14 +1051,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue( - 'complexArg', - 'ComplexInput', - '{stringListField: ["one", 2], requiredField: true}', - 4, - 41, - [ 'In field "stringListField": In element #1: Expected type "String", found 2.' ] - ), + $this->badValue('String', '2', 5, 40), ]); } @@ -1044,7 +1060,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testPartialObjectUnknownFieldArg() { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { complicatedArgs { complexArgField(complexArg: { @@ -1054,25 +1070,60 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue( - 'complexArg', - 'ComplexInput', - '{requiredField: true, unknownField: "value"}', - 4, - 41, - [ 'In field "unknownField": Unknown field.' ] - ), + $this->unknownField('ComplexInput', 'unknownField', 6, 15), ]); } - // Directive arguments + + + /** + * @it reports original error for custom scalar which throws + */ + public function testReportsOriginalErrorForCustomScalarWhichThrows() + { + $errors = $this->expectFailsRule(new ValuesOfCorrectType, ' + { + invalidArg(arg: 123) + } + ', [ + $this->badValue( + 'Invalid', + '123', + 3, + 27, + 'Invalid scalar is always invalid: 123' + ), + ]); + + $this->assertEquals( + 'Invalid scalar is always invalid: 123', + $errors[0]->getPrevious()->getMessage() + ); + } + + /** + * @it allows custom scalar to accept complex literals + */ + public function testAllowsCustomScalarToAcceptComplexLiterals() + { + $this->expectPassesRule(new ValuesOfCorrectType, ' + { + test1: anyArg(arg: 123) + test2: anyArg(arg: "abc") + test3: anyArg(arg: [123, "abc"]) + test4: anyArg(arg: {deep: [123, "abc"]}) + } + '); + } + + // DESCRIBE: Directive arguments /** * @it with directives of valid types */ public function testWithDirectivesOfValidTypes() { - $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + $this->expectPassesRule(new ValuesOfCorrectType, ' { dog @include(if: true) { name @@ -1081,7 +1132,7 @@ class ArgumentsOfCorrectTypeTest extends TestCase name } } - '); + '); } /** @@ -1089,15 +1140,134 @@ class ArgumentsOfCorrectTypeTest extends TestCase */ public function testWithDirectiveWithIncorrectTypes() { - $this->expectFailsRule(new ArgumentsOfCorrectType, ' + $this->expectFailsRule(new ValuesOfCorrectType, ' { dog @include(if: "yes") { name @skip(if: ENUM) } } - ', [ - $this->badValue('if', 'Boolean', '"yes"', 3, 28), - $this->badValue('if', 'Boolean', 'ENUM', 4, 28), + ', [ + $this->badValue('Boolean!', '"yes"', 3, 28), + $this->badValue('Boolean!', 'ENUM', 4, 28), + ]); + } + + // DESCRIBE: Variable default values + + /** + * @it variables with valid default values + */ + public function testVariablesWithValidDefaultValues() + { + $this->expectPassesRule(new ValuesOfCorrectType, ' + query WithDefaultValues( + $a: Int = 1, + $b: String = "ok", + $c: ComplexInput = { requiredField: true, intField: 3 } + ) { + dog { name } + } + '); + } + + /** + * @it variables with valid default null values + */ + public function testVariablesWithValidDefaultNullValues() + { + $this->expectPassesRule(new ValuesOfCorrectType, ' + query WithDefaultValues( + $a: Int = null, + $b: String = null, + $c: ComplexInput = { requiredField: true, intField: null } + ) { + dog { name } + } + '); + } + + /** + * @it variables with invalid default null values + */ + public function testVariablesWithInvalidDefaultNullValues() + { + $this->expectFailsRule(new ValuesOfCorrectType, ' + query WithDefaultValues( + $a: Int! = null, + $b: String! = null, + $c: ComplexInput = { requiredField: null, intField: null } + ) { + dog { name } + } + ', [ + $this->badValue('Int!', 'null', 3, 22), + $this->badValue('String!', 'null', 4, 25), + $this->badValue('Boolean!', 'null', 5, 47), + ]); + } + + /** + * @it variables with invalid default values + */ + public function testVariablesWithInvalidDefaultValues() + { + $this->expectFailsRule(new ValuesOfCorrectType, ' + query InvalidDefaultValues( + $a: Int = "one", + $b: String = 4, + $c: ComplexInput = "notverycomplex" + ) { + dog { name } + } + ', [ + $this->badValue('Int', '"one"', 3, 21), + $this->badValue('String', '4', 4, 24), + $this->badValue('ComplexInput', '"notverycomplex"', 5, 30), + ]); + } + + /** + * @it variables with complex invalid default values + */ + public function testVariablesWithComplexInvalidDefaultValues() + { + $this->expectFailsRule(new ValuesOfCorrectType, ' + query WithDefaultValues( + $a: ComplexInput = { requiredField: 123, intField: "abc" } + ) { + dog { name } + } + ', [ + $this->badValue('Boolean!', '123', 3, 47), + $this->badValue('Int', '"abc"', 3, 62), + ]); + } + + /** + * @it complex variables missing required field + */ + public function testComplexVariablesMissingRequiredField() + { + $this->expectFailsRule(new ValuesOfCorrectType, ' + query MissingRequiredField($a: ComplexInput = {intField: 3}) { + dog { name } + } + ', [ + $this->requiredField('ComplexInput', 'requiredField', 'Boolean!', 2, 55), + ]); + } + + /** + * @it list variables with invalid item + */ + public function testListVariablesWithInvalidItem() + { + $this->expectFailsRule(new ValuesOfCorrectType, ' + query InvalidItem($a: [String] = ["one", 2]) { + dog { name } + } + ', [ + $this->badValue('String', '2', 2, 50), ]); } } diff --git a/tests/Validator/VariablesDefaultValueAllowedTest.php b/tests/Validator/VariablesDefaultValueAllowedTest.php new file mode 100644 index 0000000..da4ca9b --- /dev/null +++ b/tests/Validator/VariablesDefaultValueAllowedTest.php @@ -0,0 +1,109 @@ +expectPassesRule(new VariablesDefaultValueAllowed(), ' + query NullableValues($a: Int, $b: String, $c: ComplexInput) { + dog { name } + } + '); + } + + /** + * @it required variables without default values + */ + public function testRequiredVariablesWithoutDefaultValues() + { + $this->expectPassesRule(new VariablesDefaultValueAllowed(), ' + query RequiredValues($a: Int!, $b: String!) { + dog { name } + } + '); + } + + /** + * @it variables with valid default values + */ + public function testVariablesWithValidDefaultValues() + { + $this->expectPassesRule(new VariablesDefaultValueAllowed(), ' + query WithDefaultValues( + $a: Int = 1, + $b: String = "ok", + $c: ComplexInput = { requiredField: true, intField: 3 } + ) { + dog { name } + } + '); + } + + /** + * @it variables with valid default null values + */ + public function testVariablesWithValidDefaultNullValues() + { + $this->expectPassesRule(new VariablesDefaultValueAllowed(), ' + query WithDefaultValues( + $a: Int = null, + $b: String = null, + $c: ComplexInput = { requiredField: true, intField: null } + ) { + dog { name } + } + '); + } + + /** + * @it no required variables with default values + */ + public function testNoRequiredVariablesWithDefaultValues() + { + $this->expectFailsRule(new VariablesDefaultValueAllowed(), ' + query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { + dog { name } + } + ', [ + $this->defaultForRequiredVar('a', 'Int!', 'Int', 2, 49), + $this->defaultForRequiredVar('b', 'String!', 'String', 2, 66), + ]); + } + + /** + * @it variables with invalid default null values + */ + public function testNullIntoNullableType() + { + $this->expectFailsRule(new VariablesDefaultValueAllowed(), ' + query WithDefaultValues($a: Int! = null, $b: String! = null) { + dog { name } + } + ', [ + $this->defaultForRequiredVar('a', 'Int!', 'Int', 2, 42), + $this->defaultForRequiredVar('b', 'String!', 'String', 2, 62), + ]); + } +}