You are here

AggregatedFieldsTest.php in Search API 8

File

tests/src/Unit/Processor/AggregatedFieldsTest.php
View source
<?php

namespace Drupal\Tests\search_api\Unit\Processor;

use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\DataType\DataTypeInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\Plugin\search_api\processor\AggregatedFields;
use Drupal\search_api\Plugin\search_api\processor\Property\AggregatedFieldProperty;
use Drupal\search_api\Processor\ProcessorInterface;
use Drupal\search_api\Processor\ProcessorProperty;
use Drupal\search_api\Utility\PluginHelperInterface;
use Drupal\search_api\Utility\Utility;
use Drupal\Tests\search_api\Unit\TestComplexDataInterface;
use Drupal\Tests\UnitTestCase;

/**
 * Tests the "Aggregated fields" processor.
 *
 * @group search_api
 *
 * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedFields
 */
class AggregatedFieldsTest extends UnitTestCase {
  use TestItemsTrait;

  /**
   * The processor to be tested.
   *
   * @var \Drupal\search_api\Plugin\search_api\processor\AggregatedFields
   */
  protected $processor;

  /**
   * A search index mock for the tests.
   *
   * @var \Drupal\search_api\IndexInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $index;

  /**
   * The field ID used in this test.
   *
   * @var string
   */
  protected $fieldId = 'aggregated_field';

  /**
   * The callback with which text values should be preprocessed.
   *
   * @var callable
   */
  protected $valueCallback;

  /**
   * Creates a new processor object for use in the tests.
   */
  protected function setUp() {
    parent::setUp();
    $datasource = $this
      ->createMock(DatasourceInterface::class);
    $datasource
      ->expects($this
      ->any())
      ->method('getPropertyDefinitions')
      ->willReturn([]);
    $this->index = new Index([
      'datasourceInstances' => [
        'entity:test1' => $datasource,
        'entity:test2' => $datasource,
        'entity:test3' => $datasource,
      ],
      'processorInstances' => [],
      'field_settings' => [
        'foo' => [
          'type' => 'string',
          'datasource_id' => 'entity:test1',
          'property_path' => 'foo',
        ],
        'bar' => [
          'type' => 'string',
          'datasource_id' => 'entity:test1',
          'property_path' => 'foo:bar',
        ],
        'bla' => [
          'type' => 'string',
          'datasource_id' => 'entity:test2',
          'property_path' => 'foobaz:bla',
        ],
        'always_empty' => [
          'type' => 'string',
          'datasource_id' => 'entity:test3',
          'property_path' => 'always_empty',
        ],
        'aggregated_field' => [
          'type' => 'text',
          'property_path' => 'aggregated_field',
        ],
      ],
    ], 'search_api_index');
    $this->processor = new AggregatedFields([
      '#index' => $this->index,
    ], 'aggregated_field', []);
    $this->index
      ->addProcessor($this->processor);
    $this
      ->setUpMockContainer();
    $plugin_helper = $this
      ->createMock(PluginHelperInterface::class);
    $this->container
      ->set('search_api.plugin_helper', $plugin_helper);

    // We want to check correct data type handling, so we need a somewhat more
    // complex mock-up for the datatype plugin handler.

    /** @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\search_api\DataType\DataTypePluginManager $data_type_manager */
    $data_type_manager = $this->container
      ->get('plugin.manager.search_api.data_type');
    $data_type_manager
      ->method('hasDefinition')
      ->willReturn(TRUE);
    $this->valueCallback = function ($value) {
      if (is_numeric($value)) {
        return $value + 1;
      }
      else {
        return '*' . $value;
      }
    };
    $data_type = $this
      ->createMock(DataTypeInterface::class);
    $data_type
      ->method('getValue')
      ->willReturnCallback($this->valueCallback);
    $data_type_manager
      ->method('createInstance')
      ->willReturnMap([
      [
        'text',
        [],
        $data_type,
      ],
    ]);
  }

  /**
   * Tests aggregated fields of the given type.
   *
   * @param string $type
   *   The aggregation type to test.
   * @param array $expected
   *   The expected values for the two items.
   * @param bool $integer
   *   (optional) TRUE if the items' normal fields should contain integers,
   *   FALSE otherwise.
   *
   * @dataProvider aggregationTestsDataProvider
   */
  public function testAggregation($type, array $expected, $integer = FALSE) {

    // Add the field configuration.
    $configuration = [
      'type' => $type,
      'fields' => [
        'entity:test1/foo',
        'entity:test1/foo:bar',
        'entity:test2/foobaz:bla',
        'entity:test3/always_empty',
      ],
    ];
    $this->index
      ->getField($this->fieldId)
      ->setConfiguration($configuration);
    if ($integer) {
      $field_values = [
        'foo' => [
          2,
          4,
        ],
        'bar' => [
          16,
        ],
        'bla' => [
          7,
        ],
      ];
    }
    else {
      $field_values = [
        'foo' => [
          'foo',
          'bar',
        ],
        'bar' => [
          'baz',
        ],
        'bla' => [
          'foobar',
        ],
      ];
    }
    $items = [];
    $i = 0;
    foreach ([
      'entity:test1',
      'entity:test2',
      'entity:test3',
    ] as $datasource_id) {
      $this->itemIds[$i++] = $item_id = Utility::createCombinedId($datasource_id, '1:en');
      $item = \Drupal::getContainer()
        ->get('search_api.fields_helper')
        ->createItem($this->index, $item_id);
      foreach ([
        NULL,
        $datasource_id,
      ] as $field_datasource_id) {
        foreach ($this->index
          ->getFieldsByDatasource($field_datasource_id) as $field_id => $field) {
          $field = clone $field;
          if (!empty($field_values[$field_id])) {
            $field
              ->setValues($field_values[$field_id]);
          }
          $item
            ->setField($field_id, $field);
        }
      }
      $item
        ->setFieldsExtracted(TRUE);
      $items[$item_id] = $item;
    }

    // Add the processor's field values to the items.
    foreach ($items as $item) {
      $this->processor
        ->addFieldValues($item);
    }
    $this
      ->assertEquals(array_map($this->valueCallback, $expected[0]), $items[$this->itemIds[0]]
      ->getField($this->fieldId)
      ->getValues(), 'Correct aggregation for item 1.');
    $this
      ->assertEquals(array_map($this->valueCallback, $expected[1]), $items[$this->itemIds[1]]
      ->getField($this->fieldId)
      ->getValues(), 'Correct aggregation for item 2.');
    $this
      ->assertEquals(array_map($this->valueCallback, $expected[2]), $items[$this->itemIds[2]]
      ->getField($this->fieldId)
      ->getValues(), 'Correct aggregation for item 3.');
  }

  /**
   * Provides test data for aggregation tests.
   *
   * @return array
   *   An array containing test data sets, with each being an array of
   *   arguments to pass to the test method.
   *
   * @see static::testAggregation()
   */
  public function aggregationTestsDataProvider() {
    return [
      '"Union" aggregation' => [
        'union',
        [
          [
            'foo',
            'bar',
            'baz',
          ],
          [
            'foobar',
          ],
          [],
        ],
      ],
      '"Concatenation" aggregation' => [
        'concat',
        [
          [
            "foo\n\nbar\n\nbaz",
          ],
          [
            'foobar',
          ],
          [
            '',
          ],
        ],
      ],
      '"Sum" aggregation' => [
        'sum',
        [
          [
            22,
          ],
          [
            7,
          ],
          [
            0,
          ],
        ],
        TRUE,
      ],
      '"Count" aggregation' => [
        'count',
        [
          [
            3,
          ],
          [
            1,
          ],
          [
            0,
          ],
        ],
      ],
      '"Maximum" aggregation' => [
        'max',
        [
          [
            16,
          ],
          [
            7,
          ],
          [],
        ],
        TRUE,
      ],
      '"Minimum" aggregation' => [
        'min',
        [
          [
            2,
          ],
          [
            7,
          ],
          [],
        ],
        TRUE,
      ],
      '"First" aggregation' => [
        'first',
        [
          [
            'foo',
          ],
          [
            'foobar',
          ],
          [],
        ],
      ],
      '"Last" aggregation' => [
        'last',
        [
          [
            'baz',
          ],
          [
            'foobar',
          ],
          [],
        ],
      ],
      '"First letter" aggregation' => [
        'first_char',
        [
          [
            'F',
          ],
          [
            'F',
          ],
          [],
        ],
      ],
    ];
  }

  /**
   * Tests whether the properties are correctly altered.
   *
   * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedFields::getPropertyDefinitions()
   */
  public function testGetPropertyDefinitions() {

    /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
    $translation = $this
      ->getStringTranslationStub();
    $this->processor
      ->setStringTranslation($translation);

    // Check for added properties when no datasource is given.

    /** @var \Drupal\search_api\Processor\ProcessorPropertyInterface[] $properties */
    $properties = $this->processor
      ->getPropertyDefinitions(NULL);
    $this
      ->assertArrayHasKey('aggregated_field', $properties, 'The "aggregated_field" property was added to the properties.');
    $this
      ->assertInstanceOf(AggregatedFieldProperty::class, $properties['aggregated_field'], 'The "aggregated_field" property has the correct class.');
    $this
      ->assertEquals('string', $properties['aggregated_field']
      ->getDataType(), 'Correct data type set in the data definition.');
    $this
      ->assertEquals($translation
      ->translate('Aggregated field'), $properties['aggregated_field']
      ->getLabel(), 'Correct label set in the data definition.');
    $expected_description = $translation
      ->translate('An aggregation of multiple other fields.');
    $this
      ->assertEquals($expected_description, $properties['aggregated_field']
      ->getDescription(), 'Correct description set in the data definition.');

    // Verify that there are no properties if a datasource is given.
    $datasource = $this
      ->createMock(DatasourceInterface::class);
    $properties = $this->processor
      ->getPropertyDefinitions($datasource);
    $this
      ->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
  }

  /**
   * Tests that field extraction in the processor works correctly.
   */
  public function testFieldExtraction() {

    /** @var \Drupal\Tests\search_api\Unit\TestComplexDataInterface|\PHPUnit\Framework\MockObject\MockObject $object */
    $object = $this
      ->createMock(TestComplexDataInterface::class);
    $bar_foo_property = $this
      ->createMock(TypedDataInterface::class);
    $bar_foo_property
      ->method('getValue')
      ->willReturn('value3');
    $bar_foo_property
      ->method('getDataDefinition')
      ->willReturn(new DataDefinition());
    $bar_property = $this
      ->createMock(TestComplexDataInterface::class);
    $bar_property
      ->method('get')
      ->willReturnMap([
      [
        'foo',
        $bar_foo_property,
      ],
    ]);
    $bar_property
      ->method('getProperties')
      ->willReturn([
      'foo' => TRUE,
    ]);
    $foobar_property = $this
      ->createMock(TypedDataInterface::class);
    $foobar_property
      ->method('getValue')
      ->willReturn('wrong_value2');
    $foobar_property
      ->method('getDataDefinition')
      ->willReturn(new DataDefinition());
    $object
      ->method('get')
      ->willReturnMap([
      [
        'bar',
        $bar_property,
      ],
      [
        'foobar',
        $foobar_property,
      ],
    ]);
    $object
      ->method('getProperties')
      ->willReturn([
      'bar' => TRUE,
      'foobar' => TRUE,
    ]);

    /** @var \Drupal\search_api\IndexInterface|\PHPUnit\Framework\MockObject\MockObject $index */
    $index = $this
      ->createMock(IndexInterface::class);
    $fields_helper = \Drupal::getContainer()
      ->get('search_api.fields_helper');
    $field = $fields_helper
      ->createField($index, 'aggregated_field', [
      'property_path' => 'aggregated_field',
      'configuration' => [
        'type' => 'union',
        'fields' => [
          'aggregated_field',
          'foo',
          'entity:test1/bar:foo',
          'entity:test1/baz',
          'entity:test2/foobar',
        ],
      ],
    ]);
    $index
      ->method('getFields')
      ->willReturn([
      'aggregated_field' => $field,
    ]);
    $index
      ->method('getPropertyDefinitions')
      ->willReturnMap([
      [
        NULL,
        [
          'foo' => new ProcessorProperty([
            'processor_id' => 'processor1',
          ]),
        ],
      ],
      [
        'entity:test1',
        [
          'bar' => new DataDefinition(),
          'foobar' => new DataDefinition(),
        ],
      ],
    ]);
    $processor_mock = $this
      ->createMock(ProcessorInterface::class);
    $processor_mock
      ->method('addFieldValues')
      ->willReturnCallback(function (ItemInterface $item) {
      foreach ($item
        ->getFields(FALSE) as $field) {
        if ($field
          ->getCombinedPropertyPath() == 'foo') {
          $field
            ->setValues([
            'value4',
            'value5',
          ]);
        }
      }
    });
    $index
      ->method('getProcessorsByStage')
      ->willReturnMap([
      [
        ProcessorInterface::STAGE_ADD_PROPERTIES,
        [],
        [
          'aggregated_field' => $this->processor,
          'processor1' => $processor_mock,
        ],
      ],
    ]);
    $this->processor
      ->setIndex($index);

    /** @var \Drupal\search_api\Datasource\DatasourceInterface|\PHPUnit\Framework\MockObject\MockObject $datasource */
    $datasource = $this
      ->createMock(DatasourceInterface::class);
    $datasource
      ->method('getPluginId')
      ->willReturn('entity:test1');
    $item = $fields_helper
      ->createItem($index, 'id', $datasource);
    $item
      ->setOriginalObject($object);
    $item
      ->setField('aggregated_field', clone $field);
    $item
      ->setField('test1', $fields_helper
      ->createField($index, 'test1', [
      'property_path' => 'baz',
      'values' => [
        'wrong_value1',
      ],
    ]));
    $item
      ->setField('test2', $fields_helper
      ->createField($index, 'test2', [
      'datasource_id' => 'entity:test1',
      'property_path' => 'baz',
      'values' => [
        'value1',
        'value2',
      ],
    ]));
    $item
      ->setFieldsExtracted(TRUE);
    $this->processor
      ->addFieldValues($item);
    $expected = [
      'value1',
      'value2',
      'value3',
      'value4',
      'value5',
    ];
    $actual = $item
      ->getField('aggregated_field')
      ->getValues();
    sort($actual);
    $this
      ->assertEquals($expected, $actual);
  }

}

Classes

Namesort descending Description
AggregatedFieldsTest Tests the "Aggregated fields" processor.