Ensure interface has at least 1 concrete type

This commit is contained in:
Vladimir Razuvaev 2018-11-21 20:11:11 +07:00
parent 779774b162
commit 7c19777dff
2 changed files with 97 additions and 32 deletions

View File

@ -263,6 +263,9 @@ class SchemaValidationContext
} elseif ($type instanceof InterfaceType) { } elseif ($type instanceof InterfaceType) {
// Ensure fields are valid. // Ensure fields are valid.
$this->validateFields($type); $this->validateFields($type);
// Ensure Interfaces include at least 1 Object type.
$this->validateInterfaces($type);
} elseif ($type instanceof UnionType) { } elseif ($type instanceof UnionType) {
// Ensure Unions include valid member types. // Ensure Unions include valid member types.
$this->validateUnionMembers($type); $this->validateUnionMembers($type);
@ -504,6 +507,23 @@ class SchemaValidationContext
} }
} }
private function validateInterfaces(InterfaceType $iface)
{
$possibleTypes = $this->schema->getPossibleTypes($iface);
if (count($possibleTypes) !== 0) {
return;
}
$this->reportError(
sprintf(
'Interface %s must be implemented by at least one Object type.',
$iface->name
),
$iface->astNode
);
}
/** /**
* @param InterfaceType $iface * @param InterfaceType $iface
* *

View File

@ -21,8 +21,8 @@ use GraphQL\Utils\Utils;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use function array_map; use function array_map;
use function array_merge; use function array_merge;
use function count;
use function implode; use function implode;
use function print_r;
use function sprintf; use function sprintf;
class ValidationTest extends TestCase class ValidationTest extends TestCase
@ -74,6 +74,11 @@ class ValidationTest extends TestCase
}, },
]); ]);
$this->SomeInterfaceType = new InterfaceType([
'name' => 'SomeInterface',
'fields' => ['f' => ['type' => Type::string()]],
]);
$this->SomeObjectType = new ObjectType([ $this->SomeObjectType = new ObjectType([
'name' => 'SomeObject', 'name' => 'SomeObject',
'fields' => ['f' => ['type' => Type::string()]], 'fields' => ['f' => ['type' => Type::string()]],
@ -87,11 +92,6 @@ class ValidationTest extends TestCase
'types' => [$this->SomeObjectType], 'types' => [$this->SomeObjectType],
]); ]);
$this->SomeInterfaceType = new InterfaceType([
'name' => 'SomeInterface',
'fields' => ['f' => ['type' => Type::string()]],
]);
$this->SomeEnumType = new EnumType([ $this->SomeEnumType = new EnumType([
'name' => 'SomeEnum', 'name' => 'SomeEnum',
'values' => [ 'values' => [
@ -339,12 +339,7 @@ class ValidationTest extends TestCase
*/ */
private function assertContainsValidationMessage($array, $messages) private function assertContainsValidationMessage($array, $messages)
{ {
self::assertCount( $allErrors = implode(
count($messages),
$array,
sprintf('For messages: %s', $messages[0]['message']) . "\n" .
"Received: \n" .
implode(
"\n", "\n",
array_map( array_map(
static function ($error) { static function ($error) {
@ -352,21 +347,32 @@ class ValidationTest extends TestCase
}, },
$array $array
) )
)
); );
foreach ($array as $index => $error) {
if (! isset($messages[$index]) || ! $error instanceof Error) { foreach ($messages as $expected) {
self::fail('Received unexpected error: ' . $error->getMessage()); $msg = $expected['message'];
} $locations = $expected['locations'] ?? [];
self::assertEquals($messages[$index]['message'], $error->getMessage()); foreach ($array as $actual) {
$errorLocations = []; if ($actual instanceof Error && $actual->getMessage() === $msg) {
foreach ($error->getLocations() as $location) { $actualLocations = [];
$errorLocations[] = $location->toArray(); foreach ($actual->getLocations() as $location) {
$actualLocations[] = $location->toArray();
} }
self::assertEquals( self::assertEquals(
$messages[$index]['locations'] ?? [], $locations,
$errorLocations $actualLocations,
sprintf(
'Locations do not match for error: %s\n\nExpected:\n%s\n\nActual:\n%s',
$msg,
print_r($locations, true),
print_r($actualLocations, true)
)
); );
// Found and valid, so check the next message (and don't fail)
continue 2;
}
}
self::fail(sprintf("Expected error not found:\n%s\n\nActual errors:\n%s", $msg, $allErrors));
} }
} }
@ -598,12 +604,18 @@ class ValidationTest extends TestCase
*/ */
private function schemaWithFieldType($type) : Schema private function schemaWithFieldType($type) : Schema
{ {
$ifaceImplementation = new ObjectType([
'name' => 'SomeInterfaceImplementation',
'fields' => ['f' => ['type' => Type::string()]],
'interfaces' => [ $this->SomeInterfaceType ],
]);
return new Schema([ return new Schema([
'query' => new ObjectType([ 'query' => new ObjectType([
'name' => 'Query', 'name' => 'Query',
'fields' => ['f' => ['type' => $type]], 'fields' => ['f' => ['type' => $type]],
]), ]),
'types' => [$type], 'types' => [$type, $ifaceImplementation],
]); ]);
} }
@ -1237,6 +1249,14 @@ class ValidationTest extends TestCase
], ],
]); ]);
$BadImplementingType = new ObjectType([
'name' => 'BadImplementing',
'interfaces' => [ $BadInterfaceType ],
'fields' => [
'badField' => [ 'type' => $fieldType ],
],
]);
return new Schema([ return new Schema([
'query' => new ObjectType([ 'query' => new ObjectType([
'name' => 'Query', 'name' => 'Query',
@ -1244,6 +1264,7 @@ class ValidationTest extends TestCase
'f' => ['type' => $BadInterfaceType], 'f' => ['type' => $BadInterfaceType],
], ],
]), ]),
'types' => [ $BadImplementingType, $this->SomeObjectType ],
]); ]);
} }
@ -1308,6 +1329,30 @@ class ValidationTest extends TestCase
); );
} }
/**
* @see it('rejects an interface not implemented by at least one object')
*/
public function testRejectsAnInterfaceNotImplementedByAtLeastOneObject()
{
$schema = BuildSchema::build('
type Query {
test: SomeInterface
}
interface SomeInterface {
foo: String
}
');
$this->assertContainsValidationMessage(
$schema->validate(),
[[
'message' => 'Interface SomeInterface must be implemented by at least one Object type.',
'locations' => [[ 'line' => 6, 'column' => 7 ]],
],
]
);
}
/** /**
* @see it('accepts an input type as a field arg type') * @see it('accepts an input type as a field arg type')
*/ */