Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/bundle/DependencyInjection/IbexaTestRestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace Ibexa\Bundle\Test\Rest\DependencyInjection;

use Ibexa\Contracts\Test\Rest\Schema\SchemaProviderInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
Expand All @@ -20,6 +21,9 @@ final class IbexaTestRestExtension extends Extension
*/
public function load(array $configs, ContainerBuilder $container): void
{
$container->registerForAutoconfiguration(SchemaProviderInterface::class)
->addTag('ibexa.test.rest.schema_provider');

$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__ . '/../Resources/config')
Expand Down
11 changes: 9 additions & 2 deletions src/bundle/Resources/config/services/rest_schema.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
services:
_defaults:
public: false
autowire: true
autoconfigure: true

Ibexa\Test\Rest\Schema\Validator\SchemaValidatorRegistry:
public: true
arguments:
$validators: !tagged_iterator { tag: ibexa.test.rest.schema.validator, index_by: format }

Ibexa\Test\Rest\Schema\SchemaStorageFactory: ~

Ibexa\Test\Rest\Schema\Validator\BaseSchemaValidator: ~

Ibexa\Test\Rest\Schema\Validator\JsonSchemaValidator:
Expand All @@ -20,7 +27,6 @@ services:

ibexa.test.rest.json_schema.validator:
class: JsonSchema\Validator
public: true
arguments:
$factory: '@ibexa.test.rest.json_schema.factory'

Expand All @@ -31,9 +37,10 @@ services:

ibexa.test.rest.json_schema.schema_storage:
class: JsonSchema\SchemaStorage
public: true
factory: ['@Ibexa\Test\Rest\Schema\SchemaStorageFactory', 'create']
arguments:
$uriRetriever: '@ibexa.test.rest.json_schema.uri_retriever'
$schemas: !tagged_iterator ibexa.test.rest.schema_provider

ibexa.test.rest.json_schema.uri_retriever:
class: JsonSchema\Uri\UriRetriever
35 changes: 35 additions & 0 deletions src/contracts/Schema/AbstractFileSchemaProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Test\Rest\Schema;

use LogicException;

abstract class AbstractFileSchemaProvider implements SchemaProviderInterface
{
/**
* @throws \JsonException
*/
final protected function decodeFile(string $file): object
{
$basicTypes = file_get_contents($file);
if ($basicTypes === false) {
throw new LogicException('Failed to load basic types schema from file: ' . $file);
}

$schema = json_decode($basicTypes, false, 512, JSON_THROW_ON_ERROR);
if (!is_object($schema)) {
throw new LogicException(sprintf(
'Failed to decode basic types schema from file: %s. Schema is not an object.',
$file,
));
}

return $schema;
}
}
21 changes: 21 additions & 0 deletions src/contracts/Schema/SchemaProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Test\Rest\Schema;

interface SchemaProviderInterface
{
/**
* Return schema objects, indexed by their "$id".
*
* Will be automatically prefixed with "internal://".
*
* @return iterable<non-empty-string, object>
*/
public function provideSchemas(): iterable;
}
33 changes: 33 additions & 0 deletions src/lib/Schema/SchemaStorageFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Test\Rest\Schema;

use JsonSchema\SchemaStorage;
use JsonSchema\UriRetrieverInterface;

final class SchemaStorageFactory
{
/**
* @param iterable<\Ibexa\Contracts\Test\Rest\Schema\SchemaProviderInterface> $schemas
*/
public function create(
UriRetrieverInterface $uriRetriever,
iterable $schemas
): SchemaStorage {
$storage = new SchemaStorage($uriRetriever);

foreach ($schemas as $schemaProvider) {
foreach ($schemaProvider->provideSchemas() as $id => $schema) {
$storage->addSchema("internal://$id", $schema);
}
}

return $storage;
}
}
12 changes: 9 additions & 3 deletions src/lib/Schema/Validator/JsonSchemaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ public function __construct(Validator $validator)
public function validate(string $data, string $schemaFilePath): void
{
$decodedData = json_decode($data, false, 512, JSON_THROW_ON_ERROR);
$schemaReference = [
'$ref' => 'file://' . $this->buildSchemaFilePath($schemaFilePath, 'json'),
];
if (str_starts_with($schemaFilePath, 'internal://')) {
$schemaReference = [
'$ref' => $schemaFilePath,
];
} else {
$schemaReference = [
'$ref' => 'file://' . $this->buildSchemaFilePath($schemaFilePath, 'json'),
];
}

$this->validator->validate($decodedData, $schemaReference);

Expand Down
7 changes: 7 additions & 0 deletions tests/integration/Resources/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
_defaults:
public: false
autowire: true
autoconfigure: true

Ibexa\Tests\Integration\Test\Rest\Schema\TestSchemaProvider: ~
22 changes: 22 additions & 0 deletions tests/integration/Schema/TestSchemaProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Integration\Test\Rest\Schema;

use Ibexa\Contracts\Test\Rest\Schema\AbstractFileSchemaProvider;

final class TestSchemaProvider extends AbstractFileSchemaProvider
{
/**
* @throws \JsonException
*/
public function provideSchemas(): iterable
{
yield 'ibexa/test-rest/basic_types' => $this->decodeFile(__DIR__ . '/../basic_types.json');
}
}
82 changes: 82 additions & 0 deletions tests/integration/Schema/Validator/JsonSchemaValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Integration\Test\Rest\Schema\Validator;

use Ibexa\Contracts\Test\Core\IbexaKernelTestCase;
use Ibexa\Test\Rest\Schema\Validator\JsonSchemaValidator;
use JsonSchema\SchemaStorageInterface;
use LogicException;
use PHPUnit\Framework\ExpectationFailedException;

final class JsonSchemaValidatorTest extends IbexaKernelTestCase
{
private JsonSchemaValidator $validator;

private SchemaStorageInterface $storage;

protected function setUp(): void
{
parent::setUp();
$core = self::getIbexaTestCore();
$this->validator = $core->getServiceByClassName(JsonSchemaValidator::class);
$this->storage = $core->getServiceByClassName(SchemaStorageInterface::class, 'ibexa.test.rest.json_schema.schema_storage');
}

public function testValidate(): void
{
$json = '{"data": {"xyz": false}}';

$this->expectException(ExpectationFailedException::class);
$this->expectExceptionMessage(<<<MESSAGE
property: [data], constraint: type, error: Object value found, but a string is required
property: [data], constraint: type, error: Object value found, but an integer is required
property: [data], constraint: oneOf, error: Failed to match exactly one schema

Failed asserting that false is true.
MESSAGE);
$this->validator->validate($json, __DIR__ . '/../../json_schema');
}

public function testValidateWithInternal(): void
{
$this->storage->addSchema(
'internal://ibexa/test-rest/json_schema',
$this->decodeJsonObject($this->loadFile(__DIR__ . '/../../json_schema.json'))
);

$json = '{"data": {"xyz": false}}';
$this->expectException(ExpectationFailedException::class);
$this->expectExceptionMessage(<<<MESSAGE
property: [data], constraint: type, error: Object value found, but a string is required
property: [data], constraint: type, error: Object value found, but an integer is required
property: [data], constraint: oneOf, error: Failed to match exactly one schema

Failed asserting that false is true.
MESSAGE);
$this->validator->validate($json, 'internal://ibexa/test-rest/json_schema');
}

private function decodeJsonObject(string $content): object
{
return json_decode($content, false, 512, JSON_THROW_ON_ERROR);
}

private function loadFile(string $location): string
{
$contents = file_get_contents($location);
if (empty($contents)) {
throw new LogicException(sprintf(
'Unable to load file: %s',
$location,
));
}

return $contents;
}
}
24 changes: 24 additions & 0 deletions tests/integration/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

use Ibexa\Bundle\Test\Rest\IbexaTestRestBundle;
use Ibexa\Contracts\Test\Core\IbexaTestKernel;
use Ibexa\Test\Rest\Schema\Validator\JsonSchemaValidator;
use JsonSchema\SchemaStorageInterface;
use Symfony\Component\Config\Loader\LoaderInterface;

final class TestKernel extends IbexaTestKernel
{
Expand All @@ -19,4 +22,25 @@ public function registerBundles(): iterable

yield new IbexaTestRestBundle();
}

protected static function getExposedServicesByClass(): iterable
{
yield from parent::getExposedServicesByClass();

yield JsonSchemaValidator::class;
}

protected static function getExposedServicesById(): iterable
{
yield from parent::getExposedServicesById();

yield 'ibexa.test.rest.json_schema.schema_storage' => SchemaStorageInterface::class;
}

protected function loadServices(LoaderInterface $loader): void
{
parent::loadServices($loader);

$loader->load(__DIR__ . '/Resources/services.yaml');
}
}
Loading
Loading