Skip to content

Commit 53c42ac

Browse files
committed
[Core] Unit test for XML precedence over datasource
1 parent 22de1c1 commit 53c42ac

2 files changed

Lines changed: 325 additions & 2 deletions

File tree

src/module-elasticsuite-core/Test/Unit/Index/Indices/Config/elasticsuite_indices.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@
3636
</mapping>
3737
</type>
3838
</index>
39-
39+
4040
<index identifier="index2" defaultSearchType="simpleType">
4141
<type name="simpleType" idFieldName="idField">
4242
<mapping>
4343
<field name="idField" type="integer" />
44-
<field name="stringField" type="string" />
44+
<field name="stringField" type="string">
45+
<isSearchable>1</isSearchable>
46+
</field>
4547
</mapping>
4648
</type>
4749
</index>
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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

Comments
 (0)