Skip to content

Commit

Permalink
Add assertions for testing response against JSON Schema from API reso…
Browse files Browse the repository at this point in the history
…urce

Co-authored-by: Teoh Han Hui <teohhanhui@gmail.com>
  • Loading branch information
meyerbaptiste and teohhanhui committed Aug 24, 2019
1 parent 7ac5857 commit 3a2a0bd
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 47 deletions.
8 changes: 7 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ parameters:
-
message: '#Parameter \#1 \$docblock of method phpDocumentor\\Reflection\\DocBlockFactoryInterface::create\(\) expects string, ReflectionClass given\.#'
path: %currentWorkingDirectory%/src/Metadata/Resource/Factory/PhpDocResourceMetadataFactory.php
-
message: '#Parameter \#1 \$objectValue of method GraphQL\\Type\\Definition\\InterfaceType::resolveType\(\) expects object, array(<string, string>)? given.#'
path: %currentWorkingDirectory%/tests/GraphQl/Type/TypeBuilderTest.php
-
message: '#Property ApiPlatform\\Core\\Test\\DoctrineMongoDbOdmFilterTestCase::\$repository \(Doctrine\\ODM\\MongoDB\\Repository\\DocumentRepository\) does not accept Doctrine\\ORM\\EntityRepository<ApiPlatform\\Core\\Tests\\Fixtures\\TestBundle\\Document\\Dummy>\.#'
path: %currentWorkingDirectory%/src/Test/DoctrineMongoDbOdmFilterTestCase.php
Expand All @@ -81,7 +84,10 @@ parameters:
-
message: '#Binary operation "\+" between (float\|int\|)?string and 0 results in an error\.#'
path: %currentWorkingDirectory%/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php
- '#Parameter \#1 \$objectValue of method GraphQL\\Type\\Definition\\InterfaceType::resolveType\(\) expects object, array(<string, string>)? given.#'
# https://github.com/phpstan/phpstan-symfony/issues/27
-
message: '#Service "api_platform\.json_schema\.schema_factory" is private\.#'
path: %currentWorkingDirectory%/src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php

# Expected, due to optional interfaces
- '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryCollectionExtensionInterface::applyToCollection\(\) invoked with 5 parameters, 3-4 required\.#'
Expand Down
37 changes: 36 additions & 1 deletion src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint\ArraySubset;
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint\MatchesJsonSchema;
use ApiPlatform\Core\JsonSchema\Schema;
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
use PHPUnit\Framework\ExpectationFailedException;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
Expand Down Expand Up @@ -91,18 +95,34 @@ public static function assertJsonEquals($json, string $message = ''): void
public static function assertArraySubset($subset, $array, bool $checkForObjectIdentity = false, string $message = ''): void
{
$constraint = new ArraySubset($subset, $checkForObjectIdentity);

static::assertThat($array, $constraint, $message);
}

/**
* @param array|string $jsonSchema
* @param Schema|array|string $jsonSchema
*/
public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = null, string $message = ''): void
{
$constraint = new MatchesJsonSchema($jsonSchema, $checkMode);

static::assertThat(self::getHttpResponse()->toArray(false), $constraint, $message);
}

public static function assertMatchesResourceCollectionJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void
{
$schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::COLLECTION, $operationName);

static::assertMatchesJsonSchema($schema);
}

public static function assertMatchesResourceItemJsonSchema(string $resourceClass, ?string $operationName = null, string $format = 'jsonld'): void
{
$schema = self::getSchemaFactory()->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, OperationType::ITEM, $operationName);

static::assertMatchesJsonSchema($schema);
}

private static function getHttpClient(Client $newClient = null): ?Client
{
static $client;
Expand All @@ -126,4 +146,19 @@ private static function getHttpResponse(): ResponseInterface

return $response;
}

private static function getSchemaFactory(): SchemaFactoryInterface
{
static $schemaFactory;

if (null !== $schemaFactory) {
return $schemaFactory;
}

if (!isset(static::$container) || !static::$container instanceof ContainerInterface || !static::$container->has('api_platform.json_schema.schema_factory')) {
throw new \LogicException(sprintf('You cannot use the resource JSON Schema assertions because the "%s" service cannot be fetched from the container.', 'api_platform.json_schema.schema_factory'));
}

return $schemaFactory = static::$container->get('api_platform.json_schema.schema_factory');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint;

use ApiPlatform\Core\JsonSchema\Schema;
use JsonSchema\Validator;
use PHPUnit\Framework\Constraint\Constraint;

Expand All @@ -29,12 +30,16 @@ final class MatchesJsonSchema extends Constraint
private $checkMode;

/**
* @param array|string $schema
* @param Schema|array|string $schema
*/
public function __construct($schema, ?int $checkMode = null)
{
/** @var array|string $schema */
$schema = $schema instanceof Schema ? json_encode($schema) : $schema;
$schema = \is_array($schema) ? (object) $schema : json_decode($schema);

$this->checkMode = $checkMode;
$this->schema = \is_array($schema) ? (object) $schema : json_decode($schema);
$this->schema = $schema;
}

/**
Expand Down
18 changes: 9 additions & 9 deletions src/Hydra/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ public function __construct(BaseSchemaFactory $schemaFactory)
/**
* {@inheritdoc}
*/
public function buildSchema(string $resourceClass, string $format = 'jsonld', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
public function buildSchema(string $resourceClass, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
$schema = $this->schemaFactory->buildSchema($resourceClass, $format, $output, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
$schema = $this->schemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
if ('jsonld' !== $format) {
return $schema;
}
Expand All @@ -74,24 +74,24 @@ public function buildSchema(string $resourceClass, string $format = 'jsonld', bo
],
'hydra:totalItems' => [
'type' => 'integer',
'minimum' => 1,
'minimum' => 0,
],
'hydra:view' => [
'type' => 'object',
'properties' => [
'@id' => ['type' => 'string'],
'@type' => ['type' => 'string'],
'hydra:first' => [
'type' => 'integer',
'minimum' => 1,
'type' => 'string',
'format' => 'iri-reference',
],
'hydra:last' => [
'type' => 'integer',
'minimum' => 1,
'type' => 'string',
'format' => 'iri-reference',
],
'hydra:next' => [
'type' => 'integer',
'minimum' => 1,
'type' => 'string',
'format' => 'iri-reference',
],
],
],
Expand Down
15 changes: 8 additions & 7 deletions src/JsonSchema/Command/JsonSchemaGenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\JsonSchema\Command;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\JsonSchema\Schema;
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
Expand Down Expand Up @@ -53,7 +54,7 @@ protected function configure()
->addOption('itemOperation', null, InputOption::VALUE_REQUIRED, 'The item operation')
->addOption('collectionOperation', null, InputOption::VALUE_REQUIRED, 'The collection operation')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The response format', (string) $this->formats[0])
->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of schema to generate (input or output)', 'input');
->addOption('type', null, InputOption::VALUE_REQUIRED, sprintf('The type of schema to generate (%s or %s)', Schema::TYPE_INPUT, Schema::TYPE_OUTPUT), Schema::TYPE_INPUT);
}

/**
Expand All @@ -71,11 +72,11 @@ protected function execute(InputInterface $input, OutputInterface $output)
$collectionOperation = $input->getOption('collectionOperation');
/** @var string $format */
$format = $input->getOption('format');
/** @var string $outputType */
$outputType = $input->getOption('type');
/** @var string $type */
$type = $input->getOption('type');

if (!\in_array($outputType, ['input', 'output'], true)) {
$io->error('You can only use "input" or "output" for the "type" option');
if (!\in_array($type, [Schema::TYPE_INPUT, Schema::TYPE_OUTPUT], true)) {
$io->error(sprintf('You can only use "%s" or "%s" for the "type" option', Schema::TYPE_INPUT, Schema::TYPE_OUTPUT));

return 1;
}
Expand All @@ -100,10 +101,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
$operationName = $itemOperation ?? $collectionOperation;
}

$schema = $this->schemaFactory->buildSchema($resource, $format, 'output' === $outputType, $operationType, $operationName);
$schema = $this->schemaFactory->buildSchema($resource, $format, $type, $operationType, $operationName);

if (null !== $operationType && null !== $operationName && !$schema->isDefined()) {
$io->error(sprintf('There is no %ss defined for the operation "%s" of the resource "%s".', $outputType, $operationName, $resource));
$io->error(sprintf('There is no %s defined for the operation "%s" of the resource "%s".', $type, $operationName, $resource));

return 1;
}
Expand Down
4 changes: 3 additions & 1 deletion src/JsonSchema/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
*/
final class Schema extends \ArrayObject
{
public const TYPE_INPUT = 'input';
public const TYPE_OUTPUT = 'output';
public const VERSION_JSON_SCHEMA = 'json-schema';
public const VERSION_SWAGGER = 'swagger';
public const VERSION_OPENAPI = 'openapi';
public const VERSION_SWAGGER = 'swagger';

private $version;

Expand Down
22 changes: 11 additions & 11 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,20 @@ public function addDistinctFormat(string $format): void
/**
* {@inheritdoc}
*/
public function buildSchema(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
public function buildSchema(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
$schema = $schema ?? new Schema();
if (null === $metadata = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext)) {
if (null === $metadata = $this->getMetadata($resourceClass, $type, $operationType, $operationName, $serializerContext)) {
return $schema;
}
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;

$version = $schema->getVersion();
$definitionName = $this->buildDefinitionName($resourceClass, $format, $output, $operationType, $operationName, $serializerContext);
$definitionName = $this->buildDefinitionName($resourceClass, $format, $type, $operationType, $operationName, $serializerContext);

$method = (null !== $operationType && null !== $operationName) ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method') : 'GET';

if (!$output && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
if (Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
return $schema;
}

Expand Down Expand Up @@ -196,9 +196,9 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
}

private function buildDefinitionName(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
private function buildDefinitionName(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
{
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext);
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($resourceClass, $type, $operationType, $operationName, $serializerContext);

$prefix = $resourceMetadata->getShortName();
if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) {
Expand All @@ -220,10 +220,10 @@ private function buildDefinitionName(string $resourceClass, string $format = 'js
return $name;
}

private function getMetadata(string $resourceClass, bool $output, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
private function getMetadata(string $resourceClass, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
{
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
$attribute = $output ? 'output' : 'input';
$attribute = Schema::TYPE_OUTPUT === $type ? 'output' : 'input';
if (null === $operationType || null === $operationName) {
$inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $resourceClass]);
} else {
Expand All @@ -237,14 +237,14 @@ private function getMetadata(string $resourceClass, bool $output, ?string $opera

return [
$resourceMetadata,
$serializerContext ?? $this->getSerializerContext($resourceMetadata, $output, $operationType, $operationName),
$serializerContext ?? $this->getSerializerContext($resourceMetadata, $type, $operationType, $operationName),
$inputOrOutput['class'],
];
}

private function getSerializerContext(ResourceMetadata $resourceMetadata, bool $output, ?string $operationType, ?string $operationName): array
private function getSerializerContext(ResourceMetadata $resourceMetadata, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName): array
{
$attribute = $output ? 'normalization_context' : 'denormalization_context';
$attribute = Schema::TYPE_OUTPUT === $type ? 'normalization_context' : 'denormalization_context';

if (null === $operationType || null === $operationName) {
return $resourceMetadata->getAttribute($attribute, []);
Expand Down
2 changes: 1 addition & 1 deletion src/JsonSchema/SchemaFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ interface SchemaFactoryInterface
/**
* @throws ResourceClassNotFoundException
*/
public function buildSchema(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema;
public function buildSchema(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema;
}
5 changes: 1 addition & 4 deletions src/JsonSchema/TypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,9 @@ private function getClassType(?string $className, string $format = 'json', ?bool
$version = $schema->getVersion();

$subSchema = new Schema($version);
/*
* @var Schema $schema Prevents a false positive in PHPStan
*/
$subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema

$this->schemaFactory->buildSchema($className, $format, true, null, null, $subSchema, $serializerContext);
$this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);

return ['$ref' => $subSchema['$ref']];
}
Expand Down
Loading

0 comments on commit 3a2a0bd

Please sign in to comment.