mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-29 16:36:02 +03:00
6e358eb26c
`OverlappingFieldsCanBeMerged` would infinite loop when passed something like ```graphql fragment A on User { name ...A } ``` It's not `OverlappingFieldsCanBeMerged`'s responsibility to detect that validation error, but we still would ideally avoid infinite looping. This detects that case, and pretends that the infinite spread wasn't there for the purposes of this validation step. Also, by memoizing and checking for self-references this removes duplicate reports. ref: graphql/graphql-js#1111
1147 lines
32 KiB
PHP
1147 lines
32 KiB
PHP
<?php
|
|
namespace GraphQL\Tests\Validator;
|
|
|
|
use GraphQL\Error\FormattedError;
|
|
use GraphQL\Language\SourceLocation;
|
|
use GraphQL\Type\Schema;
|
|
use GraphQL\Type\Definition\InterfaceType;
|
|
use GraphQL\Type\Definition\ObjectType;
|
|
use GraphQL\Type\Definition\Type;
|
|
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
|
|
|
class OverlappingFieldsCanBeMergedTest extends TestCase
|
|
{
|
|
// Validate: Overlapping fields can be merged
|
|
|
|
/**
|
|
* @it unique fields
|
|
*/
|
|
public function testUniqueFields()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged(), '
|
|
fragment uniqueFields on Dog {
|
|
name
|
|
nickname
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it identical fields
|
|
*/
|
|
public function testIdenticalFields()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment mergeIdenticalFields on Dog {
|
|
name
|
|
name
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it identical fields with identical args
|
|
*/
|
|
public function testIdenticalFieldsWithIdenticalArgs()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
|
|
doesKnowCommand(dogCommand: SIT)
|
|
doesKnowCommand(dogCommand: SIT)
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it identical fields with identical directives
|
|
*/
|
|
public function testIdenticalFieldsWithIdenticalDirectives()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment mergeSameFieldsWithSameDirectives on Dog {
|
|
name @include(if: true)
|
|
name @include(if: true)
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it different args with different aliases
|
|
*/
|
|
public function testDifferentArgsWithDifferentAliases()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment differentArgsWithDifferentAliases on Dog {
|
|
knowsSit : doesKnowCommand(dogCommand: SIT)
|
|
knowsDown : doesKnowCommand(dogCommand: DOWN)
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it different directives with different aliases
|
|
*/
|
|
public function testDifferentDirectivesWithDifferentAliases()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment differentDirectivesWithDifferentAliases on Dog {
|
|
nameIfTrue : name @include(if: true)
|
|
nameIfFalse : name @include(if: false)
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it different skip/include directives accepted
|
|
*/
|
|
public function testDifferentSkipIncludeDirectivesAccepted()
|
|
{
|
|
// Note: Differing skip/include directives don't create an ambiguous return
|
|
// value and are acceptable in conditions where differing runtime values
|
|
// may have the same desired effect of including or skipping a field.
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment differentDirectivesWithDifferentAliases on Dog {
|
|
name @include(if: true)
|
|
name @include(if: false)
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it Same aliases with different field targets
|
|
*/
|
|
public function testSameAliasesWithDifferentFieldTargets()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment sameAliasesWithDifferentFieldTargets on Dog {
|
|
fido : name
|
|
fido : nickname
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('fido', 'name and nickname are different fields'),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it Same aliases allowed on non-overlapping fields
|
|
*/
|
|
public function testSameAliasesAllowedOnNonOverlappingFields()
|
|
{
|
|
// This is valid since no object can be both a "Dog" and a "Cat", thus
|
|
// these fields can never overlap.
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment sameAliasesWithDifferentFieldTargets on Pet {
|
|
... on Dog {
|
|
name
|
|
}
|
|
... on Cat {
|
|
name: nickname
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it Alias masking direct field access
|
|
*/
|
|
public function testAliasMaskingDirectFieldAccess()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment aliasMaskingDirectFieldAccess on Dog {
|
|
name : nickname
|
|
name
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'nickname and name are different fields'),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it different args, second adds an argument
|
|
*/
|
|
public function testDifferentArgsSecondAddsAnArgument()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment conflictingArgs on Dog {
|
|
doesKnowCommand
|
|
doesKnowCommand(dogCommand: HEEL)
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it different args, second missing an argument
|
|
*/
|
|
public function testDifferentArgsSecondMissingAnArgument()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment conflictingArgs on Dog {
|
|
doesKnowCommand(dogCommand: SIT)
|
|
doesKnowCommand
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
)
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @it conflicting args
|
|
*/
|
|
public function testConflictingArgs()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment conflictingArgs on Dog {
|
|
doesKnowCommand(dogCommand: SIT)
|
|
doesKnowCommand(dogCommand: HEEL)
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
|
[new SourceLocation(3,9), new SourceLocation(4,9)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it allows different args where no conflict is possible
|
|
*/
|
|
public function testAllowsDifferentArgsWhereNoConflictIsPossible()
|
|
{
|
|
// This is valid since no object can be both a "Dog" and a "Cat", thus
|
|
// these fields can never overlap.
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment conflictingArgs on Pet {
|
|
... on Dog {
|
|
name(surname: true)
|
|
}
|
|
... on Cat {
|
|
name
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it encounters conflict in fragments
|
|
*/
|
|
public function testEncountersConflictInFragments()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
...A
|
|
...B
|
|
}
|
|
fragment A on Type {
|
|
x: a
|
|
}
|
|
fragment B on Type {
|
|
x: b
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
|
|
[new SourceLocation(7, 9), new SourceLocation(10, 9)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it reports each conflict once
|
|
*/
|
|
public function testReportsEachConflictOnce()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
f1 {
|
|
...A
|
|
...B
|
|
}
|
|
f2 {
|
|
...B
|
|
...A
|
|
}
|
|
f3 {
|
|
...A
|
|
...B
|
|
x: c
|
|
}
|
|
}
|
|
fragment A on Type {
|
|
x: a
|
|
}
|
|
fragment B on Type {
|
|
x: b
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
|
|
[new SourceLocation(18, 9), new SourceLocation(21, 9)]
|
|
),
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and a are different fields'),
|
|
[new SourceLocation(14, 11), new SourceLocation(18, 9)]
|
|
),
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and b are different fields'),
|
|
[new SourceLocation(14, 11), new SourceLocation(21, 9)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it deep conflict
|
|
*/
|
|
public function testDeepConflict()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
x: a
|
|
},
|
|
field {
|
|
x: b
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['x', 'a and b are different fields']]),
|
|
[
|
|
new SourceLocation(3, 9),
|
|
new SourceLocation(4, 11),
|
|
new SourceLocation(6,9),
|
|
new SourceLocation(7, 11)
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it deep conflict with multiple issues
|
|
*/
|
|
public function testDeepConflictWithMultipleIssues()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
x: a
|
|
y: c
|
|
},
|
|
field {
|
|
x: b
|
|
y: d
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [
|
|
['x', 'a and b are different fields'],
|
|
['y', 'c and d are different fields']
|
|
]),
|
|
[
|
|
new SourceLocation(3,9),
|
|
new SourceLocation(4,11),
|
|
new SourceLocation(5,11),
|
|
new SourceLocation(7,9),
|
|
new SourceLocation(8,11),
|
|
new SourceLocation(9,11)
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it very deep conflict
|
|
*/
|
|
public function testVeryDeepConflict()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
deepField {
|
|
x: a
|
|
}
|
|
},
|
|
field {
|
|
deepField {
|
|
x: b
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]),
|
|
[
|
|
new SourceLocation(3,9),
|
|
new SourceLocation(4,11),
|
|
new SourceLocation(5,13),
|
|
new SourceLocation(8,9),
|
|
new SourceLocation(9,11),
|
|
new SourceLocation(10,13)
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it reports deep conflict to nearest common ancestor
|
|
*/
|
|
public function testReportsDeepConflictToNearestCommonAncestor()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
deepField {
|
|
x: a
|
|
}
|
|
deepField {
|
|
x: b
|
|
}
|
|
},
|
|
field {
|
|
deepField {
|
|
y
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]),
|
|
[
|
|
new SourceLocation(4,11),
|
|
new SourceLocation(5,13),
|
|
new SourceLocation(7,11),
|
|
new SourceLocation(8,13)
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it reports deep conflict to nearest common ancestor in fragments
|
|
*/
|
|
public function testReportsDeepConflictToNearestCommonAncestorInFragments()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
...F
|
|
}
|
|
field {
|
|
...F
|
|
}
|
|
}
|
|
fragment F on T {
|
|
deepField {
|
|
deeperField {
|
|
x: a
|
|
}
|
|
deeperField {
|
|
x: b
|
|
}
|
|
}
|
|
deepField {
|
|
deeperField {
|
|
y
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('deeperField', [['x', 'a and b are different fields']]),
|
|
[
|
|
new SourceLocation(12,11),
|
|
new SourceLocation(13,13),
|
|
new SourceLocation(15,11),
|
|
new SourceLocation(16,13),
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it reports deep conflict in nested fragments
|
|
*/
|
|
public function testReportsDeepConflictInNestedFragments()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
...F
|
|
}
|
|
field {
|
|
...I
|
|
}
|
|
}
|
|
fragment F on T {
|
|
x: a
|
|
...G
|
|
}
|
|
fragment G on T {
|
|
y: c
|
|
}
|
|
fragment I on T {
|
|
y: d
|
|
...J
|
|
}
|
|
fragment J on T {
|
|
x: b
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [
|
|
['x', 'a and b are different fields'],
|
|
['y', 'c and d are different fields'],
|
|
]),
|
|
[
|
|
new SourceLocation(3,9),
|
|
new SourceLocation(11,9),
|
|
new SourceLocation(15,9),
|
|
new SourceLocation(6,9),
|
|
new SourceLocation(22,9),
|
|
new SourceLocation(18,9),
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it ignores unknown fragments
|
|
*/
|
|
public function testIgnoresUnknownFragments()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
field {
|
|
...Unknown
|
|
...Known
|
|
}
|
|
}
|
|
fragment Known on T {
|
|
field
|
|
...OtherUnknown
|
|
}
|
|
');
|
|
}
|
|
|
|
// Describe: return types must be unambiguous
|
|
|
|
/**
|
|
* @it conflicting return types which potentially overlap
|
|
*/
|
|
public function testConflictingReturnTypesWhichPotentiallyOverlap()
|
|
{
|
|
// This is invalid since an object could potentially be both the Object
|
|
// type IntBox and the interface type NonNullStringBox1. While that
|
|
// condition does not exist in the current schema, the schema could
|
|
// expand in the future to allow this. Thus it is invalid.
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
...on IntBox {
|
|
scalar
|
|
}
|
|
...on NonNullStringBox1 {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'scalar',
|
|
'they return conflicting types Int and String!'
|
|
),
|
|
[new SourceLocation(5, 15),
|
|
new SourceLocation(8, 15)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it compatible return shapes on different return types
|
|
*/
|
|
public function testCompatibleReturnShapesOnDifferentReturnTypes()
|
|
{
|
|
// In this case `deepBox` returns `SomeBox` in the first usage, and
|
|
// `StringBox` in the second usage. These return types are not the same!
|
|
// however this is valid because the return *shapes* are compatible.
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on SomeBox {
|
|
deepBox {
|
|
unrelatedField
|
|
}
|
|
}
|
|
... on StringBox {
|
|
deepBox {
|
|
unrelatedField
|
|
}
|
|
}
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it disallows differing return types despite no overlap
|
|
*/
|
|
public function testDisallowsDifferingReturnTypesDespiteNoOverlap()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
scalar
|
|
}
|
|
... on StringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'scalar',
|
|
'they return conflicting types Int and String'
|
|
),
|
|
[ new SourceLocation(5, 15),
|
|
new SourceLocation(8, 15)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it reports correctly when a non-exclusive follows an exclusive
|
|
*/
|
|
public function testReportsCorrectlyWhenANonExclusiveFollowsAnExclusive()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
deepBox {
|
|
...X
|
|
}
|
|
}
|
|
}
|
|
someBox {
|
|
... on StringBox {
|
|
deepBox {
|
|
...Y
|
|
}
|
|
}
|
|
}
|
|
memoed: someBox {
|
|
... on IntBox {
|
|
deepBox {
|
|
...X
|
|
}
|
|
}
|
|
}
|
|
memoed: someBox {
|
|
... on StringBox {
|
|
deepBox {
|
|
...Y
|
|
}
|
|
}
|
|
}
|
|
other: someBox {
|
|
...X
|
|
}
|
|
other: someBox {
|
|
...Y
|
|
}
|
|
}
|
|
fragment X on SomeBox {
|
|
scalar
|
|
}
|
|
fragment Y on SomeBox {
|
|
scalar: unrelatedField
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'other',
|
|
[['scalar', 'scalar and unrelatedField are different fields']]
|
|
),
|
|
[
|
|
new SourceLocation(31, 11),
|
|
new SourceLocation(39, 11),
|
|
new SourceLocation(34, 11),
|
|
new SourceLocation(42, 11),
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it disallows differing return type nullability despite no overlap
|
|
*/
|
|
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on NonNullStringBox1 {
|
|
scalar
|
|
}
|
|
... on StringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'scalar',
|
|
'they return conflicting types String! and String'
|
|
),
|
|
[new SourceLocation(5, 15),
|
|
new SourceLocation(8, 15)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it disallows differing return type list despite no overlap
|
|
*/
|
|
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: listStringBox {
|
|
scalar
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: stringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'box',
|
|
'they return conflicting types [StringBox] and StringBox'
|
|
),
|
|
[new SourceLocation(5, 15),
|
|
new SourceLocation(10, 15)]
|
|
)
|
|
]);
|
|
|
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: stringBox {
|
|
scalar
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: listStringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'box',
|
|
'they return conflicting types StringBox and [StringBox]'
|
|
),
|
|
[new SourceLocation(5, 15),
|
|
new SourceLocation(10, 15)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
public function testDisallowsDifferingSubfields()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: stringBox {
|
|
val: scalar
|
|
val: unrelatedField
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: stringBox {
|
|
val: scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'val',
|
|
'scalar and unrelatedField are different fields'
|
|
),
|
|
[new SourceLocation(6, 17),
|
|
new SourceLocation(7, 17)]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it disallows differing deep return types despite no overlap
|
|
*/
|
|
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: stringBox {
|
|
scalar
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: intBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'box',
|
|
[ [ 'scalar', 'they return conflicting types String and Int' ] ]
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(6, 17),
|
|
new SourceLocation(10, 15),
|
|
new SourceLocation(11, 17)
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it allows non-conflicting overlaping types
|
|
*/
|
|
public function testAllowsNonConflictingOverlapingTypes()
|
|
{
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
scalar: unrelatedField
|
|
}
|
|
... on StringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it same wrapped scalar return types
|
|
*/
|
|
public function testSameWrappedScalarReturnTypes()
|
|
{
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
...on NonNullStringBox1 {
|
|
scalar
|
|
}
|
|
...on NonNullStringBox2 {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it allows inline typeless fragments
|
|
*/
|
|
public function testAllowsInlineTypelessFragments()
|
|
{
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
a
|
|
... {
|
|
a
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it compares deep types including list
|
|
*/
|
|
public function testComparesDeepTypesIncludingList()
|
|
{
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
connection {
|
|
...edgeID
|
|
edges {
|
|
node {
|
|
id: name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fragment edgeID on Connection {
|
|
edges {
|
|
node {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
', [
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('edges', [['node', [['id', 'name and id are different fields']]]]),
|
|
[
|
|
new SourceLocation(5, 13),
|
|
new SourceLocation(6, 15),
|
|
new SourceLocation(7, 17),
|
|
new SourceLocation(14, 11),
|
|
new SourceLocation(15, 13),
|
|
new SourceLocation(16, 15),
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @it ignores unknown types
|
|
*/
|
|
public function testIgnoresUnknownTypes()
|
|
{
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
|
{
|
|
someBox {
|
|
...on UnknownType {
|
|
scalar
|
|
}
|
|
...on NonNullStringBox2 {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it error message contains hint for alias conflict
|
|
*/
|
|
public function testErrorMessageContainsHintForAliasConflict()
|
|
{
|
|
// The error template should end with a hint for the user to try using
|
|
// different aliases.
|
|
$error = OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields');
|
|
$hint = 'Use different aliases on the fields to fetch both if this was intentional.';
|
|
|
|
$this->assertStringEndsWith($hint, $error);
|
|
}
|
|
|
|
/**
|
|
* @it does not infinite loop on recursive fragment
|
|
*/
|
|
public function testDoesNotInfiniteLoopOnRecursiveFragment()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment fragA on Human { name, relatives { name, ...fragA } }
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it does not infinite loop on immediately recursive fragment
|
|
*/
|
|
public function testDoesNotInfiniteLoopOnImmeditelyRecursiveFragment()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment fragA on Human { name, ...fragA }
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it does not infinite loop on transitively recursive fragment
|
|
*/
|
|
public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragment()
|
|
{
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment fragA on Human { name, ...fragB }
|
|
fragment fragB on Human { name, ...fragC }
|
|
fragment fragC on Human { name, ...fragA }
|
|
');
|
|
}
|
|
|
|
/**
|
|
* @it find invalid case even with immediately recursive fragment
|
|
*/
|
|
public function testFindInvalidCaseEvenWithImmediatelyRecursiveFragment()
|
|
{
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
fragment sameAliasesWithDifferentFieldTargets on Dob {
|
|
...sameAliasesWithDifferentFieldTargets
|
|
fido: name
|
|
fido: nickname
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'fido',
|
|
'name and nickname are different fields'
|
|
),
|
|
[
|
|
new SourceLocation(4, 9),
|
|
new SourceLocation(5, 9),
|
|
]
|
|
)
|
|
]);
|
|
}
|
|
|
|
private function getSchema()
|
|
{
|
|
$StringBox = null;
|
|
$IntBox = null;
|
|
$SomeBox = null;
|
|
|
|
$SomeBox = new InterfaceType([
|
|
'name' => 'SomeBox',
|
|
'fields' => function() use (&$SomeBox) {
|
|
return [
|
|
'deepBox' => ['type' => $SomeBox],
|
|
'unrelatedField' => ['type' => Type::string()]
|
|
];
|
|
|
|
}
|
|
]);
|
|
|
|
$StringBox = new ObjectType([
|
|
'name' => 'StringBox',
|
|
'interfaces' => [$SomeBox],
|
|
'fields' => function() use (&$StringBox, &$IntBox) {
|
|
return [
|
|
'scalar' => ['type' => Type::string()],
|
|
'deepBox' => ['type' => $StringBox],
|
|
'unrelatedField' => ['type' => Type::string()],
|
|
'listStringBox' => ['type' => Type::listOf($StringBox)],
|
|
'stringBox' => ['type' => $StringBox],
|
|
'intBox' => ['type' => $IntBox],
|
|
];
|
|
}
|
|
]);
|
|
|
|
$IntBox = new ObjectType([
|
|
'name' => 'IntBox',
|
|
'interfaces' => [$SomeBox],
|
|
'fields' => function() use (&$StringBox, &$IntBox) {
|
|
return [
|
|
'scalar' => ['type' => Type::int()],
|
|
'deepBox' => ['type' => $IntBox],
|
|
'unrelatedField' => ['type' => Type::string()],
|
|
'listStringBox' => ['type' => Type::listOf($StringBox)],
|
|
'stringBox' => ['type' => $StringBox],
|
|
'intBox' => ['type' => $IntBox],
|
|
];
|
|
}
|
|
]);
|
|
|
|
$NonNullStringBox1 = new InterfaceType([
|
|
'name' => 'NonNullStringBox1',
|
|
'fields' => [
|
|
'scalar' => [ 'type' => Type::nonNull(Type::string()) ]
|
|
]
|
|
]);
|
|
|
|
$NonNullStringBox1Impl = new ObjectType([
|
|
'name' => 'NonNullStringBox1Impl',
|
|
'interfaces' => [ $SomeBox, $NonNullStringBox1 ],
|
|
'fields' => [
|
|
'scalar' => [ 'type' => Type::nonNull(Type::string()) ],
|
|
'unrelatedField' => ['type' => Type::string() ],
|
|
'deepBox' => [ 'type' => $SomeBox ],
|
|
]
|
|
]);
|
|
|
|
$NonNullStringBox2 = new InterfaceType([
|
|
'name' => 'NonNullStringBox2',
|
|
'fields' => [
|
|
'scalar' => ['type' => Type::nonNull(Type::string())]
|
|
]
|
|
]);
|
|
|
|
$NonNullStringBox2Impl = new ObjectType([
|
|
'name' => 'NonNullStringBox2Impl',
|
|
'interfaces' => [ $SomeBox, $NonNullStringBox2 ],
|
|
'fields' => [
|
|
'scalar' => [ 'type' => Type::nonNull(Type::string()) ],
|
|
'unrelatedField' => [ 'type' => Type::string() ],
|
|
'deepBox' => [ 'type' => $SomeBox ],
|
|
]
|
|
]);
|
|
|
|
$Connection = new ObjectType([
|
|
'name' => 'Connection',
|
|
'fields' => [
|
|
'edges' => [
|
|
'type' => Type::listOf(new ObjectType([
|
|
'name' => 'Edge',
|
|
'fields' => [
|
|
'node' => [
|
|
'type' => new ObjectType([
|
|
'name' => 'Node',
|
|
'fields' => [
|
|
'id' => ['type' => Type::id()],
|
|
'name' => ['type' => Type::string()]
|
|
]
|
|
])
|
|
]
|
|
]
|
|
]))
|
|
]
|
|
]
|
|
]);
|
|
|
|
$schema = new Schema([
|
|
'query' => new ObjectType([
|
|
'name' => 'QueryRoot',
|
|
'fields' => [
|
|
'someBox' => ['type' => $SomeBox],
|
|
'connection' => ['type' => $Connection]
|
|
]
|
|
]),
|
|
'types' => [$IntBox, $StringBox, $NonNullStringBox1Impl, $NonNullStringBox2Impl]
|
|
]);
|
|
|
|
return $schema;
|
|
}
|
|
}
|