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
This commit is contained in:
Daniel Tschinder 2018-02-15 21:29:14 +01:00
parent 17520876d8
commit 58e0c7a178
16 changed files with 834 additions and 593 deletions

View File

@ -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 ]
);
}

View File

@ -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;

View File

@ -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();
}
/**

View File

@ -1,39 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class ArgumentsOfCorrectType extends AbstractValidationRule
{
static function badValueMessage($argName, $type, $value, $verboseErrors = [])
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Argument \"$argName\" has invalid value $value.$message";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::ARGUMENT => 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();
}
];
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class DefaultValuesOfCorrectType extends AbstractValidationRule
{
static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null)
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Variable \$$varName has invalid default value: $value.$message";
}
static function defaultForNonNullArgMessage($varName, $type, $guessType)
{
return "Variable \$$varName of type $type " .
"is required and will never use the default value. " .
"Perhaps you meant to use type $guessType.";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::VARIABLE_DEFINITION => 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();}
];
}
}

View File

@ -0,0 +1,226 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
/**
* Value literals of correct type
*
* A GraphQL document is only valid if all value literals are of the type
* expected at their position.
*/
class ValuesOfCorrectType extends AbstractValidationRule
{
static function badValueMessage($typeName, $valueName, $message = null)
{
return "Expected type {$typeName}, found {$valueName}" .
($message ? "; ${message}" : '.');
}
static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName)
{
return "Field {$typeName}.{$fieldName} of required type " .
"{$fieldTypeName} was not provided.";
}
static function unknownFieldMessage($typeName, $fieldName)
{
return "Field \"{$fieldName}\" is not defined by type {$typeName}.";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::NULL => 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) . '?' : '';
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\ValidationContext;
/**
* Variable's default value is allowed
*
* A GraphQL document is only valid if all variable default values are allowed
* due to a variable not being required.
*/
class VariablesDefaultValueAllowed extends AbstractValidationRule
{
static function defaultForRequiredVarMessage($varName, $type, $guessType)
{
return (
"Variable \"\${$varName}\" of type \"{$type}\" is required and " .
'will not use the default value. ' .
"Perhaps you meant to use type \"{$guessType}\"."
);
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::VARIABLE_DEFINITION => 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();
},
];
}
}

View File

@ -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
*/

View File

@ -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',

View File

@ -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."
);
}

View File

@ -0,0 +1,37 @@
<?php
namespace GraphQL\Tests\Utils;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\DocumentValidator;
class IsValidLiteralValueTest extends \PHPUnit_Framework_TestCase
{
// DESCRIBE: isValidLiteralValue
/**
* @it Returns no errors for a valid value
*/
public function testReturnsNoErrorsForAValidValue()
{
$this->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());
}
}

View File

@ -1,188 +0,0 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\Error\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
class DefaultValuesOfCorrectTypeTest extends TestCase
{
// Validate: Variable default values of correct type
/**
* @it variables with no default values
*/
public function testVariablesWithNoDefaultValues()
{
$this->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) ]
);
}
}

View File

@ -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(),
],
]
]);

View File

@ -1,10 +1,6 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\Error\FormattedError;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
class ValidationTest extends TestCase
{
// Validate: Supports full validation
@ -40,8 +36,7 @@ class ValidationTest extends TestCase
';
$expectedError = [
'message' => "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] ]
];

View File

@ -0,0 +1,109 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\Error\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
class VariablesDefaultValueAllowedTest extends TestCase
{
private function defaultForRequiredVar($varName, $typeName, $guessTypeName, $line, $column)
{
return FormattedError::create(
VariablesDefaultValueAllowed::defaultForRequiredVarMessage(
$varName,
$typeName,
$guessTypeName
),
[new SourceLocation($line, $column)]
);
}
// DESCRIBE: Validate: Variable default value is allowed
/**
* @it variables with no default values
*/
public function testVariablesWithNoDefaultValues()
{
$this->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),
]);
}
}