Skip to content
Commits on Source (16)
This diff is collapsed.
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\LocalData;
use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService;
use Dbp\Relay\CoreBundle\Exception\ApiError;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\Event;
/*
* Abstract implementation of a configurable local data provider post event subscriber.
* It is intended to be derived by local data aware entity post event subscribers.
* A mapping between source attribute and local data attribute,
* and default values for the attributes can be specified by means of the deriving event subscriber's bundle config.
* If no default value is specified, an exception is thrown in the case the mapped source attribute is not found.
*/
abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface
{
protected const ROOT_CONFIG_NODE = 'local_data_mapping';
protected const SOURCE_ATTRIBUTE_CONFIG_NODE = 'source_attribute';
protected const LOCAL_DATA_ATTRIBUTE_CONFIG_NODE = 'local_data_attribute';
protected const AUTHORIZATION_EXPRESSION_CONFIG_NODE = 'authorization_expression';
protected const ALLOW_LOCAL_QUERY_CONFIG_NODE = 'allow_query';
protected const DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE = 'default_value';
protected const DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE = 'default_values';
private const SOURCE_ATTRIBUTE_KEY = 'source';
private const DEFAULT_VALUE_KEY = 'default';
private const QUERYABLE_KEY = 'queryable';
/*
* WORKAROUND: could not find a way to determine whether a Symfony config array node was NOT specified since it provides an empty
* array in case it is not specified. So I use an array value as default which does not seem to be reproducible by the configurator.
*/
private const ARRAY_VALUE_NOT_SPECIFIED = [null => null];
/** @var array */
private $attributeMapping;
public function __construct()
{
$this->attributeMapping = [];
}
public function setConfig(array $config)
{
$configNode = $config[self::ROOT_CONFIG_NODE] ?? [];
$rightExpressions = [];
foreach ($configNode as $configMappingEntry) {
$localDataAttributeName = $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE];
if (isset($this->attributeMapping[$localDataAttributeName])) {
throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $localDataAttributeName));
}
$attributeMapEntry = [];
$attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTE_CONFIG_NODE];
$attributeMapEntry[self::QUERYABLE_KEY] = $configMappingEntry[self::ALLOW_LOCAL_QUERY_CONFIG_NODE];
$defaultValue = $configMappingEntry[self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE] ?? null;
if ($defaultValue === null) {
$defaultArray = $configMappingEntry[self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE] ?? null;
if ($defaultArray !== null && $defaultArray !== self::ARRAY_VALUE_NOT_SPECIFIED) {
$defaultValue = $defaultArray;
}
}
if ($defaultValue !== null) {
$attributeMapEntry[self::DEFAULT_VALUE_KEY] = $defaultValue;
}
$this->attributeMapping[$localDataAttributeName] = $attributeMapEntry;
// the name of the local data attribute is used as name for the right to view that attribute
// the attribute is visible false by default
$rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false';
}
if (!empty($rightExpressions)) {
parent::setConfig(parent::createConfig($rightExpressions));
}
}
public static function getSubscribedEvents(): array
{
$eventMapping = [];
foreach (static::getSubscribedEventNames() as $eventName) {
$eventMapping[$eventName] = 'onEvent';
}
return $eventMapping;
}
public function onEvent(Event $event)
{
if ($event instanceof LocalDataPreEvent) {
$localQueryParameters = [];
foreach ($event->getPendingQueryParameters() as $localDataAttributeName => $localDataAttributeValue) {
if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null &&
$attributeMapEntry[self::QUERYABLE_KEY] === true) {
if (!$this->isGranted($localDataAttributeName)) {
throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
}
$sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY];
$localQueryParameters[$sourceAttributeName] = $localDataAttributeValue;
$event->acknowledgeQueryParameter($localDataAttributeName);
}
}
$this->onPreEvent($event, $localQueryParameters);
} elseif ($event instanceof LocalDataPostEvent) {
foreach ($event->getPendingRequestedAttributes() as $localDataAttributeName) {
if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null) {
if (!$this->isGranted($localDataAttributeName)) {
throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
}
$attributeValue = $event->getSourceData()[$attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY]] ?? null;
$attributeValue = $attributeValue ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null;
if ($attributeValue !== null) {
$event->setLocalDataAttribute($localDataAttributeName, $attributeValue);
} else {
throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('none of the source attributes available for local data attribute \'%s\'', $localDataAttributeName));
}
}
}
$this->onPostEvent($event);
}
}
/**
* @deprecated Use getLocalDataMappingConfigNodeDefinition instead
*/
public static function getConfigNode(): NodeDefinition
{
return self::getLocalDataMappingConfigNodeDefinition();
}
public static function getLocalDataMappingConfigNodeDefinition(): NodeDefinition
{
$treeBuilder = new TreeBuilder(self::ROOT_CONFIG_NODE);
return $treeBuilder->getRootNode()
->arrayPrototype()
->children()
->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)
->info('The name of the local data attribute.')
->end()
->scalarNode(self::SOURCE_ATTRIBUTE_CONFIG_NODE)
->info('The source attribute to map to the local data attribute. If the source attribute is not found, the default value is used.')
->end()
->scalarNode(self::AUTHORIZATION_EXPRESSION_CONFIG_NODE)
->defaultValue('false')
->info('A boolean expression evaluable by the Symfony Expression Language determining whether the current user may request read the local data attribute.')
->end()
->booleanNode(self::ALLOW_LOCAL_QUERY_CONFIG_NODE)
->defaultValue('false')
->info('Indicates whether the local data attribute can be used in local queries.')
->end()
->scalarNode(self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE)
->info('The default value for scalar (i.e. non-array) attributes. If none is specified, an exception is thrown in case the source attribute is not found.')
->end()
->arrayNode(self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE)
->defaultValue(self::ARRAY_VALUE_NOT_SPECIFIED)
->info('The default value for array type attributes. If none is specified, an exception is thrown in case the source attribute is not found.')
->scalarPrototype()->end()
->end()
->end()
->end()
;
}
protected static function getSubscribedEventNames(): array
{
throw new \RuntimeException(sprintf('child classes must implement the \'%s\' method', __METHOD__));
}
protected function onPreEvent(LocalDataPreEvent $preEvent, array $localQueryParameters)
{
}
protected function onPostEvent(LocalDataPostEvent $postEvent)
{
}
}
......@@ -4,159 +4,24 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\LocalData;
use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService;
use Dbp\Relay\CoreBundle\Exception\ApiError;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
/*
* @deprecated Use AbstractLocalDataEventSubscriber
*
* Abstract implementation of a configurable local data provider post event subscriber.
* It is intended to be derived by local data aware entity post event subscribers.
* A mapping between source attribute and local data attribute,
* and default values for the attributes can be specified by means of the deriving event subscriber's bundle config.
* If no default value is specified, an exception is thrown in the case the mapped source attribute is not found.
*/
abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface
abstract class AbstractLocalDataPostEventSubscriber extends AbstractLocalDataEventSubscriber
{
protected const ROOT_CONFIG_NODE = 'local_data_mapping';
protected const SOURCE_ATTRIBUTES_CONFIG_NODE = 'source_attributes';
protected const LOCAL_DATA_ATTRIBUTE_CONFIG_NODE = 'local_data_attribute';
protected const AUTHORIZATION_EXPRESSION_CONFIG_NODE = 'authorization_expression';
protected const DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE = 'default_value';
protected const DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE = 'default_values';
private const SOURCE_ATTRIBUTES_KEY = 'source';
private const DEFAULT_VALUE_KEY = 'default';
/*
* WORKAROUND: could not find a way to determine whether a Symfony config array node was NOT specified since it provides an empty
* array in case it is not specified. So I use an array value as default which does not seem to be reproducible by the configurator.
*/
private const ARRAY_VALUE_NOT_SPECIFIED = [null => null];
/** @var array */
private $attributeMapping;
public function __construct()
{
$this->attributeMapping = [];
}
public function setConfig(array $config)
{
$configNode = $config[self::ROOT_CONFIG_NODE] ?? [];
$rightExpressions = [];
foreach ($configNode as $configMappingEntry) {
$localDataAttributeName = $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE];
if (isset($this->attributeMapping[$localDataAttributeName])) {
throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $localDataAttributeName));
}
$attributeMapEntry = [];
$attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTES_CONFIG_NODE];
$defaultValue = $configMappingEntry[self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE] ?? null;
if ($defaultValue === null) {
$defaultArray = $configMappingEntry[self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE] ?? null;
if ($defaultArray !== null && $defaultArray !== self::ARRAY_VALUE_NOT_SPECIFIED) {
$defaultValue = $defaultArray;
}
}
if ($defaultValue !== null) {
$attributeMapEntry[self::DEFAULT_VALUE_KEY] = $defaultValue;
}
$this->attributeMapping[$localDataAttributeName] = $attributeMapEntry;
// the name of the local data attribute is used as name for the right to view that attribute
// the attribute is visible false by default
$rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false';
}
if (!empty($rightExpressions)) {
parent::setConfig(parent::createConfig($rightExpressions));
}
}
public static function getSubscribedEvents(): array
protected static function getSubscribedEventNames(): array
{
return [static::getSubscribedEventName() => 'onPost'];
return [static::getSubscribedEventName()];
}
public static function getSubscribedEventName(): string
{
throw new \RuntimeException(sprintf('child classes must implement the \'%s\' method', __METHOD__));
}
public function onPost(LocalDataPostEvent $postEvent)
{
$sourceData = $postEvent->getSourceData();
foreach ($this->attributeMapping as $localDataAttributeName => $attributeMapEntry) {
if ($postEvent->isLocalDataAttributeRequested($localDataAttributeName)) {
if (!$this->isGranted($localDataAttributeName)) {
throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
}
$attributeValue = null;
foreach ($attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY] as $sourceAttributeName) {
if (($value = $sourceData[$sourceAttributeName] ?? null) !== null) {
$attributeValue = $value;
break;
}
}
$attributeValue = $attributeValue ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null;
if ($attributeValue !== null) {
$postEvent->setLocalDataAttribute($localDataAttributeName, $attributeValue);
} else {
throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('none of the source attributes available for local data attribute \'%s\'', $localDataAttributeName));
}
}
}
}
/**
* @deprecated Use getLocalDataMappingConfigNodeDefinition instead
*/
public static function getConfigNode(): NodeDefinition
{
return self::getLocalDataMappingConfigNodeDefinition();
}
public static function getLocalDataMappingConfigNodeDefinition(): NodeDefinition
{
$treeBuilder = new TreeBuilder(self::ROOT_CONFIG_NODE);
return $treeBuilder->getRootNode()
->arrayPrototype()
->children()
->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)
->info('The name of the local data attribute.')
->end()
->arrayNode(self::SOURCE_ATTRIBUTES_CONFIG_NODE)
->info('The list of source attributes to map to the local data attribute ordered by preferred usage. If an attribute is not found, the next attribute in the list is used.')
->scalarPrototype()->end()
->end()
->scalarNode(self::AUTHORIZATION_EXPRESSION_CONFIG_NODE)
->defaultValue('false')
->info('A boolean expression evaluable by the Symfony Expression Language determining whether the current user may request read the local data attribute.')
->end()
->scalarNode(self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE)
->info('The default value for scalar (i.e. non-array) attributes. If none is specified, an exception is thrown in case none of the source attributes is found.')
->end()
->arrayNode(self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE)
->defaultValue(self::ARRAY_VALUE_NOT_SPECIFIED)
->info('The default value for array type attributes. If none is specified, an exception is thrown in case none of the source attributes is found.')
->scalarPrototype()->end()
->end()
->end()
->end()
;
}
}
......@@ -36,7 +36,22 @@ class LocalData
return $options[self::QUERY_PARAMETER_NAME] ?? null;
}
public static function toIncludeLocalParameterValue(array $attributeNames): string
/**
* Adds the 'include Local Data' parameter to the set of options in order to request the local data attributes with the given names.
*
* @param array $targetOptions the set of options to add the 'include Local Data' parameter to
* @param array $attributeNames the names of the local data attributes to request
*
* @return array the resultant $targetOptions
*/
public static function addIncludeParameter(array &$targetOptions, array $attributeNames): array
{
$targetOptions[self::INCLUDE_PARAMETER_NAME] = self::toIncludeParameterValue($attributeNames);
return $targetOptions;
}
private static function toIncludeParameterValue(array $attributeNames): string
{
return implode(LocalDataEventDispatcher::SEPARATOR, $attributeNames);
}
......
......@@ -83,15 +83,20 @@ class LocalDataEventDispatcher
public function dispatch(Event $event, string $eventName = null): void
{
if ($event instanceof LocalDataPreEvent) {
$event->setQueryParameters($this->queryParameters);
$event->initQueryParameters($this->queryParameters);
$this->eventDispatcher->dispatch($event, $eventName);
$pendingAttributes = $event->getPendingQueryParameters();
if (count($pendingAttributes) !== 0) {
throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, sprintf("the following local query attributes were not acknowledged for resource '%s': %s", $this->uniqueEntityName, implode(', ', array_keys($pendingAttributes))));
}
} elseif ($event instanceof LocalDataPostEvent) {
$event->setRequestedAttributes($this->requestedAttributes);
$event->initRequestedAttributes($this->requestedAttributes);
$this->eventDispatcher->dispatch($event, $eventName);
$remainingLocalDataAttributes = $event->getRemainingRequestedAttributes();
if (!empty($remainingLocalDataAttributes)) {
throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, sprintf("the following requested local data attributes could not be provided for resource '%s': %s", $this->uniqueEntityName, implode(', ', $remainingLocalDataAttributes)));
$pendingAttributes = $event->getPendingRequestedAttributes();
if (count($pendingAttributes) !== 0) {
throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, sprintf("the following requested local data attributes could not be provided for resource '%s': %s", $this->uniqueEntityName, implode(', ', $pendingAttributes)));
}
} else {
$this->eventDispatcher->dispatch($event, $eventName);
......
......@@ -43,7 +43,7 @@ class LocalDataPostEvent extends Event implements LoggerAwareInterface
*
* @param string[] $requestedAttributes
*/
public function setRequestedAttributes(array $requestedAttributes): void
public function initRequestedAttributes(array $requestedAttributes): void
{
$this->requestedAttributes = $requestedAttributes;
}
......@@ -53,7 +53,7 @@ class LocalDataPostEvent extends Event implements LoggerAwareInterface
*
* @retrun string[]
*/
public function getRemainingRequestedAttributes(): array
public function getPendingRequestedAttributes(): array
{
return $this->requestedAttributes;
}
......@@ -103,11 +103,11 @@ class LocalDataPostEvent extends Event implements LoggerAwareInterface
*
* @throws ApiError if attribute $key is not in the set of requested attributes
*/
private function setLocalDataAttributeInternal(string $key, $value, bool $warnfNotFound): void
private function setLocalDataAttributeInternal(string $key, $value, bool $warnIfNotFound): void
{
$arrayKey = array_search($key, $this->requestedAttributes, true);
if ($arrayKey === false) {
if ($warnfNotFound) {
if ($warnIfNotFound) {
if ($this->logger !== null) {
$this->logger->warning(sprintf("trying to set local data attribute '%s', which was not requested for entity '%s'", $key, LocalDataEventDispatcher::getUniqueEntityName(get_class($this->entity))));
}
......
......@@ -8,33 +8,40 @@ use Symfony\Contracts\EventDispatcher\Event;
class LocalDataPreEvent extends Event
{
public const NAME = 'dbp.relay.relay_core.local_data_aware_event.pre';
/** @var string[] */
private $queryParametersIn;
/** @var array */
private $queryParameters;
private $options;
public function __construct()
public function __construct(array $options)
{
$this->queryParameters = [];
$this->queryParametersIn = [];
$this->options = $options;
}
/**
* Sets the list of query parameters.
*
* @param string[] $queryParameters
*/
public function setQueryParameters(array $queryParameters): void
public function initQueryParameters(array $queryParametersIn): void
{
$this->queryParameters = $queryParameters;
$this->queryParametersIn = $queryParametersIn;
}
/**
* Returns the list of query parameters.
*
* @return string[]
*/
public function getQueryParameters(): array
public function getPendingQueryParameters(): array
{
return $this->queryParameters;
return $this->queryParametersIn;
}
public function acknowledgeQueryParameter(string $queryParameterName): void
{
unset($this->queryParametersIn[$queryParameterName]);
}
public function getOptions(): array
{
return $this->options;
}
public function setOptions(array $options): void
{
$this->options = $options;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests\LocalData;
use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataMuxer;
use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataProviderProvider;
use Dbp\Relay\CoreBundle\Exception\ApiError;
use Dbp\Relay\CoreBundle\LocalData\LocalData;
use Dbp\Relay\CoreBundle\LocalData\LocalDataEventDispatcher;
use Dbp\Relay\CoreBundle\TestUtils\TestUserSession;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Response;
class LocalDataTest extends TestCase
{
/** @var LocalDataEventDispatcher */
private $localDataEventDispatcher;
/** @var TestEntityLocalDataEventSubscriber */
private $localDataEventSubscriber;
protected function setUp(): void
{
parent::setUp();
$localDataEventSubscriber = new TestEntityLocalDataEventSubscriber();
$localDataEventSubscriber->_injectServices(new TestUserSession('testuser'), new AuthorizationDataMuxer(new AuthorizationDataProviderProvider([]), new EventDispatcher()));
$localDataEventSubscriber->setConfig(self::createConfig());
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber($localDataEventSubscriber);
// explicitly specify the entities unique name since it doesn't have an @ApiResource annotation
$this->localDataEventDispatcher = new LocalDataEventDispatcher(TestEntity::class, $eventDispatcher, TestEntity::class);
$this->localDataEventSubscriber = $localDataEventSubscriber;
}
public function testLocalDataMapping()
{
// source attribute specified in config is present in source data -> return source attribute value
$localDataAttributeName = 'attribute_1';
$testEntity = $this->getTestEntity($localDataAttributeName, ['src_attribute_1' => 'value_1']);
$this->assertEquals('value_1', $testEntity->getLocalDataValue($localDataAttributeName));
}
public function testLocalDataMappingDefaultValue()
{
// default value specified in config -> return default value
$localDataAttributeName = 'attribute_1';
$testEntity = $this->getTestEntity($localDataAttributeName, []);
$this->assertEquals(0, $testEntity->getLocalDataValue($localDataAttributeName));
}
public function testLocalDataMappingNoneOfSourceAttributesFoundError()
{
// no default value specified in config -> throw exception on none of source attributes found
$localDataAttributeName = 'attribute_2';
try {
$this->getTestEntity($localDataAttributeName, []);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_INTERNAL_SERVER_ERROR, $exception->getStatusCode());
}
}
public function testLocalDataMappingDefaultArrayValue()
{
// default array value specified in config -> return default array value
$localDataAttributeName = 'array_attribute_1';
$testEntity = $this->getTestEntity($localDataAttributeName, []);
$this->assertEquals([0], $testEntity->getLocalDataValue($localDataAttributeName));
}
public function testLocalDataMappingAccessDenied()
{
// authorization expression of attribute evaluates to false -> deny access
$localDataAttributeName = 'attribute_3';
try {
$this->getTestEntity($localDataAttributeName, ['src_attribute_2_1' => 'value_2_1', 'src_attribute_2_2' => 'value_2_2']);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_UNAUTHORIZED, $exception->getStatusCode());
}
}
public function testLocalDataQuery()
{
// 'attribute_1' has a configured source attribute 'src_attribute_1'.
// Post-condition: options contain the mapped attribute 'src_attribute_1' as a key with the given value 'value_1'.
$localDataAttributeName = 'attribute_1';
$options = [];
$options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_1';
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
$options = $preEvent->getOptions();
$this->assertArrayHasKey('src_attribute_1', $options);
$this->assertEquals('value_1', $options['src_attribute_1']);
}
public function testLocalDataQueryAttributeUnacknowledgedNotConfigure()
{
// 'attribute_4' has no configured source attribute.
// Throw bad request error because no event subscriber acknowledged local query parameter 'attribute_4'.
$localDataAttributeName = 'attribute_4';
$options = [];
$options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_4';
try {
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_BAD_REQUEST, $exception->getStatusCode());
}
}
public function testLocalDataQueryAttributeUnacknowledgedNotQueryable()
{
// 'attribute_2' is configured 'allow_query': false (default value)
// Throw bad request error because no event subscriber acknowledged local query parameter 'attribute_2'.
$localDataAttributeName = 'attribute_2';
$options = [];
$options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_2';
try {
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_BAD_REQUEST, $exception->getStatusCode());
}
}
public function testLocalDataQueryAccessDenied()
{
// authorization expression of attribute evaluates to false -> deny access
$localDataAttributeName = 'attribute_3';
$options = [];
$options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_1';
try {
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_UNAUTHORIZED, $exception->getStatusCode());
}
}
private function getTestEntity(string $includeLocal, array $sourceData): TestEntity
{
$testEntity = new TestEntity();
$options = [];
$options[LocalData::INCLUDE_PARAMETER_NAME] = $includeLocal;
$this->localDataEventDispatcher->onNewOperation($options);
$this->localDataEventDispatcher->dispatch(new TestEntityPostEvent($testEntity, $sourceData));
return $testEntity;
}
private static function createConfig(): array
{
$config = [];
$config['local_data_mapping'] = [
[
'local_data_attribute' => 'attribute_1',
'source_attribute' => 'src_attribute_1',
'authorization_expression' => 'true',
'allow_query' => true,
'default_value' => 0,
],
[
'local_data_attribute' => 'attribute_2',
'source_attribute' => 'src_attribute_2_1',
'authorization_expression' => 'true',
'allow_query' => false,
],
[
'local_data_attribute' => 'attribute_3',
'source_attribute' => 'src_attribute_3',
'authorization_expression' => 'false',
'allow_query' => true,
],
[
'local_data_attribute' => 'array_attribute_1',
'source_attribute' => 'array_src_attribute_1',
'authorization_expression' => 'true',
'allow_query' => false,
'default_values' => [0],
],
];
return $config;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests\LocalData;
use Dbp\Relay\CoreBundle\LocalData\LocalDataAwareInterface;
use Dbp\Relay\CoreBundle\LocalData\LocalDataAwareTrait;
class TestEntity implements LocalDataAwareInterface
{
use LocalDataAwareTrait;
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests\LocalData;
use Dbp\Relay\CoreBundle\LocalData\AbstractLocalDataPostEventSubscriber;
use Dbp\Relay\CoreBundle\LocalData\LocalDataPreEvent;
class TestEntityLocalDataEventSubscriber extends AbstractLocalDataPostEventSubscriber
{
public static function getSubscribedEventNames(): array
{
return [
TestEntityPostEvent::class,
TestEntityPreEvent::class,
];
}
protected function onPreEvent(LocalDataPreEvent $preEvent, array $localQueryParameters)
{
$preEvent->setOptions($localQueryParameters);
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests\LocalData;
use Dbp\Relay\CoreBundle\LocalData\LocalDataPostEvent;
class TestEntityPostEvent extends LocalDataPostEvent
{
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests\LocalData;
use Dbp\Relay\CoreBundle\LocalData\LocalDataPreEvent;
class TestEntityPreEvent extends LocalDataPreEvent
{
}