|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * DISCLAIMER |
| 4 | + * |
| 5 | + * Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer versions in the future. |
| 6 | + * |
| 7 | + * @category Smile |
| 8 | + * @package Smile\ElasticsuiteCore |
| 9 | + * @author Richard Bayet <[email protected]> |
| 10 | + * @copyright 2026 Smile |
| 11 | + * @license Open Software License ("OSL") v. 3.0 |
| 12 | + */ |
| 13 | + |
| 14 | +namespace Smile\ElasticsuiteCore\Test\Unit\Index\Indices; |
| 15 | + |
| 16 | +use Magento\Framework\Config\CacheInterface; |
| 17 | +use Magento\Framework\Serialize\SerializerInterface; |
| 18 | +use Smile\ElasticsuiteCore\Index\Indices\Config\Converter; |
| 19 | +use Smile\ElasticsuiteCore\Api\Index\MappingInterfaceFactory as MappingFactory; |
| 20 | +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterfaceFactory as MappingFieldFactory; |
| 21 | +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface; |
| 22 | +use Smile\ElasticsuiteCore\Api\Index\DataSourceResolverInterfaceFactory as DataSourceResolverFactory; |
| 23 | +use Smile\ElasticsuiteCore\Api\Index\Mapping\DynamicFieldProviderInterface; |
| 24 | +use Smile\ElasticsuiteCore\Index\Mapping; |
| 25 | +use Smile\ElasticsuiteCore\Index\Mapping\Field; |
| 26 | +use Smile\ElasticsuiteCore\Index\DataSourceResolver; |
| 27 | +use Smile\ElasticsuiteCore\Index\Indices\Config; |
| 28 | + |
| 29 | +/** |
| 30 | + * Indices configuration test case. |
| 31 | + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
| 32 | + * |
| 33 | + * @category Smile |
| 34 | + * @package Smile\ElasticsuiteCore |
| 35 | + */ |
| 36 | +class ConfigTest extends \PHPUnit\Framework\TestCase |
| 37 | +{ |
| 38 | + /** |
| 39 | + * @var array |
| 40 | + */ |
| 41 | + protected $parsedData; |
| 42 | + |
| 43 | + /** |
| 44 | + * Tests that the indices configuration is correctly built and returned. |
| 45 | + * @dataProvider stringFieldDynamicConfigProvider |
| 46 | + * |
| 47 | + * Verifies that: |
| 48 | + * - The configuration is not empty. |
| 49 | + * - Both 'index1' and 'index2' are present in the configuration. |
| 50 | + * - 'index2' contains a 'mapping' key holding a valid {@see Mapping} instance. |
| 51 | + * - The mapping for 'index2' contains fields. |
| 52 | + * - The 'stringField' field in 'index2' mapping is a valid {@see Field} instance with |
| 53 | + * - its type set to 'string', |
| 54 | + * - its searchability set to true, |
| 55 | + * whatever the dynamic configuration provided, confirming that the statically configured field type |
| 56 | + * and field properties takes precedence over the dynamically provided field type and/or properties. |
| 57 | + * |
| 58 | + * @param array $stringFieldDynamicConfig Dynamic configuration for the stringField field in 'index2'. |
| 59 | + * |
| 60 | + * @return void |
| 61 | + */ |
| 62 | + public function testConfig($stringFieldDynamicConfig) |
| 63 | + { |
| 64 | + $config = new Config( |
| 65 | + $this->getConfigReaderMock(), |
| 66 | + $this->getCacheMock(), |
| 67 | + $this->getMappingFactoryMock(), |
| 68 | + $this->getMappingFieldFactoryMock(), |
| 69 | + $this->getDataSourceResolverFactoryMock($stringFieldDynamicConfig), |
| 70 | + $this->getSerializerMock() |
| 71 | + ); |
| 72 | + |
| 73 | + $indicesConfig = $config->get(); |
| 74 | + $this->assertNotEmpty($indicesConfig); |
| 75 | + |
| 76 | + $this->assertArrayHasKey('index1', $indicesConfig); |
| 77 | + $this->assertArrayHasKey('index2', $indicesConfig); |
| 78 | + $this->assertArrayHasKey('mapping', $indicesConfig['index2']); |
| 79 | + $this->assertInstanceOf(Mapping::class, $indicesConfig['index2']['mapping']); |
| 80 | + $this->assertNotEmpty($indicesConfig['index2']['mapping']->getFields()); |
| 81 | + |
| 82 | + $stringField = $indicesConfig['index2']['mapping']->getField('stringField'); |
| 83 | + $this->assertInstanceOf(Field::class, $stringField); |
| 84 | + $this->assertEquals('string', $stringField->getType()); |
| 85 | + $this->assertTrue($stringField->isSearchable()); |
| 86 | + |
| 87 | + if (array_key_exists('fieldConfig', $stringFieldDynamicConfig)) { |
| 88 | + $keptDynamicConfig = array_diff_key($stringFieldDynamicConfig['fieldConfig'], ['is_searchable' => false]); |
| 89 | + if (!empty($keptDynamicConfig)) { |
| 90 | + $fieldConfig = array_diff_key($stringField->getConfig(), ['is_searchable' => false]); |
| 91 | + foreach ($keptDynamicConfig as $key => $value) { |
| 92 | + $this->assertEquals($value, $fieldConfig[$key], 'Kept dynamic field property'); |
| 93 | + } |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * Provides dynamic configuration sets for the 'stringField' field in 'index2'. |
| 100 | + * |
| 101 | + * @return array |
| 102 | + */ |
| 103 | + public function stringFieldDynamicConfigProvider() |
| 104 | + { |
| 105 | + return [ |
| 106 | + // Dynamic configuration for the stringField field in 'index2'. |
| 107 | + [['type' => FieldInterface::FIELD_TYPE_KEYWORD]], |
| 108 | + [['type' => FieldInterface::FIELD_TYPE_TEXT]], |
| 109 | + [['fieldConfig' => ['is_searchable' => false, 'default_search_analyzer' => FieldInterface::ANALYZER_SHINGLE]]], |
| 110 | + [['type' => FieldInterface::FIELD_TYPE_KEYWORD, 'fieldConfig' => ['is_searchable' => false]]], |
| 111 | + [ |
| 112 | + [ |
| 113 | + 'type' => FieldInterface::FIELD_TYPE_KEYWORD, |
| 114 | + 'fieldConfig' => ['default_search_analyzer' => FieldInterface::ANALYZER_EDGE_NGRAM], |
| 115 | + ], |
| 116 | + ], |
| 117 | + ]; |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * {@inheritDoc} |
| 122 | + */ |
| 123 | + protected function setUp(): void |
| 124 | + { |
| 125 | + $xml = new \DOMDocument(); |
| 126 | + $xml->load(__DIR__ . '/Config/elasticsuite_indices.xml'); |
| 127 | + $converter = new Converter(); |
| 128 | + $this->parsedData = $converter->convert($xml); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Creates and returns a mock of the indices configuration reader |
| 133 | + * with data coming from the sample elasticsuite_indices.xml already parsed in the setUp method. |
| 134 | + * |
| 135 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of Config\Reader. |
| 136 | + */ |
| 137 | + protected function getConfigReaderMock() |
| 138 | + { |
| 139 | + $reader = $this->getMockBuilder(Config\Reader::class) |
| 140 | + ->disableOriginalConstructor() |
| 141 | + ->getMock(); |
| 142 | + $reader->method('read')->willReturn($this->parsedData); |
| 143 | + |
| 144 | + return $reader; |
| 145 | + } |
| 146 | + |
| 147 | + /** |
| 148 | + * Creates and returns a mock of the cache interface. |
| 149 | + * |
| 150 | + * The mock is configured so that the `load` method always returns false, |
| 151 | + * simulating a cache miss to ensure the configuration is always read |
| 152 | + * from the reader rather than from the cache. |
| 153 | + * |
| 154 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of CacheInterface. |
| 155 | + */ |
| 156 | + protected function getCacheMock() |
| 157 | + { |
| 158 | + $cache = $this->getMockBuilder(CacheInterface::class) |
| 159 | + ->disableOriginalConstructor() |
| 160 | + ->getMock(); |
| 161 | + |
| 162 | + $cache->method('load')->willReturn(false); |
| 163 | + |
| 164 | + return $cache; |
| 165 | + } |
| 166 | + |
| 167 | + /** |
| 168 | + * Creates and returns a mock of the mapping factory. |
| 169 | + * |
| 170 | + * The mock is configured so that the `create` method instantiates and returns |
| 171 | + * a real {@see Mapping} object using the provided arguments, allowing the factory |
| 172 | + * to behave as closely as possible to the actual implementation during testing. |
| 173 | + * |
| 174 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of MappingFactory. |
| 175 | + */ |
| 176 | + protected function getMappingFactoryMock() |
| 177 | + { |
| 178 | + $mappingFactory = $this->getMockBuilder(MappingFactory::class) |
| 179 | + ->disableOriginalConstructor() |
| 180 | + ->getMock(); |
| 181 | + |
| 182 | + $mappingFactory->method('create')->willReturnCallback(function ($args) { |
| 183 | + return new Mapping(...array_values($args)); |
| 184 | + }); |
| 185 | + |
| 186 | + return $mappingFactory; |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Creates and returns a mock of the mapping field factory. |
| 191 | + * |
| 192 | + * The mock is configured so that the `create` method instantiates and returns |
| 193 | + * a real {@see Field} object using the provided arguments, allowing the factory |
| 194 | + * to behave as closely as possible to the actual implementation during testing. |
| 195 | + * |
| 196 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of MappingFieldFactory. |
| 197 | + */ |
| 198 | + protected function getMappingFieldFactoryMock() |
| 199 | + { |
| 200 | + $mappingFieldFactory = $this->getMockBuilder(MappingFieldFactory::class) |
| 201 | + ->disableOriginalConstructor() |
| 202 | + ->getMock(); |
| 203 | + |
| 204 | + $mappingFieldFactory->method('create')->willReturnCallback(function ($args) { |
| 205 | + return new Field(...$args); |
| 206 | + }); |
| 207 | + |
| 208 | + return $mappingFieldFactory; |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * Creates and returns a mock of the data source resolver factory. |
| 213 | + * |
| 214 | + * The mock is configured so that the `create` method returns a mock of |
| 215 | + * {@see DataSourceResolver}, allowing the factory to simulate the creation |
| 216 | + * of data source resolver instances during testing without relying on the |
| 217 | + * actual implementation or its dependencies. |
| 218 | + * |
| 219 | + * @param array $stringFieldDynamicConfig Dynamic configuration for the stringField field in 'index2'. |
| 220 | + * |
| 221 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of DataSourceResolverFactory. |
| 222 | + */ |
| 223 | + protected function getDataSourceResolverFactoryMock($stringFieldDynamicConfig) |
| 224 | + { |
| 225 | + $dataSourceResolverFactory = $this->getMockBuilder(DataSourceResolverFactory::class) |
| 226 | + ->disableOriginalConstructor() |
| 227 | + ->getMock(); |
| 228 | + |
| 229 | + $dataSourceResolverFactory->method('create')->willReturnCallback( |
| 230 | + function () use ($stringFieldDynamicConfig) { |
| 231 | + return $this->getDataSourceResolverMock($stringFieldDynamicConfig); |
| 232 | + } |
| 233 | + ); |
| 234 | + |
| 235 | + return $dataSourceResolverFactory; |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Creates and returns a mock of the data source resolver. |
| 240 | + * |
| 241 | + * The mock is configured so that the `getDataSources` method returns different |
| 242 | + * results based on the provided index name: |
| 243 | + * - For 'index2', it returns an array containing a dynamic data source mock. |
| 244 | + * - For any other index name (e.g. 'index1'), it returns an empty array. |
| 245 | + * |
| 246 | + * Note: The test XML file declares two types under 'index1', which is no longer |
| 247 | + * supported. This would cause the mapping for 'index1' to be based on the last |
| 248 | + * declared type, which does not include the static field 'stringField'. Returning |
| 249 | + * an empty array for 'index1' reflects this unsupported scenario. |
| 250 | + * |
| 251 | + * @param array $stringFieldDynamicConfig Dynamic configuration for the stringField field in 'index2'. |
| 252 | + * |
| 253 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of DataSourceResolver. |
| 254 | + */ |
| 255 | + protected function getDataSourceResolverMock($stringFieldDynamicConfig) |
| 256 | + { |
| 257 | + $dataSourceResolver = $this->getMockBuilder(DataSourceResolver::class) |
| 258 | + ->disableOriginalConstructor() |
| 259 | + ->getMock(); |
| 260 | + |
| 261 | + $dataSourceResolver->method('getDataSources')->willReturnCallback( |
| 262 | + function ($indexName) use ($stringFieldDynamicConfig) { |
| 263 | + /* |
| 264 | + * Test XML file has two types under 'index1' which is not supposed to be supported anymore, |
| 265 | + * and would lead to the mapping for 'index1' being the one of the last declared type which |
| 266 | + * does not contain the static field 'stringField' |
| 267 | + */ |
| 268 | + if ($indexName === 'index2') { |
| 269 | + return [$this->getDynamicDatasourceMock($stringFieldDynamicConfig)]; |
| 270 | + } |
| 271 | + |
| 272 | + return []; |
| 273 | + } |
| 274 | + ); |
| 275 | + |
| 276 | + return $dataSourceResolver; |
| 277 | + } |
| 278 | + |
| 279 | + /** |
| 280 | + * Creates and returns a mock of a dynamic data source. |
| 281 | + * |
| 282 | + * The mock is configured so that the `getFields` method returns an array |
| 283 | + * containing a single dynamic field named 'stringField', typed as {@see FieldInterface::FIELD_TYPE_KEYWORD}. |
| 284 | + * This type intentionally differs from the type declared in the elasticsuite_indices.xml configuration file, |
| 285 | + * allowing tests to verify that dynamic fields provided at runtime can supplement statically configured fields |
| 286 | + * but that statically configured fields take precedence. |
| 287 | + * |
| 288 | + * @param array $stringFieldDynamicConfig Dynamic configuration for the stringField field in 'index2'. |
| 289 | + * |
| 290 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of DynamicFieldProviderInterface. |
| 291 | + */ |
| 292 | + protected function getDynamicDatasourceMock($stringFieldDynamicConfig) |
| 293 | + { |
| 294 | + // Mock dynamic field with a different type than in the elasticsuite_indices.xml config file. |
| 295 | + $dynamicFields = [ |
| 296 | + 'stringField' => $this->getMappingFieldFactoryMock()->create( |
| 297 | + ['name' => 'stringField'] + $stringFieldDynamicConfig |
| 298 | + ), |
| 299 | + ]; |
| 300 | + |
| 301 | + $datasource = $this->getMockBuilder(DynamicFieldProviderInterface::class) |
| 302 | + ->disableOriginalConstructor() |
| 303 | + ->getMock(); |
| 304 | + |
| 305 | + $datasource->method('getFields')->willReturn($dynamicFields); |
| 306 | + |
| 307 | + return $datasource; |
| 308 | + } |
| 309 | + |
| 310 | + /** |
| 311 | + * Creates and returns a mock of the serializer interface. |
| 312 | + * |
| 313 | + * @return \PHPUnit\Framework\MockObject\MockObject Mock instance of SerializerInterface. |
| 314 | + */ |
| 315 | + protected function getSerializerMock() |
| 316 | + { |
| 317 | + return $this->getMockBuilder(SerializerInterface::class) |
| 318 | + ->disableOriginalConstructor() |
| 319 | + ->getMock(); |
| 320 | + } |
| 321 | +} |
0 commit comments