From e41f93e1dce03ae5ddab331dd218f3e79836f781 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Thu, 8 Feb 2018 16:50:52 +0100 Subject: [PATCH 1/4] Add additional number lexing test ref: graphql/graphql-js#72421378550cf51b13c6db59b8fc912591fd1a4b --- tests/Language/LexerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Language/LexerTest.php b/tests/Language/LexerTest.php index e818fa291..81c2a0490 100644 --- a/tests/Language/LexerTest.php +++ b/tests/Language/LexerTest.php @@ -329,6 +329,7 @@ public function reportsUsefulNumberErrors() [ '00', "Syntax Error GraphQL (1:2) Invalid number, unexpected digit after 0: \"0\"\n\n1: 00\n ^\n"], [ '+1', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"+\".\n\n1: +1\n ^\n"], [ '1.', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \n\n1: 1.\n ^\n"], + [ '1.e1', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \"e\"\n\n1: 1.e1\n ^\n"], [ '.123', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \".\".\n\n1: .123\n ^\n"], [ '1.A', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \"A\"\n\n1: 1.A\n ^\n"], [ '-A', "Syntax Error GraphQL (1:2) Invalid number, expected digit but got: \"A\"\n\n1: -A\n ^\n"], From 621eccf54bd9080dac787103ecd0ec0e93b12ffa Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Thu, 8 Feb 2018 16:52:56 +0100 Subject: [PATCH 2/4] Remove notes about subscription being experimental ref: graphql/graphql-js#bf4a25a33a62280e82680518adc279e34ec816e0 --- src/Language/Parser.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 2a7c7f0f1..5e82c17e2 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -404,7 +404,6 @@ function parseOperationType() switch ($operationToken->value) { case 'query': return 'query'; case 'mutation': return 'mutation'; - // Note: subscription is an experimental non-spec addition. case 'subscription': return 'subscription'; } From a3771cd14a28d230d8bcb7d5216b90257e849e2a Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Fri, 9 Feb 2018 13:49:10 +0100 Subject: [PATCH 3/4] Simplify operationTypes validation ref: graphql/graphql-js#999 --- src/Utils/BuildSchema.php | 125 ++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 67 deletions(-) diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index d75a11f69..f9b108be1 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -13,7 +13,7 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeDefinitionNode; -use GraphQL\Language\AST\TypeDefinitionNode; +use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\TypeNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\Parser; @@ -41,7 +41,7 @@ class BuildSchema /** * @param Type $innerType * @param TypeNode $inputTypeNode - * @return Type + * @return Type */ private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) { @@ -99,9 +99,10 @@ public function __construct(DocumentNode $ast, callable $typeConfigDecorator = n $this->typeConfigDecorator = $typeConfigDecorator; $this->loadedTypeDefs = []; } - + public function buildSchema() { + /** @var SchemaDefinitionNode $schemaDef */ $schemaDef = null; $typeDefs = []; $this->nodeMap = []; @@ -133,61 +134,13 @@ public function buildSchema() } } - $queryTypeName = null; - $mutationTypeName = null; - $subscriptionTypeName = null; - if ($schemaDef) { - foreach ($schemaDef->operationTypes as $operationType) { - $typeName = $operationType->type->name->value; - if ($operationType->operation === 'query') { - if ($queryTypeName) { - throw new Error('Must provide only one query type in schema.'); - } - if (!isset($this->nodeMap[$typeName])) { - throw new Error( - 'Specified query type "' . $typeName . '" not found in document.' - ); - } - $queryTypeName = $typeName; - } else if ($operationType->operation === 'mutation') { - if ($mutationTypeName) { - throw new Error('Must provide only one mutation type in schema.'); - } - if (!isset($this->nodeMap[$typeName])) { - throw new Error( - 'Specified mutation type "' . $typeName . '" not found in document.' - ); - } - $mutationTypeName = $typeName; - } else if ($operationType->operation === 'subscription') { - if ($subscriptionTypeName) { - throw new Error('Must provide only one subscription type in schema.'); - } - if (!isset($this->nodeMap[$typeName])) { - throw new Error( - 'Specified subscription type "' . $typeName . '" not found in document.' - ); - } - $subscriptionTypeName = $typeName; - } - } - } else { - if (isset($this->nodeMap['Query'])) { - $queryTypeName = 'Query'; - } - if (isset($this->nodeMap['Mutation'])) { - $mutationTypeName = 'Mutation'; - } - if (isset($this->nodeMap['Subscription'])) { - $subscriptionTypeName = 'Subscription'; - } - } - - if (!$queryTypeName) { - throw new Error( - 'Must provide schema definition with query type or a type named Query.' - ); - } + $operationTypes = $schemaDef + ? $this->getOperationTypes($schemaDef) + : [ + 'query' => isset($this->nodeMap['Query']) ? 'Query' : null, + 'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null, + 'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null, + ]; $this->innerTypeMap = [ 'String' => Type::string(), @@ -229,13 +182,19 @@ public function buildSchema() $directives[] = Directive::deprecatedDirective(); } + if (!isset($operationTypes['query'])) { + throw new Error( + 'Must provide schema definition with query type or a type named Query.' + ); + } + $schema = new Schema([ - 'query' => $this->getObjectType($this->nodeMap[$queryTypeName]), - 'mutation' => $mutationTypeName ? - $this->getObjectType($this->nodeMap[$mutationTypeName]) : + 'query' => $this->getObjectType($operationTypes['query']), + 'mutation' => isset($operationTypes['mutation']) ? + $this->getObjectType($operationTypes['mutation']) : null, - 'subscription' => $subscriptionTypeName ? - $this->getObjectType($this->nodeMap[$subscriptionTypeName]) : + 'subscription' => isset($operationTypes['subscription']) ? + $this->getObjectType($operationTypes['subscription']) : null, 'typeLoader' => function($name) { return $this->typeDefNamed($name); @@ -256,6 +215,33 @@ public function buildSchema() return $schema; } + /** + * @param SchemaDefinitionNode $schemaDef + * @return array + * @throws Error + */ + private function getOperationTypes($schemaDef) + { + $opTypes = []; + + foreach ($schemaDef->operationTypes as $operationType) { + $typeName = $operationType->type->name->value; + $operation = $operationType->operation; + + if (isset($opTypes[$operation])) { + throw new Error("Must provide only one $operation type in schema."); + } + + if (!isset($this->nodeMap[$typeName])) { + throw new Error("Specified $operation type \"$typeName\" not found in document."); + } + + $opTypes[$operation] = $typeName; + } + + return $opTypes; + } + private function getDirective(DirectiveDefinitionNode $directiveNode) { return new Directive([ @@ -269,9 +255,14 @@ private function getDirective(DirectiveDefinitionNode $directiveNode) ]); } - private function getObjectType(TypeDefinitionNode $typeNode) + /** + * @param string $name + * @return CustomScalarType|EnumType|InputObjectType|UnionType + * @throws Error + */ + private function getObjectType($name) { - $type = $this->typeDefNamed($typeNode->name->value); + $type = $this->typeDefNamed($name); Utils::invariant( $type instanceof ObjectType, 'AST must provide object type.' @@ -619,7 +610,7 @@ public function getDescription($node) /** * A helper function to build a GraphQLSchema directly from a source * document. - * + * * @api * @param DocumentNode|Source|string $source * @param callable $typeConfigDecorator @@ -644,4 +635,4 @@ public function cannotExecuteSchema() ); } -} \ No newline at end of file +} From 51315af0b870dd7ca73c3c6bafa2684591e0a50c Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Fri, 9 Feb 2018 14:17:34 +0100 Subject: [PATCH 4/4] Add warnings for nullable changes ref: https://github.com/graphql/graphql-js/commit/db4cfdc31d1b4a824a95118196843841bccfdf4f ref: graphql/graphql-js#1096 --- src/Utils/FindBreakingChanges.php | 114 +++++++++++++---------- tests/Utils/FindBreakingChangesTest.php | 118 +++++++++++++++++++++++- 2 files changed, 177 insertions(+), 55 deletions(-) diff --git a/src/Utils/FindBreakingChanges.php b/src/Utils/FindBreakingChanges.php index c747a71f4..63acbef15 100644 --- a/src/Utils/FindBreakingChanges.php +++ b/src/Utils/FindBreakingChanges.php @@ -34,37 +34,42 @@ class FindBreakingChanges const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE = 'ARG_DEFAULT_VALUE_CHANGE'; const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM'; const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; + const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED = 'NULLABLE_INPUT_FIELD_ADDED'; + const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED = 'NULLABLE_ARG_ADDED'; /** * Given two schemas, returns an Array containing descriptions of all the types - * of potentially dangerous changes covered by the other functions down below. + * of breaking changes covered by the other functions down below. * * @return array */ - public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema) + public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema) { - return array_merge(self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'], - self::findValuesAddedToEnums($oldSchema, $newSchema), - self::findTypesAddedToUnions($oldSchema, $newSchema) + return array_merge( + self::findRemovedTypes($oldSchema, $newSchema), + self::findTypesThatChangedKind($oldSchema, $newSchema), + self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema), + self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'], + self::findTypesRemovedFromUnions($oldSchema, $newSchema), + self::findValuesRemovedFromEnums($oldSchema, $newSchema), + self::findArgChanges($oldSchema, $newSchema)['breakingChanges'], + self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema) ); } /** * Given two schemas, returns an Array containing descriptions of all the types - * of breaking changes covered by the other functions down below. + * of potentially dangerous changes covered by the other functions down below. * * @return array */ - public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema) + public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema) { return array_merge( - self::findRemovedTypes($oldSchema, $newSchema), - self::findTypesThatChangedKind($oldSchema, $newSchema), - self::findFieldsThatChangedType($oldSchema, $newSchema), - self::findTypesRemovedFromUnions($oldSchema, $newSchema), - self::findValuesRemovedFromEnums($oldSchema, $newSchema), - self::findArgChanges($oldSchema, $newSchema)['breakingChanges'], - self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema) + self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'], + self::findValuesAddedToEnums($oldSchema, $newSchema), + self::findTypesAddedToUnions($oldSchema, $newSchema), + self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges'] ); } @@ -191,17 +196,24 @@ public static function findArgChanges( $oldArgs = $oldTypeFields[$fieldName]->args; $oldArgDef = Utils::find( $oldArgs, function ($arg) use ($newArgDef) { - return $arg->name === $newArgDef->name; - } + return $arg->name === $newArgDef->name; + } ); - if (!$oldArgDef && $newArgDef->getType() instanceof NonNull) { + if (!$oldArgDef) { $newTypeName = $newTypeDefinition->name; $newArgName = $newArgDef->name; - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, - 'description' => "A non-null arg ${newArgName} on ${newTypeName}->${fieldName} was added." - ]; + if ($newArgDef->getType() instanceof NonNull) { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, + 'description' => "A non-null arg ${newArgName} on ${newTypeName}->${fieldName} was added." + ]; + } else { + $dangerousChanges[] = [ + 'type' => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED, + 'description' => "A nullable arg ${newArgName} on ${newTypeName}->${fieldName} was added." + ]; + } } } } @@ -236,36 +248,18 @@ private static function typeKindName(Type $type) throw new \TypeError('unknown type ' . $type->name); } - /** - * Given two schemas, returns an Array containing descriptions of any breaking - * changes in the newSchema related to the fields on a type. This includes if - * a field has been removed from a type, if a field has changed type, or if - * a non-null field is added to an input type. - * - * @return array - */ - public static function findFieldsThatChangedType( - Schema $oldSchema, Schema $newSchema - ) - { - return array_merge( - self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema), - self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema) - ); - } - /** * @param Schema $oldSchema * @param Schema $newSchema * * @return array */ - private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldSchema, Schema $newSchema) + public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldSchema, Schema $newSchema) { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); - $breakingFieldChanges = []; + $breakingChanges = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; if (!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || !($newType instanceof $oldType)) { @@ -275,7 +269,7 @@ private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $newTypeFieldsDef = $newType->getFields(); foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { if (!isset($newTypeFieldsDef[$fieldName])) { - $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; + $breakingChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; } else { $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); $newfieldType = $newTypeFieldsDef[$fieldName]->getType(); @@ -284,12 +278,12 @@ private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType; $newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType; - $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; + $breakingChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; } } } } - return $breakingFieldChanges; + return $breakingChanges; } /** @@ -305,7 +299,8 @@ public static function findFieldsThatChangedTypeOnInputObjectTypes( $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); - $breakingFieldChanges = []; + $breakingChanges = []; + $dangerousChanges = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) { @@ -315,7 +310,10 @@ public static function findFieldsThatChangedTypeOnInputObjectTypes( $newTypeFieldsDef = $newType->getFields(); foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { if (!isset($newTypeFieldsDef[$fieldName])) { - $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, + 'description' => "${typeName}->${fieldName} was removed." + ]; } else { $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); $newfieldType = $newTypeFieldsDef[$fieldName]->getType(); @@ -323,18 +321,32 @@ public static function findFieldsThatChangedTypeOnInputObjectTypes( if (!$isSafe) { $oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType; $newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType; - $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; } } } + // Check if a field was added to the input object type foreach ($newTypeFieldsDef as $fieldName => $fieldDef) { - if (!isset($oldTypeFieldsDef[$fieldName]) && $fieldDef->getType() instanceof NonNull) { + if (!isset($oldTypeFieldsDef[$fieldName])) { $newTypeName = $newType->name; - $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."]; + if ($fieldDef->getType() instanceof NonNull) { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, + 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added." + ]; + } else { + $dangerousChanges[] = [ + 'type' => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED, + 'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added." + ]; + } } } } - return $breakingFieldChanges; + + return ['breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges]; } @@ -580,4 +592,4 @@ private static function isNamedType(Type $type) $type instanceof InputObjectType ); } -} \ No newline at end of file +} diff --git a/tests/Utils/FindBreakingChangesTest.php b/tests/Utils/FindBreakingChangesTest.php index 2fc0f6f5f..8441b0ae5 100644 --- a/tests/Utils/FindBreakingChangesTest.php +++ b/tests/Utils/FindBreakingChangesTest.php @@ -259,7 +259,7 @@ public function testShouldDetectFieldChangesAndDeletions() ]) ]); - $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedType($oldSchema, $newSchema)); + $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema)); } @@ -429,7 +429,7 @@ public function testShouldDetectInputFieldChanges() ], ]; - $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedType($oldSchema, $newSchema)); + $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges']); } public function testDetectsNonNullFieldAddedToInputType() @@ -473,7 +473,7 @@ public function testDetectsNonNullFieldAddedToInputType() 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => 'A non-null field requiredField on input type InputType1 was added.' ], - FindBreakingChanges::findFieldsThatChangedType($oldSchema, $newSchema)[0] + FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'][0] ); } @@ -1387,6 +1387,55 @@ public function testDetectsAdditionsToUnionType() ); } + /** + * @it should detect if a nullable field was added to an input + */ + public function testShouldDetectIfANullableFieldWasAddedToAnInput() + { + $oldInputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + ], + ], + ]); + $newInputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + ], + 'field2' => [ + 'type' => Type::int(), + ], + ], + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $oldInputType, + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $newInputType, + ] + ]); + + $expectedFieldChanges = [ + [ + 'description' => 'A nullable field field2 on input type InputType1 was added.', + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED + ], + ]; + + $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']); + } + public function testFindsAllDangerousChanges() { $enumThatGainsAValueOld = new EnumType([ @@ -1498,4 +1547,65 @@ public function testFindsAllDangerousChanges() $this->assertEquals($expectedDangerousChanges, FindBreakingChanges::findDangerousChanges($oldSchema, $newSchema)); } -} \ No newline at end of file + + + /** + * @it should detect if a nullable field argument was added + */ + public function testShouldDetectIfANullableFieldArgumentWasAdded() + { + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => [ + 'type' => Type::string(), + ], + ], + ], + ], + ]); + + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => [ + 'type' => Type::string(), + ], + 'arg2' => [ + 'type' => Type::string(), + ], + ], + ], + ], + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $oldType, + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $newType, + ] + ]); + + $expectedFieldChanges = [ + [ + 'description' => 'A nullable arg arg2 on Type1->field1 was added.', + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED + ], + ]; + + $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['dangerousChanges']); + } +}