Skip to content

Commit 36d4d6f

Browse files
authored
Merge pull request #10048 from magento-gl/spartans_pr_26082025
[Spartans] BugFixes Delivery
2 parents e457c5e + ca15b61 commit 36d4d6f

File tree

19 files changed

+2091
-73
lines changed

19 files changed

+2091
-73
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+
}

app/code/Magento/Customer/view/frontend/templates/form/register.phtml

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
55
*/
66

77
use Magento\Customer\Helper\Address;
88

99
/** @var \Magento\Customer\Block\Form\Register $block */
10-
if (!$block->getButtonLockManager()) {
11-
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
12-
$block->setButtonLockManager(
13-
$objectManager->get(\Magento\Framework\View\Element\ButtonLockManager::class)
14-
);
15-
}
1610
/** @var \Magento\Framework\Escaper $escaper */
1711
/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */
12+
/** @var \Magento\Framework\View\Element\ButtonLockManager|null $buttonLockManager */
13+
$buttonLockManager = $block->getButtonLockManager();
1814

1915
/** @var Magento\Customer\Helper\Address $addressHelper */
2016
$addressHelper = $block->getData('addressHelper');
@@ -33,7 +29,8 @@ $formData = $block->getFormData();
3329
method="post"
3430
id="form-validate"
3531
enctype="multipart/form-data"
36-
autocomplete="off">
32+
autocomplete="off"
33+
data-mage-init='{"validation":{"errorClass":"mage-error","errorElement":"div"}}'>
3734
<?= /* @noEscape */ $block->getBlockHtml('formkey') ?>
3835
<fieldset class="fieldset create info">
3936
<legend class="legend"><span><?= $escaper->escapeHtml(__('Personal Information')) ?></span></legend><br>
@@ -300,7 +297,7 @@ $formData = $block->getFormData();
300297
class="action submit primary"
301298
title="<?= $escaper->escapeHtmlAttr(__('Create an Account')) ?>"
302299
id="send2"
303-
<?php if ($block->getButtonLockManager()->isDisabled('customer_create_form_submit')): ?>
300+
<?php if ($buttonLockManager && $buttonLockManager->isDisabled('customer_create_form_submit')): ?>
304301
disabled="disabled"
305302
<?php endif; ?>>
306303
<span><?= $escaper->escapeHtml(__('Create an Account')) ?></span>
@@ -314,20 +311,14 @@ $formData = $block->getFormData();
314311
</div>
315312
</div>
316313
</form>
317-
<?php $ignore = /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null';
318-
$scriptString = <<<script
314+
<?php if ($_dob->isEnabled()): ?>
315+
<script>
319316
require([
320317
'jquery',
321318
'mage/mage'
322319
], function($){
323-
324320
var dataForm = $('#form-validate');
325-
var ignore = {$ignore};
326-
327321
dataForm.mage('validation', {
328-
script;
329-
if ($_dob->isEnabled()):
330-
$scriptString .= <<<script
331322
errorPlacement: function(error, element) {
332323
if (element.prop('id').search('full') !== -1) {
333324
var dobElement = $(element).parents('.customer-dob'),
@@ -340,19 +331,11 @@ if ($_dob->isEnabled()):
340331
error.insertAfter(element);
341332
}
342333
},
343-
ignore: ':hidden:not(' + ignore + ')'
344-
script;
345-
else:
346-
$scriptString .= <<<script
347-
ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden'
348-
script;
349-
endif;
350-
$scriptString .= <<<script
334+
ignore: ':hidden:not(input[id$="full"])'
351335
}).find('input:text').attr('autocomplete', 'off');
352336
});
353-
script;
354-
?>
355-
<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?>
337+
</script>
338+
<?php endif; ?>
356339
<?php if ($block->getShowAddressFields()): ?>
357340
<?php
358341
$regionJson = /* @noEscape */ $regionProvider->getRegionJson();

0 commit comments

Comments
 (0)