Skip to content

Commit d8c17df

Browse files
committed
AC-14011: Admin product page error when all product attributes are set to global scope
Add unit test coverage
1 parent de5d02f commit d8c17df

File tree

2 files changed

+380
-0
lines changed

2 files changed

+380
-0
lines changed

app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public function containsValue($entityType, $entity, $attributeCode, $storeId)
105105
* @return array
106106
*
107107
* @deprecated 101.0.0
108+
* @see MAGETWO-71174
108109
*/
109110
public function getDefaultValues($entityType, $entity)
110111
{
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Model\Attribute;
9+
10+
use Magento\Catalog\Model\AbstractModel;
11+
use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
12+
use Magento\Eav\Api\AttributeRepositoryInterface;
13+
use Magento\Eav\Api\Data\AttributeSearchResultsInterface;
14+
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
15+
use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;
16+
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
17+
use Magento\Framework\Api\FilterBuilder;
18+
use Magento\Framework\Api\SearchCriteriaBuilder;
19+
use Magento\Framework\App\ResourceConnection;
20+
use Magento\Framework\DataObject;
21+
use Magento\Framework\DB\Adapter\AdapterInterface;
22+
use Magento\Framework\DB\Select;
23+
use Magento\Framework\EntityManager\EntityMetadataInterface;
24+
use Magento\Framework\EntityManager\MetadataPool;
25+
use Magento\Store\Model\Store;
26+
use PHPUnit\Framework\MockObject\MockObject;
27+
use PHPUnit\Framework\TestCase;
28+
29+
/**
30+
* Unit test for ScopeOverriddenValue class with 100% coverage
31+
*/
32+
class ScopeOverriddenValueTest extends TestCase
33+
{
34+
/**
35+
* Test containsValue method with default store ID (early return)
36+
*/
37+
public function testContainsValueWithDefaultStoreId(): void
38+
{
39+
$model = $this->createScopeOverriddenValue();
40+
$entityMock = $this->createMock(AbstractModel::class);
41+
42+
$result = $model->containsValue('entity_type', $entityMock, 'attribute_code', Store::DEFAULT_STORE_ID);
43+
$this->assertFalse($result);
44+
}
45+
46+
/**
47+
* Test our main scenario: empty selects array when all attributes are global
48+
*/
49+
public function testInitAttributeValuesWithEmptySelectsArray(): void
50+
{
51+
$model = $this->createScopeOverriddenValueWithEmptyAttributes();
52+
$entityMock = $this->createMockEntity();
53+
54+
// This should not throw exception due to our fix
55+
$result = $model->containsValue('entity_type', $entityMock, 'attr', 1);
56+
$this->assertFalse($result);
57+
58+
// Test getDefaultValues as well
59+
$defaultValues = $model->getDefaultValues('entity_type', $entityMock);
60+
$this->assertIsArray($defaultValues);
61+
$this->assertEmpty($defaultValues);
62+
}
63+
64+
/**
65+
* Test initAttributeValues with no EAV entity type
66+
*/
67+
public function testInitAttributeValuesWithoutEavEntityType(): void
68+
{
69+
$model = $this->createScopeOverriddenValueWithoutEavEntityType();
70+
$entityMock = $this->createMockEntity();
71+
72+
$result = $model->containsValue('entity_type', $entityMock, 'attr', 1);
73+
$this->assertFalse($result);
74+
}
75+
76+
/**
77+
* Test initAttributeValues with static attributes (should be skipped)
78+
*/
79+
public function testInitAttributeValuesWithStaticAttributes(): void
80+
{
81+
$model = $this->createScopeOverriddenValueWithStaticAttributes();
82+
$entityMock = $this->createMockEntity();
83+
84+
$result = $model->containsValue('entity_type', $entityMock, 'attr', 1);
85+
$this->assertFalse($result);
86+
}
87+
88+
/**
89+
* Test full flow with non-static attributes and database query
90+
*/
91+
public function testInitAttributeValuesWithNonStaticAttributes(): void
92+
{
93+
$model = $this->createScopeOverriddenValueWithNonStaticAttributes();
94+
$entityMock = $this->createMockEntity();
95+
96+
// Test containsValue - should populate cache and return true for existing attribute
97+
$result1 = $model->containsValue('entity_type', $entityMock, 'test_attr', 1);
98+
$this->assertTrue($result1);
99+
100+
// Test containsValue with cached values - second call should use cache
101+
$result2 = $model->containsValue('entity_type', $entityMock, 'test_attr', 1);
102+
$this->assertTrue($result2);
103+
104+
// Test containsValue for non-existent attribute
105+
$result3 = $model->containsValue('entity_type', $entityMock, 'nonexistent', 1);
106+
$this->assertFalse($result3);
107+
108+
// Test getDefaultValues
109+
$defaultValues = $model->getDefaultValues('entity_type', $entityMock);
110+
$this->assertIsArray($defaultValues);
111+
$this->assertEquals(['test_attr' => 'test_value'], $defaultValues);
112+
}
113+
114+
/**
115+
* Test getDefaultValues when store ID needs to be fetched from entity
116+
*/
117+
public function testGetDefaultValuesWithEntityStoreId(): void
118+
{
119+
$model = $this->createScopeOverriddenValueWithNonStaticAttributes();
120+
$entityMock = $this->createMockEntity();
121+
122+
$result = $model->getDefaultValues('entity_type', $entityMock);
123+
$this->assertIsArray($result);
124+
}
125+
126+
/**
127+
* Test clearAttributesValues when cache exists
128+
*/
129+
public function testClearAttributesValuesWithExistingCache(): void
130+
{
131+
$model = $this->createScopeOverriddenValueWithNonStaticAttributes();
132+
$entityMock = $this->createMockEntity();
133+
134+
// First populate cache
135+
$model->containsValue('entity_type', $entityMock, 'test_attr', 1);
136+
137+
// Now clear cache - should work without exception
138+
$entity = new DataObject(['entity_id' => 1]);
139+
$model->clearAttributesValues('entity_type', $entity);
140+
$this->assertTrue(true);
141+
}
142+
143+
/**
144+
* Test clearAttributesValues when no cache exists
145+
*/
146+
public function testClearAttributesValuesWithNoCache(): void
147+
{
148+
$model = $this->createScopeOverriddenValue();
149+
$entity = new DataObject(['entity_id' => 1]);
150+
151+
// Should not throw exception when no cache exists
152+
$model->clearAttributesValues('entity_type', $entity);
153+
$this->assertTrue(true);
154+
}
155+
156+
/**
157+
* Test with non-default store ID to cover store ID logic
158+
*/
159+
public function testWithNonDefaultStoreId(): void
160+
{
161+
$model = $this->createScopeOverriddenValueWithNonStaticAttributes();
162+
$entityMock = $this->createMockEntity();
163+
164+
// Test with store ID 2
165+
$result = $model->containsValue('entity_type', $entityMock, 'test_attr', 2);
166+
$this->assertTrue($result);
167+
}
168+
169+
/**
170+
* Create basic ScopeOverriddenValue instance
171+
*/
172+
private function createScopeOverriddenValue(): ScopeOverriddenValue
173+
{
174+
return new ScopeOverriddenValue(
175+
$this->createMock(AttributeRepositoryInterface::class),
176+
$this->createMock(MetadataPool::class),
177+
$this->createMock(SearchCriteriaBuilder::class),
178+
$this->createMock(FilterBuilder::class),
179+
$this->createMock(ResourceConnection::class)
180+
);
181+
}
182+
183+
/**
184+
* Create ScopeOverriddenValue with empty attributes (global scope scenario)
185+
*/
186+
private function createScopeOverriddenValueWithEmptyAttributes(): ScopeOverriddenValue
187+
{
188+
$attributeRepositoryMock = $this->createMock(AttributeRepositoryInterface::class);
189+
$metadataPoolMock = $this->createMock(MetadataPool::class);
190+
$searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class);
191+
$filterBuilderMock = $this->createMock(FilterBuilder::class);
192+
$resourceConnectionMock = $this->createMock(ResourceConnection::class);
193+
194+
// Setup metadata
195+
$metadataMock = $this->createMock(EntityMetadataInterface::class);
196+
$metadataMock->method('getEavEntityType')->willReturn('catalog_product');
197+
$metadataMock->method('getLinkField')->willReturn('entity_id');
198+
$metadataPoolMock->method('getMetadata')->willReturn($metadataMock);
199+
200+
// Setup empty attributes
201+
$searchResultsMock = $this->createMock(AttributeSearchResultsInterface::class);
202+
$searchResultsMock->method('getItems')->willReturn([]);
203+
204+
// Setup search criteria
205+
$filterMock = $this->createMock(\Magento\Framework\Api\Filter::class);
206+
$searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class);
207+
208+
$filterBuilderMock->method('setField')->willReturnSelf();
209+
$filterBuilderMock->method('setConditionType')->willReturnSelf();
210+
$filterBuilderMock->method('setValue')->willReturnSelf();
211+
$filterBuilderMock->method('create')->willReturn($filterMock);
212+
213+
$searchCriteriaBuilderMock->method('addFilters')->willReturnSelf();
214+
$searchCriteriaBuilderMock->method('create')->willReturn($searchCriteriaMock);
215+
216+
$attributeRepositoryMock->method('getList')->willReturn($searchResultsMock);
217+
218+
return new ScopeOverriddenValue(
219+
$attributeRepositoryMock,
220+
$metadataPoolMock,
221+
$searchCriteriaBuilderMock,
222+
$filterBuilderMock,
223+
$resourceConnectionMock
224+
);
225+
}
226+
227+
/**
228+
* Create ScopeOverriddenValue without EAV entity type
229+
*/
230+
private function createScopeOverriddenValueWithoutEavEntityType(): ScopeOverriddenValue
231+
{
232+
$metadataPoolMock = $this->createMock(MetadataPool::class);
233+
$metadataMock = $this->createMock(EntityMetadataInterface::class);
234+
235+
$metadataMock->method('getEavEntityType')->willReturn(null);
236+
$metadataPoolMock->method('getMetadata')->willReturn($metadataMock);
237+
238+
return new ScopeOverriddenValue(
239+
$this->createMock(AttributeRepositoryInterface::class),
240+
$metadataPoolMock,
241+
$this->createMock(SearchCriteriaBuilder::class),
242+
$this->createMock(FilterBuilder::class),
243+
$this->createMock(ResourceConnection::class)
244+
);
245+
}
246+
247+
/**
248+
* Create ScopeOverriddenValue with static attributes
249+
*/
250+
private function createScopeOverriddenValueWithStaticAttributes(): ScopeOverriddenValue
251+
{
252+
$attributeRepositoryMock = $this->createMock(AttributeRepositoryInterface::class);
253+
$metadataPoolMock = $this->createMock(MetadataPool::class);
254+
$searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class);
255+
$filterBuilderMock = $this->createMock(FilterBuilder::class);
256+
$resourceConnectionMock = $this->createMock(ResourceConnection::class);
257+
258+
// Setup metadata
259+
$metadataMock = $this->createMock(EntityMetadataInterface::class);
260+
$metadataMock->method('getEavEntityType')->willReturn('catalog_product');
261+
$metadataMock->method('getLinkField')->willReturn('entity_id');
262+
$metadataPoolMock->method('getMetadata')->willReturn($metadataMock);
263+
264+
// Setup static attribute
265+
$staticAttributeMock = $this->createMock(AbstractAttribute::class);
266+
$staticAttributeMock->method('isStatic')->willReturn(true);
267+
268+
$searchResultsMock = $this->createMock(AttributeSearchResultsInterface::class);
269+
$searchResultsMock->method('getItems')->willReturn([$staticAttributeMock]);
270+
271+
// Setup search criteria
272+
$filterMock = $this->createMock(\Magento\Framework\Api\Filter::class);
273+
$searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class);
274+
275+
$filterBuilderMock->method('setField')->willReturnSelf();
276+
$filterBuilderMock->method('setConditionType')->willReturnSelf();
277+
$filterBuilderMock->method('setValue')->willReturnSelf();
278+
$filterBuilderMock->method('create')->willReturn($filterMock);
279+
280+
$searchCriteriaBuilderMock->method('addFilters')->willReturnSelf();
281+
$searchCriteriaBuilderMock->method('create')->willReturn($searchCriteriaMock);
282+
283+
$attributeRepositoryMock->method('getList')->willReturn($searchResultsMock);
284+
285+
return new ScopeOverriddenValue(
286+
$attributeRepositoryMock,
287+
$metadataPoolMock,
288+
$searchCriteriaBuilderMock,
289+
$filterBuilderMock,
290+
$resourceConnectionMock
291+
);
292+
}
293+
294+
/**
295+
* Create ScopeOverriddenValue with non-static attributes and full database simulation
296+
*/
297+
private function createScopeOverriddenValueWithNonStaticAttributes(): ScopeOverriddenValue
298+
{
299+
$attributeRepositoryMock = $this->createMock(AttributeRepositoryInterface::class);
300+
$metadataPoolMock = $this->createMock(MetadataPool::class);
301+
$searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class);
302+
$filterBuilderMock = $this->createMock(FilterBuilder::class);
303+
$resourceConnectionMock = $this->createMock(ResourceConnection::class);
304+
305+
// Setup database connection and select
306+
$connectionMock = $this->createMock(AdapterInterface::class);
307+
$selectMock = $this->createMock(Select::class);
308+
309+
$connectionMock->method('select')->willReturn($selectMock);
310+
$selectMock->method('from')->willReturnSelf();
311+
$selectMock->method('join')->willReturnSelf();
312+
$selectMock->method('where')->willReturnSelf();
313+
314+
// Mock database results
315+
$connectionMock->method('fetchAll')->willReturn([
316+
['attribute_code' => 'test_attr', 'value' => 'test_value', 'store_id' => '0'],
317+
['attribute_code' => 'test_attr', 'value' => 'test_value', 'store_id' => '1'],
318+
['attribute_code' => 'test_attr', 'value' => 'test_value', 'store_id' => '2']
319+
]);
320+
321+
// Setup metadata
322+
$metadataMock = $this->createMock(EntityMetadataInterface::class);
323+
$metadataMock->method('getEavEntityType')->willReturn('catalog_product');
324+
$metadataMock->method('getLinkField')->willReturn('entity_id');
325+
$metadataMock->method('getEntityConnection')->willReturn($connectionMock);
326+
$metadataPoolMock->method('getMetadata')->willReturn($metadataMock);
327+
328+
// Setup non-static attribute
329+
$attributeMock = $this->createMock(AbstractAttribute::class);
330+
$attributeMock->method('isStatic')->willReturn(false);
331+
332+
$backendMock = $this->createMock(AbstractBackend::class);
333+
$backendMock->method('getTable')->willReturn('catalog_product_entity_varchar');
334+
$attributeMock->method('getBackend')->willReturn($backendMock);
335+
$attributeMock->method('getAttributeId')->willReturn(1);
336+
337+
$searchResultsMock = $this->createMock(AttributeSearchResultsInterface::class);
338+
$searchResultsMock->method('getItems')->willReturn([$attributeMock]);
339+
340+
// Setup search criteria
341+
$filterMock = $this->createMock(\Magento\Framework\Api\Filter::class);
342+
$searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class);
343+
344+
$filterBuilderMock->method('setField')->willReturnSelf();
345+
$filterBuilderMock->method('setConditionType')->willReturnSelf();
346+
$filterBuilderMock->method('setValue')->willReturnSelf();
347+
$filterBuilderMock->method('create')->willReturn($filterMock);
348+
349+
$searchCriteriaBuilderMock->method('addFilters')->willReturnSelf();
350+
$searchCriteriaBuilderMock->method('create')->willReturn($searchCriteriaMock);
351+
352+
$attributeRepositoryMock->method('getList')->willReturn($searchResultsMock);
353+
354+
$resourceConnectionMock->method('getTableName')->willReturn('eav_attribute');
355+
356+
return new ScopeOverriddenValue(
357+
$attributeRepositoryMock,
358+
$metadataPoolMock,
359+
$searchCriteriaBuilderMock,
360+
$filterBuilderMock,
361+
$resourceConnectionMock
362+
);
363+
}
364+
365+
/**
366+
* Create mock entity
367+
*/
368+
private function createMockEntity(): MockObject
369+
{
370+
$entityMock = $this->getMockBuilder(AbstractModel::class)
371+
->disableOriginalConstructor()
372+
->onlyMethods(['getData'])
373+
->addMethods(['getStoreId'])
374+
->getMock();
375+
$entityMock->method('getData')->willReturn(1);
376+
$entityMock->method('getStoreId')->willReturn(0);
377+
return $entityMock;
378+
}
379+
}

0 commit comments

Comments
 (0)