Skip to content

Commit 643a926

Browse files
Merge branch '2.4-develop' into MC-30675
2 parents f58ddb5 + ee5d56a commit 643a926

File tree

150 files changed

+2663
-497
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

150 files changed

+2663
-497
lines changed

app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
1111
<actionGroup name="LoginActionGroup">
1212
<annotations>
13-
<description>Login to Backend Admin using ENV Admin credentials. PLEASE NOTE: This Action Group does NOT validate that you are Logged In.</description>
13+
<description>DEPRECATED. Please use LoginAsAdmin instead.
14+
Login to Backend Admin using ENV Admin credentials. PLEASE NOTE: This Action Group does NOT validate that you are Logged In.</description>
1415
</annotations>
1516

1617
<amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}" stepKey="navigateToAdmin"/>

app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,7 @@ public function addParentFilterData(int $parentId, int $parentEntityId, string $
7878
public function getOptionsByParentId(int $parentId) : array
7979
{
8080
$options = $this->fetch();
81-
if (!isset($options[$parentId])) {
82-
return [];
83-
}
84-
85-
return $options[$parentId];
81+
return $options[$parentId] ?? [];
8682
}
8783

8884
/**
@@ -115,7 +111,7 @@ private function fetch() : array
115111

116112
$this->extensionAttributesJoinProcessor->process($optionsCollection);
117113
if (empty($optionsCollection->getData())) {
118-
return null;
114+
return [];
119115
}
120116

121117
/** @var \Magento\Bundle\Model\Option $option */
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Captcha\Test\Unit\Observer;
10+
11+
use Magento\Captcha\Model\ResourceModel\Log;
12+
use Magento\Captcha\Model\ResourceModel\LogFactory;
13+
use Magento\Captcha\Observer\ResetAttemptForBackendObserver;
14+
use Magento\Framework\Event;
15+
use Magento\Framework\Event\Observer;
16+
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/**
21+
* Unit test for \Magento\Captcha\Observer\ResetAttemptForBackendObserver
22+
*/
23+
class ResetAttemptForBackendObserverTest extends TestCase
24+
{
25+
/**
26+
* Test that the method resets attempts for Backend
27+
*/
28+
public function testExecuteExpectsDeleteUserAttemptsCalled()
29+
{
30+
$logMock = $this->createMock(Log::class);
31+
$logMock->expects($this->once())->method('deleteUserAttempts')->willReturnSelf();
32+
33+
$resLogFactoryMock = $this->createMock(LogFactory::class);
34+
$resLogFactoryMock->expects($this->once())
35+
->method('create')
36+
->willReturn($logMock);
37+
38+
/** @var MockObject|Observer $eventObserverMock */
39+
$eventObserverMock = $this->createPartialMock(Observer::class, ['getUser']);
40+
$eventMock = $this->createMock(Event::class);
41+
$eventObserverMock->expects($this->once())
42+
->method('getUser')
43+
->willReturn($eventMock);
44+
45+
$objectManager = new ObjectManagerHelper($this);
46+
/** @var ResetAttemptForBackendObserver $observer */
47+
$observer = $objectManager->getObject(
48+
ResetAttemptForBackendObserver::class,
49+
['resLogFactory' => $resLogFactoryMock]
50+
);
51+
$this->assertInstanceOf(Log::class, $observer->execute($eventObserverMock));
52+
}
53+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Captcha\Test\Unit\Observer;
10+
11+
use Magento\Captcha\Model\ResourceModel\Log;
12+
use Magento\Captcha\Model\ResourceModel\LogFactory;
13+
use Magento\Captcha\Observer\ResetAttemptForFrontendObserver;
14+
use Magento\Customer\Model\Customer;
15+
use Magento\Framework\Event\Observer;
16+
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/**
21+
* Unit test for \Magento\Captcha\Observer\ResetAttemptForFrontendObserver
22+
*/
23+
class ResetAttemptForFrontendObserverTest extends TestCase
24+
{
25+
/**
26+
* Test that the method resets attempts for Frontend
27+
*/
28+
public function testExecuteExpectsDeleteUserAttemptsCalled()
29+
{
30+
$logMock = $this->createMock(Log::class);
31+
$logMock->expects($this->once())->method('deleteUserAttempts')->willReturnSelf();
32+
33+
$resLogFactoryMock = $this->createMock(LogFactory::class);
34+
$resLogFactoryMock->expects($this->once())
35+
->method('create')
36+
->willReturn($logMock);
37+
38+
/** @var MockObject|Observer $eventObserverMock */
39+
$eventObserverMock = $this->createPartialMock(Observer::class, ['getModel']);
40+
$eventObserverMock->expects($this->once())
41+
->method('getModel')
42+
->willReturn($this->createMock(Customer::class));
43+
44+
$objectManager = new ObjectManagerHelper($this);
45+
/** @var ResetAttemptForFrontendObserver $observer */
46+
$observer = $objectManager->getObject(
47+
ResetAttemptForFrontendObserver::class,
48+
['resLogFactory' => $resLogFactoryMock]
49+
);
50+
$this->assertInstanceOf(Log::class, $observer->execute($eventObserverMock));
51+
}
52+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Api;
8+
9+
/**
10+
* @api
11+
* @since 100.0.2
12+
*/
13+
interface CategoryListDeleteBySkuInterface
14+
{
15+
/**
16+
* Delete by skus list
17+
*
18+
* @param int $categoryId
19+
* @param string[] $productSkuList
20+
* @return bool
21+
*
22+
* @throws \Magento\Framework\Exception\CouldNotSaveException
23+
* @throws \Magento\Framework\Exception\NoSuchEntityException
24+
* @throws \Magento\Framework\Exception\InputException
25+
*/
26+
public function deleteBySkus(int $categoryId, array $productSkuList): bool;
27+
}

app/code/Magento/Catalog/Model/CategoryLinkRepository.php

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,52 @@
66

77
namespace Magento\Catalog\Model;
88

9-
use Magento\Framework\Exception\InputException;
9+
use Magento\Catalog\Api\CategoryLinkRepositoryInterface;
10+
use Magento\Catalog\Api\CategoryListDeleteBySkuInterface;
11+
use Magento\Catalog\Api\CategoryRepositoryInterface;
12+
use Magento\Catalog\Api\ProductRepositoryInterface;
13+
use Magento\Catalog\Model\ResourceModel\Product;
14+
use Magento\Framework\App\ObjectManager;
1015
use Magento\Framework\Exception\CouldNotSaveException;
16+
use Magento\Framework\Exception\InputException;
1117

12-
class CategoryLinkRepository implements \Magento\Catalog\Api\CategoryLinkRepositoryInterface
18+
/**
19+
* @inheritdoc
20+
*/
21+
class CategoryLinkRepository implements CategoryLinkRepositoryInterface, CategoryListDeleteBySkuInterface
1322
{
1423
/**
1524
* @var CategoryRepository
1625
*/
1726
protected $categoryRepository;
1827

1928
/**
20-
* @var \Magento\Catalog\Api\ProductRepositoryInterface
29+
* @var ProductRepositoryInterface
2130
*/
2231
protected $productRepository;
2332

2433
/**
25-
* @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository
26-
* @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
34+
* @var Product
35+
*/
36+
private $productResource;
37+
38+
/**
39+
* @param CategoryRepositoryInterface $categoryRepository
40+
* @param ProductRepositoryInterface $productRepository
41+
* @param Product $productResource
2742
*/
2843
public function __construct(
29-
\Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository,
30-
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository
44+
CategoryRepositoryInterface $categoryRepository,
45+
ProductRepositoryInterface $productRepository,
46+
Product $productResource = null
3147
) {
3248
$this->categoryRepository = $categoryRepository;
3349
$this->productRepository = $productRepository;
50+
$this->productResource = $productResource ?? ObjectManager::getInstance()->get(Product::class);
3451
}
3552

3653
/**
37-
* {@inheritdoc}
54+
* @inheritdoc
3855
*/
3956
public function save(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $productLink)
4057
{
@@ -60,15 +77,15 @@ public function save(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $pro
6077
}
6178

6279
/**
63-
* {@inheritdoc}
80+
* @inheritdoc
6481
*/
6582
public function delete(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $productLink)
6683
{
6784
return $this->deleteByIds($productLink->getCategoryId(), $productLink->getSku());
6885
}
6986

7087
/**
71-
* {@inheritdoc}
88+
* @inheritdoc
7289
*/
7390
public function deleteByIds($categoryId, $sku)
7491
{
@@ -101,4 +118,44 @@ public function deleteByIds($categoryId, $sku)
101118
}
102119
return true;
103120
}
121+
122+
/**
123+
* @inheritdoc
124+
*/
125+
public function deleteBySkus(int $categoryId, array $productSkuList): bool
126+
{
127+
$category = $this->categoryRepository->get($categoryId);
128+
$products = $this->productResource->getProductsIdsBySkus($productSkuList);
129+
130+
if (!$products) {
131+
throw new InputException(__("The category doesn't contain the specified products."));
132+
}
133+
134+
$productPositions = $category->getProductsPosition();
135+
136+
foreach ($products as $productId) {
137+
if (isset($productPositions[$productId])) {
138+
unset($productPositions[$productId]);
139+
}
140+
}
141+
142+
$category->setPostedProducts($productPositions);
143+
144+
try {
145+
$category->save();
146+
} catch (\Exception $e) {
147+
throw new CouldNotSaveException(
148+
__(
149+
'Could not save products "%products" to category %category',
150+
[
151+
"products" => implode(',', $productSkuList),
152+
"category" => $category->getId()
153+
]
154+
),
155+
$e
156+
);
157+
}
158+
159+
return true;
160+
}
104161
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
11+
<actionGroup name="AdminCategoryPageOpenProductsInCategorySectionActionGroup">
12+
<annotations>
13+
<description>Open 'Products in Category' section on category edit page in Admin.</description>
14+
</annotations>
15+
16+
<conditionalClick selector="{{AdminCategoryProductsSection.sectionHeader}}" dependentSelector="{{AdminCategoryProductsSection.sectionBody}}" visible="false" stepKey="openSectionIfHidden" />
17+
<scrollTo selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="scrollToSection" />
18+
<waitForPageLoad stepKey="waitSectionFullyLoaded"/>
19+
</actionGroup>
20+
</actionGroups>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
11+
<actionGroup name="AdminProductFormAdvancedPricingAddTierPriceActionGroup">
12+
<annotations>
13+
<description>Add new tier price on Advanced Pricing dialog on the Admin Product creation/edit page.</description>
14+
</annotations>
15+
<arguments>
16+
<argument name="website" type="string" defaultValue="All Websites [USD]"/>
17+
<argument name="customerGroup" type="string" defaultValue="ALL GROUPS"/>
18+
<argument name="quantity" type="string" defaultValue="1"/>
19+
<argument name="priceType" type="string" defaultValue="Fixed"/>
20+
<argument name="amount" type="string" defaultValue="10"/>
21+
</arguments>
22+
23+
<waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForGroupPriceAddButtonAppears"/>
24+
<click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/>
25+
<waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.lastTierPriceWebsite}}" stepKey="waitForPriceWebsiteInputAppears"/>
26+
<selectOption selector="{{AdminProductFormAdvancedPricingSection.lastTierPriceWebsite}}" userInput="{{website}}" stepKey="selectWebsite"/>
27+
<selectOption selector="{{AdminProductFormAdvancedPricingSection.lastTierPriceCustomerGroup}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/>
28+
<fillField selector="{{AdminProductFormAdvancedPricingSection.lastTierPriceQty}}" userInput="{{quantity}}" stepKey="fillQuantity"/>
29+
<selectOption selector="{{AdminProductFormAdvancedPricingSection.lastTierPriceType}}" userInput="{{priceType}}" stepKey="selectPriceType"/>
30+
<executeJS function="return '{{priceType}}' == 'Discount' ? &quot;{{AdminProductFormAdvancedPricingSection.lastTierPriceDiscountAmount}}&quot; : &quot;{{AdminProductFormAdvancedPricingSection.lastTierPriceFixedAmount}}&quot;" stepKey="priceAmountSelector"/>
31+
<waitForElementVisible selector="{$priceAmountSelector}" stepKey="waitPriceAmountFieldAppers"/>
32+
<fillField selector="{$priceAmountSelector}" userInput="{{amount}}" stepKey="fillPriceAmount"/>
33+
</actionGroup>
34+
</actionGroups>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
11+
<actionGroup name="AdminProductFormCloseAdvancedPricingDialogActionGroup">
12+
<annotations>
13+
<description>Close Advanced Pricing dialog from product form.</description>
14+
</annotations>
15+
16+
<scrollToTopOfPage stepKey="scrollToTopOfThePage"/>
17+
<click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickCloseButton"/>
18+
</actionGroup>
19+
</actionGroups>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
11+
<actionGroup name="AdminProductFormDoneAdvancedPricingDialogActionGroup">
12+
<annotations>
13+
<description>Done Advanced Pricing dialog from product form.</description>
14+
</annotations>
15+
16+
<scrollToTopOfPage stepKey="scrollToTopOfThePage"/>
17+
<click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/>
18+
</actionGroup>
19+
</actionGroups>

0 commit comments

Comments
 (0)