Skip to content

Commit 9549152

Browse files
committed
Merge remote-tracking branch 'origin/AC-15252' into spartans_pr_26082025
2 parents ef2bf4f + 754a5c5 commit 9549152

File tree

2 files changed

+244
-3
lines changed
  • app/code/Magento/Catalog
    • Controller/Adminhtml/Product/Action/Attribute
    • Test/Unit/Controller/Adminhtml/Product/Action/Attribute

2 files changed

+244
-3
lines changed

app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,22 @@ private function validateProductAttributes(array $attributesData): void
229229
$product = $this->productFactory->create();
230230
$product->setData($attributesData);
231231

232-
foreach (array_keys($attributesData) as $attributeCode) {
233-
$attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode);
234-
$attribute->getBackend()->validate($product);
232+
// Ensure Special Price From Date cannot exceed To Date during mass update
233+
if (array_key_exists('special_from_date', $attributesData)
234+
|| array_key_exists('special_to_date', $attributesData)) {
235+
$this->eavConfig
236+
->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'special_from_date')
237+
->setMaxValue($product->getSpecialToDate());
238+
}
239+
240+
try {
241+
foreach (array_keys($attributesData) as $attributeCode) {
242+
$attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode);
243+
$attribute->getBackend()->validate($product);
244+
}
245+
} catch (\Magento\Eav\Model\Entity\Attribute\Exception $e) {
246+
// Re-throw as LocalizedException so the specific validation message is displayed
247+
throw new LocalizedException(__($e->getMessage()));
235248
}
236249
}
237250

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Action\Attribute;
9+
10+
use Magento\Backend\App\Action\Context;
11+
use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save;
12+
use Magento\Catalog\Helper\Product\Edit\Action\Attribute as AttributeHelper;
13+
use Magento\Catalog\Model\ProductFactory;
14+
use Magento\Eav\Model\Config as EavConfig;
15+
use Magento\Framework\App\RequestInterface;
16+
use Magento\Framework\Bulk\BulkManagementInterface;
17+
use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory;
18+
use Magento\Framework\DataObject\IdentityGeneratorInterface;
19+
use Magento\Framework\Serialize\SerializerInterface;
20+
use Magento\Authorization\Model\UserContextInterface;
21+
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
22+
use Magento\Catalog\Model\Product\Filter\DateTime as DateTimeFilter;
23+
use Magento\Framework\Exception\LocalizedException;
24+
use Magento\Eav\Model\Entity\Attribute\Exception as EavAttributeException;
25+
use PHPUnit\Framework\TestCase;
26+
27+
/**
28+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
29+
*/
30+
class SaveTest extends TestCase
31+
{
32+
/**
33+
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
34+
*/
35+
private function buildController(
36+
Context $context,
37+
AttributeHelper $attributeHelper,
38+
BulkManagementInterface $bulkManagement,
39+
OperationInterfaceFactory $operationFactory,
40+
IdentityGeneratorInterface $identityService,
41+
SerializerInterface $serializer,
42+
UserContextInterface $userContext,
43+
TimezoneInterface $timezone,
44+
EavConfig $eavConfig,
45+
ProductFactory $productFactory,
46+
DateTimeFilter $dateTimeFilter
47+
): Save {
48+
return new Save(
49+
$context,
50+
$attributeHelper,
51+
$bulkManagement,
52+
$operationFactory,
53+
$identityService,
54+
$serializer,
55+
$userContext,
56+
100,
57+
$timezone,
58+
$eavConfig,
59+
$productFactory,
60+
$dateTimeFilter
61+
);
62+
}
63+
64+
public function testValidateProductAttributesSetsMaxValueAndConvertsEavException(): void
65+
{
66+
$context = $this->createMock(Context::class);
67+
$attributeHelper = $this->createMock(AttributeHelper::class);
68+
$bulkManagement = $this->createMock(BulkManagementInterface::class);
69+
$operationFactory = $this->createMock(OperationInterfaceFactory::class);
70+
$identityService = $this->createMock(IdentityGeneratorInterface::class);
71+
$serializer = $this->createMock(SerializerInterface::class);
72+
$userContext = $this->createMock(UserContextInterface::class);
73+
$timezone = $this->createMock(TimezoneInterface::class);
74+
$eavConfig = $this->createMock(EavConfig::class);
75+
$productFactory = $this->createMock(ProductFactory::class);
76+
$dateTimeFilter = $this->createMock(DateTimeFilter::class);
77+
78+
$product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
79+
->disableOriginalConstructor()
80+
->onlyMethods(['setData', 'getSpecialToDate'])
81+
->getMock();
82+
$product->method('setData')->with([
83+
'special_from_date' => '2025-09-10 00:00:00',
84+
'special_to_date' => '2025-09-01 00:00:00',
85+
]);
86+
$product->method('getSpecialToDate')->willReturn('2025-09-01 00:00:00');
87+
88+
$productFactory->method('create')->willReturn($product);
89+
90+
// Attribute for special_from_date
91+
$fromAttrBackend = $this->getMockBuilder(\stdClass::class)
92+
->addMethods(['validate'])
93+
->getMock();
94+
$fromAttrBackend->method('validate')->willThrowException(
95+
new EavAttributeException(__('Make sure the To Date is later than or the same as the From Date.'))
96+
);
97+
98+
$fromAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
99+
->disableOriginalConstructor()
100+
->onlyMethods(['getBackend'])
101+
->addMethods(['setMaxValue'])
102+
->getMockForAbstractClass();
103+
$fromAttribute->expects($this->once())
104+
->method('setMaxValue')
105+
->with('2025-09-01 00:00:00');
106+
$fromAttribute->method('getBackend')->willReturn($fromAttrBackend);
107+
108+
// Attribute for special_to_date
109+
$toAttrBackend = $this->getMockBuilder(\stdClass::class)
110+
->addMethods(['validate'])
111+
->getMock();
112+
$toAttrBackend->method('validate')->willReturn(true);
113+
114+
$toAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
115+
->disableOriginalConstructor()
116+
->onlyMethods(['getBackend'])
117+
->getMockForAbstractClass();
118+
$toAttribute->method('getBackend')->willReturn($toAttrBackend);
119+
120+
// eavConfig should return attributes for 'special_from_date' and 'special_to_date'
121+
$eavConfig->method('getAttribute')
122+
->willReturnCallback(function ($entity, $code) use ($fromAttribute, $toAttribute) {
123+
unset($entity);
124+
return $code === 'special_from_date' ? $fromAttribute : $toAttribute;
125+
});
126+
127+
$controller = $this->buildController(
128+
$context,
129+
$attributeHelper,
130+
$bulkManagement,
131+
$operationFactory,
132+
$identityService,
133+
$serializer,
134+
$userContext,
135+
$timezone,
136+
$eavConfig,
137+
$productFactory,
138+
$dateTimeFilter
139+
);
140+
141+
$method = new \ReflectionMethod($controller, 'validateProductAttributes');
142+
$method->setAccessible(true);
143+
144+
$this->expectException(LocalizedException::class);
145+
$this->expectExceptionMessage('Make sure the To Date is later than or the same as the From Date.');
146+
147+
$method->invoke($controller, [
148+
'special_from_date' => '2025-09-10 00:00:00',
149+
'special_to_date' => '2025-09-01 00:00:00',
150+
]);
151+
}
152+
153+
public function testValidateProductAttributesPassesWhenDatesValid(): void
154+
{
155+
$context = $this->createMock(Context::class);
156+
$attributeHelper = $this->createMock(AttributeHelper::class);
157+
$bulkManagement = $this->createMock(BulkManagementInterface::class);
158+
$operationFactory = $this->createMock(OperationInterfaceFactory::class);
159+
$identityService = $this->createMock(IdentityGeneratorInterface::class);
160+
$serializer = $this->createMock(SerializerInterface::class);
161+
$userContext = $this->createMock(UserContextInterface::class);
162+
$timezone = $this->createMock(TimezoneInterface::class);
163+
$eavConfig = $this->createMock(EavConfig::class);
164+
$productFactory = $this->createMock(ProductFactory::class);
165+
$dateTimeFilter = $this->createMock(DateTimeFilter::class);
166+
167+
$product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
168+
->disableOriginalConstructor()
169+
->onlyMethods(['setData', 'getSpecialToDate'])
170+
->getMock();
171+
$product->method('setData')->with([
172+
'special_from_date' => '2025-09-01 00:00:00',
173+
'special_to_date' => '2025-09-10 00:00:00',
174+
]);
175+
$product->method('getSpecialToDate')->willReturn('2025-09-10 00:00:00');
176+
$productFactory->method('create')->willReturn($product);
177+
178+
$okBackend = $this->getMockBuilder(\stdClass::class)
179+
->addMethods(['validate'])
180+
->getMock();
181+
$okBackend->method('validate')->willReturn(true);
182+
183+
$fromAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
184+
->disableOriginalConstructor()
185+
->onlyMethods(['getBackend'])
186+
->addMethods(['setMaxValue'])
187+
->getMockForAbstractClass();
188+
$fromAttribute->expects($this->once())->method('setMaxValue')->with('2025-09-10 00:00:00');
189+
$fromAttribute->method('getBackend')->willReturn($okBackend);
190+
191+
$toAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
192+
->disableOriginalConstructor()
193+
->onlyMethods(['getBackend'])
194+
->getMockForAbstractClass();
195+
$toAttribute->method('getBackend')->willReturn($okBackend);
196+
197+
$eavConfig->method('getAttribute')
198+
->willReturnCallback(function ($entity, $code) use ($fromAttribute, $toAttribute) {
199+
unset($entity);
200+
return $code === 'special_from_date' ? $fromAttribute : $toAttribute;
201+
});
202+
203+
$controller = $this->buildController(
204+
$context,
205+
$attributeHelper,
206+
$bulkManagement,
207+
$operationFactory,
208+
$identityService,
209+
$serializer,
210+
$userContext,
211+
$timezone,
212+
$eavConfig,
213+
$productFactory,
214+
$dateTimeFilter
215+
);
216+
217+
$method = new \ReflectionMethod($controller, 'validateProductAttributes');
218+
$method->setAccessible(true);
219+
220+
// Should not throw
221+
$method->invoke($controller, [
222+
'special_from_date' => '2025-09-01 00:00:00',
223+
'special_to_date' => '2025-09-10 00:00:00',
224+
]);
225+
226+
$this->addToAssertionCount(1);
227+
}
228+
}

0 commit comments

Comments
 (0)