View source
<?php
namespace Drupal\Tests\jsonapi\Kernel\Query;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
use Drupal\jsonapi\Context\FieldResolver;
use Drupal\jsonapi\Query\Filter;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
use Prophecy\Argument;
class FilterTest extends JsonapiKernelTestBase {
use ImageFieldCreationTrait;
public static $modules = [
'field',
'file',
'image',
'jsonapi',
'node',
'serialization',
'system',
'text',
'user',
];
protected $nodeStorage;
protected $resourceTypeRepository;
public function setUp() {
parent::setUp();
$this
->setUpSchemas();
$this
->savePaintingType();
$this
->savePaintings([
[
'colors' => [
'red',
],
'shapes' => [
'triangle',
],
'title' => 'FIND',
],
[
'colors' => [
'orange',
],
'shapes' => [
'circle',
],
'title' => 'FIND',
],
[
'colors' => [
'orange',
],
'shapes' => [
'triangle',
],
'title' => 'DO_NOT_FIND',
],
[
'colors' => [
'yellow',
],
'shapes' => [
'square',
],
'title' => 'FIND',
],
[
'colors' => [
'yellow',
],
'shapes' => [
'triangle',
],
'title' => 'DO_NOT_FIND',
],
[
'colors' => [
'orange',
],
'shapes' => [
'square',
],
'title' => 'DO_NOT_FIND',
],
]);
$this->nodeStorage = $this->container
->get('entity_type.manager')
->getStorage('node');
$this->fieldResolver = $this->container
->get('jsonapi.field_resolver');
$this->resourceTypeRepository = $this->container
->get('jsonapi.resource_type.repository');
}
public function testInvalidFilterPathDueToMissingPropertyName() {
$this
->expectException(CacheableBadRequestHttpException::class);
$this
->expectExceptionMessage('Invalid nested filtering. The field `colors`, given in the path `colors` is incomplete, it must end with one of the following specifiers: `value`, `format`, `processed`.');
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
Filter::createFromQueryParameter([
'colors' => '',
], $resource_type, $this->fieldResolver);
}
public function testInvalidFilterPathDueToMissingPropertyNameReferenceFieldWithMetaProperties() {
$this
->expectException(CacheableBadRequestHttpException::class);
$this
->expectExceptionMessage('Invalid nested filtering. The field `photo`, given in the path `photo` is incomplete, it must end with one of the following specifiers: `id`, `meta.alt`, `meta.title`, `meta.width`, `meta.height`.');
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
Filter::createFromQueryParameter([
'photo' => '',
], $resource_type, $this->fieldResolver);
}
public function testInvalidFilterPathDueMissingMetaPrefixReferenceFieldWithMetaProperties() {
$this
->expectException(CacheableBadRequestHttpException::class);
$this
->expectExceptionMessage('Invalid nested filtering. The property `alt`, given in the path `photo.alt` belongs to the meta object of a relationship and must be preceded by `meta`.');
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
Filter::createFromQueryParameter([
'photo.alt' => '',
], $resource_type, $this->fieldResolver);
}
public function testInvalidFilterPathDueToMissingPropertyNameReferenceFieldWithoutMetaProperties() {
$this
->expectException(CacheableBadRequestHttpException::class);
$this
->expectExceptionMessage('Invalid nested filtering. The field `uid`, given in the path `uid` is incomplete, it must end with one of the following specifiers: `id`.');
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
Filter::createFromQueryParameter([
'uid' => '',
], $resource_type, $this->fieldResolver);
}
public function testInvalidFilterPathDueToNonexistentProperty() {
$this
->expectException(CacheableBadRequestHttpException::class);
$this
->expectExceptionMessage('Invalid nested filtering. The property `foobar`, given in the path `colors.foobar`, does not exist. Must be one of the following property names: `value`, `format`, `processed`.');
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
Filter::createFromQueryParameter([
'colors.foobar' => '',
], $resource_type, $this->fieldResolver);
}
public function testInvalidFilterPathDueToElidedSoleProperty() {
$this
->expectException(CacheableBadRequestHttpException::class);
$this
->expectExceptionMessage('Invalid nested filtering. The property `value`, given in the path `promote.value`, does not exist. Filter by `promote`, not `promote.value` (the JSON:API module elides property names from single-property fields).');
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
Filter::createFromQueryParameter([
'promote.value' => '',
], $resource_type, $this->fieldResolver);
}
public function testQueryCondition() {
$data = $this
->queryConditionData();
$get_sql_query_for_entity_query = function ($entity_query) {
$o = new \ReflectionObject($entity_query);
$m1 = $o
->getMethod('prepare');
$m1
->setAccessible(TRUE);
$m2 = $o
->getMethod('compile');
$m2
->setAccessible(TRUE);
$p = $o
->getProperty('sqlQuery');
$p
->setAccessible(TRUE);
$m1
->invoke($entity_query);
$m2
->invoke($entity_query);
return (string) $p
->getValue($entity_query);
};
$resource_type = $this->resourceTypeRepository
->get('node', 'painting');
foreach ($data as $case) {
$parameter = $case[0];
$expected_query = $case[1];
$filter = Filter::createFromQueryParameter($parameter, $resource_type, $this->fieldResolver);
$query = $this->nodeStorage
->getQuery();
$condition = $filter
->queryCondition($query);
$query
->condition($condition);
$expected_sql_query = $get_sql_query_for_entity_query($expected_query);
$actual_sql_query = $get_sql_query_for_entity_query($query);
$this
->assertSame($expected_sql_query, $actual_sql_query);
$this
->assertEquals($expected_query
->execute(), $query
->execute());
}
}
protected function queryConditionData() {
$query = $this->nodeStorage
->getQuery();
$or_group = $query
->orConditionGroup();
$nested_or_group = $query
->orConditionGroup();
$nested_or_group
->condition('colors', 'red', 'CONTAINS');
$nested_or_group
->condition('shapes', 'circle', 'CONTAINS');
$or_group
->condition($nested_or_group);
$nested_and_group = $query
->andConditionGroup();
$nested_and_group
->condition('colors', 'yellow', 'CONTAINS');
$nested_and_group
->condition('shapes', 'square', 'CONTAINS');
$nested_and_group
->notExists('photo.alt');
$or_group
->condition($nested_and_group);
$query
->condition($or_group);
return [
[
[
'or-group' => [
'group' => [
'conjunction' => 'OR',
],
],
'nested-or-group' => [
'group' => [
'conjunction' => 'OR',
'memberOf' => 'or-group',
],
],
'nested-and-group' => [
'group' => [
'conjunction' => 'AND',
'memberOf' => 'or-group',
],
],
'condition-0' => [
'condition' => [
'path' => 'colors.value',
'value' => 'red',
'operator' => 'CONTAINS',
'memberOf' => 'nested-or-group',
],
],
'condition-1' => [
'condition' => [
'path' => 'shapes.value',
'value' => 'circle',
'operator' => 'CONTAINS',
'memberOf' => 'nested-or-group',
],
],
'condition-2' => [
'condition' => [
'path' => 'colors.value',
'value' => 'yellow',
'operator' => 'CONTAINS',
'memberOf' => 'nested-and-group',
],
],
'condition-3' => [
'condition' => [
'path' => 'shapes.value',
'value' => 'square',
'operator' => 'CONTAINS',
'memberOf' => 'nested-and-group',
],
],
'condition-4' => [
'condition' => [
'path' => 'photo.meta.alt',
'operator' => 'IS NULL',
'memberOf' => 'nested-and-group',
],
],
],
$query,
],
];
}
protected function setUpSchemas() {
$this
->installSchema('system', [
'sequences',
]);
$this
->installSchema('node', [
'node_access',
]);
$this
->installSchema('user', [
'users_data',
]);
$this
->installSchema('user', []);
foreach ([
'user',
'node',
] as $entity_type_id) {
$this
->installEntitySchema($entity_type_id);
}
}
protected function savePaintingType() {
NodeType::create([
'type' => 'painting',
])
->save();
$this
->createTextField('node', 'painting', 'colors', 'Colors', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$this
->createTextField('node', 'painting', 'shapes', 'Shapes', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$this
->createImageField('photo', 'painting');
}
protected function savePaintings($paintings) {
foreach ($paintings as $painting) {
Node::create(array_merge([
'type' => 'painting',
], $painting))
->save();
}
}
public function testCreateFromQueryParameter($case, $expected) {
$resource_type = new ResourceType('foo', 'bar', NULL);
$actual = Filter::createFromQueryParameter($case, $resource_type, $this
->getFieldResolverMock($resource_type));
$conditions = $actual
->root()
->members();
for ($i = 0; $i < count($case); $i++) {
$this
->assertEquals($expected[$i]['path'], $conditions[$i]
->field());
$this
->assertEquals($expected[$i]['value'], $conditions[$i]
->value());
$this
->assertEquals($expected[$i]['operator'], $conditions[$i]
->operator());
}
}
public function parameterProvider() {
return [
'shorthand' => [
[
'uid' => [
'value' => 1,
],
],
[
[
'path' => 'uid',
'value' => 1,
'operator' => '=',
],
],
],
'extreme shorthand' => [
[
'uid' => 1,
],
[
[
'path' => 'uid',
'value' => 1,
'operator' => '=',
],
],
],
];
}
public function testCreateFromQueryParameterNested() {
$parameter = [
'or-group' => [
'group' => [
'conjunction' => 'OR',
],
],
'nested-or-group' => [
'group' => [
'conjunction' => 'OR',
'memberOf' => 'or-group',
],
],
'nested-and-group' => [
'group' => [
'conjunction' => 'AND',
'memberOf' => 'or-group',
],
],
'condition-0' => [
'condition' => [
'path' => 'field0',
'value' => 'value0',
'memberOf' => 'nested-or-group',
],
],
'condition-1' => [
'condition' => [
'path' => 'field1',
'value' => 'value1',
'memberOf' => 'nested-or-group',
],
],
'condition-2' => [
'condition' => [
'path' => 'field2',
'value' => 'value2',
'memberOf' => 'nested-and-group',
],
],
'condition-3' => [
'condition' => [
'path' => 'field3',
'value' => 'value3',
'memberOf' => 'nested-and-group',
],
],
];
$resource_type = new ResourceType('foo', 'bar', NULL);
$filter = Filter::createFromQueryParameter($parameter, $resource_type, $this
->getFieldResolverMock($resource_type));
$root = $filter
->root();
$this
->assertEquals($root
->conjunction(), 'AND');
$members = $root
->members();
$or_group = $members[0];
$this
->assertEquals($or_group
->conjunction(), 'OR');
$or_group_members = $or_group
->members();
$nested_or_group = $or_group_members[0];
$this
->assertEquals($nested_or_group
->conjunction(), 'OR');
$nested_or_group_members = $nested_or_group
->members();
$this
->assertEquals($nested_or_group_members[0]
->field(), 'field0');
$this
->assertEquals($nested_or_group_members[1]
->field(), 'field1');
$nested_and_group = $or_group_members[1];
$this
->assertEquals($nested_and_group
->conjunction(), 'AND');
$nested_and_group_members = $nested_and_group
->members();
$this
->assertEquals($nested_and_group_members[0]
->field(), 'field2');
$this
->assertEquals($nested_and_group_members[1]
->field(), 'field3');
}
protected function getFieldResolverMock(ResourceType $resource_type) {
$field_resolver = $this
->prophesize(FieldResolver::class);
$field_resolver
->resolveInternalEntityQueryPath($resource_type, Argument::any(), Argument::any())
->willReturnArgument(1);
return $field_resolver
->reveal();
}
}