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,34 +339,40 @@ class ValidationTest extends TestCase
*/ */
private function assertContainsValidationMessage($array, $messages) private function assertContainsValidationMessage($array, $messages)
{ {
self::assertCount( $allErrors = implode(
count($messages), "\n",
$array, array_map(
sprintf('For messages: %s', $messages[0]['message']) . "\n" . static function ($error) {
"Received: \n" . return $error->getMessage();
implode( },
"\n", $array
array_map(
static function ($error) {
return $error->getMessage();
},
$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'] ?? [];
foreach ($array as $actual) {
if ($actual instanceof Error && $actual->getMessage() === $msg) {
$actualLocations = [];
foreach ($actual->getLocations() as $location) {
$actualLocations[] = $location->toArray();
}
self::assertEquals(
$locations,
$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::assertEquals($messages[$index]['message'], $error->getMessage()); self::fail(sprintf("Expected error not found:\n%s\n\nActual errors:\n%s", $msg, $allErrors));
$errorLocations = [];
foreach ($error->getLocations() as $location) {
$errorLocations[] = $location->toArray();
}
self::assertEquals(
$messages[$index]['locations'] ?? [],
$errorLocations
);
} }
} }
@ -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')
*/ */