diff --git a/app/code/Magento/Backend/Block/Store/Switcher.php b/app/code/Magento/Backend/Block/Store/Switcher.php index 2ae929f9b7398..76b6c54fdae50 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher.php +++ b/app/code/Magento/Backend/Block/Store/Switcher.php @@ -7,6 +7,8 @@ namespace Magento\Backend\Block\Store; +use Magento\Framework\Exception\LocalizedException; + /** * Store switcher block * @@ -471,9 +473,17 @@ public function getCurrentSelectionName() */ public function getCurrentWebsiteName() { - if ($this->getWebsiteId() !== null) { + $websiteId = $this->getWebsiteId(); + if ($websiteId !== null) { + if ($this->hasData('get_data_from_request')) { + $requestedWebsite = $this->getRequest()->getParams('website'); + if (!empty($requestedWebsite) + && array_key_exists('website', $requestedWebsite)) { + $websiteId = $requestedWebsite['website']; + } + } $website = $this->_websiteFactory->create(); - $website->load($this->getWebsiteId()); + $website->load($websiteId); if ($website->getId()) { return $website->getName(); } @@ -504,12 +514,21 @@ public function getCurrentStoreGroupName() * Get current store view name * * @return string + * @throws LocalizedException */ public function getCurrentStoreName() { - if ($this->getStoreId() !== null) { + $storeId = $this->getStoreId(); + if ($storeId !== null) { + if ($this->hasData('get_data_from_request')) { + $requestedStore = $this->getRequest()->getParams('store'); + if (!empty($requestedStore) + && array_key_exists('store', $requestedStore)) { + $storeId = $requestedStore['store']; + } + } $store = $this->_storeFactory->create(); - $store->load($this->getStoreId()); + $store->load($storeId); if ($store->getId()) { return $store->getName(); } diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminOrderViewPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminOrderViewPage.xml new file mode 100644 index 0000000000000..cfdb99ee36251 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminOrderViewPage.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/Backend/Test/Unit/Block/Store/SwitcherTest.php b/app/code/Magento/Backend/Test/Unit/Block/Store/SwitcherTest.php index 42b7254d8f3da..85f4709994e2a 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Store/SwitcherTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Store/SwitcherTest.php @@ -8,10 +8,15 @@ namespace Magento\Backend\Test\Unit\Block\Store; use Magento\Backend\Block\Store\Switcher; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Store\Model\StoreFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\WebsiteFactory; +use Magento\Store\Model\Website; use Magento\Backend\Block\Template\Context; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\StoreManagerInterface; -use Magento\Store\Model\Website; use PHPUnit\Framework\TestCase; class SwitcherTest extends TestCase @@ -23,20 +28,81 @@ class SwitcherTest extends TestCase private $storeManagerMock; + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var WebsiteFactory|MockObject + */ + private $websiteFactoryMock; + + /** + * @var StoreFactory|MockObject + */ + private $storeFactoryMock; + + /** + * @var Website|MockObject + */ + private $websiteMock; + + /** + * @var Store|MockObject + */ + private $storeMock; + protected function setUp(): void { $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); $objectHelper = new ObjectManager($this); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) + ->getMockForAbstractClass(); + $this->websiteFactoryMock = $this->getMockBuilder(WebsiteFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->storeFactoryMock = $this->getMockBuilder(StoreFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->websiteMock = $this->getMockBuilder(Website::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getId', 'getName']) + ->getMock(); + $this->storeMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getId', 'getName']) + ->getMock(); + $this->websiteFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->websiteMock); + $this->storeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->storeMock); + $this->websiteMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->storeMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); $context = $objectHelper->getObject( Context::class, [ 'storeManager' => $this->storeManagerMock, + 'request' => $this->requestMock ] ); $this->switcherBlock = $objectHelper->getObject( Switcher::class, - ['context' => $context] + [ + 'context' => $context, + 'data' => ['get_data_from_request' => 1], + 'websiteFactory' => $this->websiteFactoryMock, + 'storeFactory' => $this->storeFactoryMock + ] ); } @@ -58,4 +124,91 @@ public function testGetWebsitesIfSetWebsiteIds() $expected = [1 => $websiteMock]; $this->assertEquals($expected, $this->switcherBlock->getWebsites()); } + + /** + * Test case for after current store name plugin + * + * @param array $requestedStore + * @param string $expectedResult + * @return void + * @dataProvider getStoreNameDataProvider + * @throws LocalizedException + */ + public function testAfterGetCurrentStoreName(array $requestedStore, string $expectedResult): void + { + $this->requestMock->expects($this->any()) + ->method('getParams') + ->willReturn($requestedStore); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn($requestedStore); + $this->storeMock->expects($this->any()) + ->method('getName') + ->willReturn($expectedResult); + $this->assertSame($expectedResult, $this->switcherBlock->getCurrentStoreName()); + } + + /** + * Data provider for getStoreName plugin + * + * @return array + */ + public function getStoreNameDataProvider(): array + { + return [ + 'test storeName with valid requested store' => + [ + ['store' => 'test store'], + 'base store' + ], + 'test storeName with invalid requested store' => + [ + ['store' => 'test store'], + 'test store' + ] + ]; + } + + /** + * Test case for get current website name + * + * @param array $requestedWebsite + * @param string $expectedResult + * @return void + * @dataProvider getWebsiteNameDataProvider + */ + public function testGetCurrentWebsiteName(array $requestedWebsite, string $expectedResult): void + { + $this->requestMock->expects($this->any()) + ->method('getParams') + ->willReturn($requestedWebsite); + $this->websiteMock->expects($this->any()) + ->method('getId') + ->willReturn($requestedWebsite); + $this->websiteMock->expects($this->any()) + ->method('getName') + ->willReturn($expectedResult); + $this->assertSame($expectedResult, $this->switcherBlock->getCurrentWebsiteName()); + } + + /** + * Data provider for getWebsiteName plugin + * + * @return array + */ + public function getWebsiteNameDataProvider(): array + { + return [ + 'test websiteName with valid requested website' => + [ + ['website' => 'test website'], + 'base website' + ], + 'test websiteName with invalid requested website' => + [ + ['website' => 'test website'], + 'test website' + ] + ]; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index b04d3da8f0223..0ce40d0523c73 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -6,12 +6,13 @@ namespace Magento\Catalog\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Option\Repository as OptionRepository; use Magento\Catalog\Model\ProductFactory; -use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\ResourceModel\DuplicatedProductAttributesCopier; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; @@ -50,25 +51,41 @@ class Copier */ private $scopeOverriddenValue; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var DuplicatedProductAttributesCopier + */ + private $attributeCopier; + /** * @param CopyConstructorInterface $copyConstructor * @param ProductFactory $productFactory * @param ScopeOverriddenValue $scopeOverriddenValue * @param OptionRepository|null $optionRepository * @param MetadataPool|null $metadataPool + * @param ProductRepositoryInterface $productRepository + * @param DuplicatedProductAttributesCopier $attributeCopier */ public function __construct( CopyConstructorInterface $copyConstructor, ProductFactory $productFactory, ScopeOverriddenValue $scopeOverriddenValue, OptionRepository $optionRepository, - MetadataPool $metadataPool + MetadataPool $metadataPool, + ProductRepositoryInterface $productRepository, + DuplicatedProductAttributesCopier $attributeCopier ) { $this->productFactory = $productFactory; $this->copyConstructor = $copyConstructor; $this->scopeOverriddenValue = $scopeOverriddenValue; $this->optionRepository = $optionRepository; $this->metadataPool = $metadataPool; + $this->productRepository = $productRepository; + $this->attributeCopier = $attributeCopier; } /** @@ -79,11 +96,13 @@ public function __construct( */ public function copy(Product $product): Product { - $product->getWebsiteIds(); - $product->getCategoryIds(); - $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + /* Regardless in what scope the product was provided, + for duplicating we want to clone product in Global scope first */ + if ((int)$product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $product = $this->productRepository->getById($product->getId(), true, Store::DEFAULT_STORE_ID); + } /** @var Product $duplicate */ $duplicate = $this->productFactory->create(); $productData = $product->getData(); @@ -102,6 +121,7 @@ public function copy(Product $product): Product $duplicate->setStoreId(Store::DEFAULT_STORE_ID); $this->copyConstructor->build($product, $duplicate); $this->setDefaultUrl($product, $duplicate); + $this->attributeCopier->copyProductAttributes($product, $duplicate); $this->setStoresUrl($product, $duplicate); $this->optionRepository->duplicate($product, $duplicate); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/DuplicatedProductAttributesCopier.php b/app/code/Magento/Catalog/Model/ResourceModel/DuplicatedProductAttributesCopier.php new file mode 100644 index 0000000000000..5c46b379cda32 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/DuplicatedProductAttributesCopier.php @@ -0,0 +1,99 @@ +metadataPool = $metadataPool; + $this->collectionFactory = $collectionFactory; + $this->resource = $resource; + } + + /** + * Copy non-global Product Attributes form source to target + * + * @param $source Product + * @param $target Product + * @return void + */ + public function copyProductAttributes(Product $source, Product $target): void + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + $attributeCollection = $this->collectionFactory->create() + ->setAttributeSetFilter($source->getAttributeSetId()) + ->addFieldToFilter('backend_type', ['neq' => 'static']) + ->addFieldToFilter('is_global', 0); + + $eavTableNames = []; + foreach ($attributeCollection->getItems() as $item) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $item */ + $eavTableNames[] = $item->getBackendTable(); + } + + $connection = $this->resource->getConnection(); + foreach (array_unique($eavTableNames) as $eavTable) { + $select = $connection->select() + ->from( + ['main_table' => $this->resource->getTableName($eavTable)], + ['attribute_id', 'store_id', 'value'] + )->where($linkField . ' = ?', $source->getData($linkField)) + ->where('store_id <> ?', Store::DEFAULT_STORE_ID); + $records = $connection->fetchAll($select); + + if (!count($records)) { + continue; + } + + foreach ($records as $index => $bind) { + $bind[$linkField] = $target->getData($linkField); + $records[$index] = $bind; + } + + $connection->insertMultiple($this->resource->getTableName($eavTable), $records); + } + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index a247e6b09760b..246192fdc7bd5 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -1767,9 +1767,7 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) if ($attribute == 'price' && $storeId != 0) { $this->addPriceData(); if ($this->_productLimitationFilters->isUsingPriceIndex()) { - $this->getSelect()->order( - new \Zend_Db_Expr("price_index.min_price = 0, price_index.min_price {$dir}") - ); + $this->getSelect()->order("price_index.min_price {$dir}"); return $this; } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index be1d1637b8532..f78dbcb14c3f4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -305,7 +305,7 @@ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $priceExpression): \Zend_Db_Expr { return $this->getConnection()->getCheckSql( - sprintf('%s.value = 0', $tableAlias), + sprintf('%s.percentage_value IS NOT NULL', $tableAlias), sprintf( 'ROUND(%s * (1 - ROUND(%s.percentage_value * cwd.rate, 4) / 100), 4)', $priceExpression, diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php deleted file mode 100644 index d3a4494c071b7..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php +++ /dev/null @@ -1,394 +0,0 @@ -copyConstructorMock = $this->getMockForAbstractClass(CopyConstructorInterface::class); - $this->productFactoryMock = $this->createPartialMock(ProductFactory::class, ['create']); - $this->scopeOverriddenValueMock = $this->createMock(ScopeOverriddenValue::class); - $this->optionRepositoryMock = $this->createMock(Repository::class); - $this->productMock = $this->createMock(Product::class); - - $this->metadata = $this->getMockBuilder(EntityMetadata::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var MetadataPool|MockObject $metadataPool */ - $metadataPool = $this->getMockBuilder(MetadataPool::class) - ->disableOriginalConstructor() - ->getMock(); - $metadataPool->expects($this->once()) - ->method('getMetadata') - ->willReturn($this->metadata); - $this->_model = new Copier( - $this->copyConstructorMock, - $this->productFactoryMock, - $this->scopeOverriddenValueMock, - $this->optionRepositoryMock, - $metadataPool - ); - } - - /** - * Test duplicate product - * - * @return void - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testCopy(): void - { - $stockItem = $this->getMockForAbstractClass(StockItemInterface::class); - $extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getStockItem', 'setData']) - ->getMockForAbstractClass(); - $extensionAttributes - ->expects($this->once()) - ->method('getStockItem') - ->willReturn($stockItem); - $extensionAttributes - ->expects($this->once()) - ->method('setData') - ->with('stock_item', null); - - $productData = [ - 'product data' => ['product data'], - ProductInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributes, - ]; - $this->productMock->expects($this->atLeastOnce()) - ->method('getWebsiteIds'); - $this->productMock->expects($this->atLeastOnce()) - ->method('getCategoryIds'); - $this->productMock->expects($this->exactly(2)) - ->method('getData') - ->willReturnMap([ - ['', null, $productData], - ['linkField', null, '1'], - ]); - - $entityMock = $this->getMockForAbstractClass( - AbstractEntity::class, - [], - '', - false, - true, - true, - ['checkAttributeUniqueValue'] - ); - $entityMock->expects($this->once()) - ->method('checkAttributeUniqueValue') - ->willReturn(true); - - $attributeMock = $this->getMockForAbstractClass( - AbstractAttribute::class, - [], - '', - false, - true, - true, - ['getEntity'] - ); - $attributeMock->expects($this->once()) - ->method('getEntity') - ->willReturn($entityMock); - - $resourceMock = $this->getMockBuilder(ProductResourceModel::class) - ->disableOriginalConstructor() - ->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute']) - ->getMock(); - $resourceMock->expects($this->once()) - ->method('getAttributeRawValue') - ->willReturn('urk-key-1'); - $resourceMock->expects($this->exactly(2)) - ->method('getAttribute') - ->willReturn($attributeMock); - - $this->productMock->expects($this->exactly(2)) - ->method('getResource') - ->willReturn($resourceMock); - - $duplicateMock = $this->getMockBuilder(Product::class) - ->addMethods( - [ - 'setIsDuplicate', - 'setOriginalLinkId', - 'setUrlKey', - 'setMetaTitle', - 'setMetaKeyword', - 'setMetaDescription' - ] - ) - ->onlyMethods( - [ - 'setData', - 'setOptions', - 'getData', - 'setStatus', - 'setCreatedAt', - 'setUpdatedAt', - 'setId', - 'getEntityId', - 'save', - 'setStoreId', - 'getStoreIds' - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $this->productFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($duplicateMock); - - $duplicateMock->expects($this->once())->method('setOptions')->with([]); - $duplicateMock->expects($this->once())->method('setIsDuplicate')->with(true); - $duplicateMock->expects($this->once())->method('setOriginalLinkId')->with(1); - $duplicateMock->expects($this->once()) - ->method('setStatus') - ->with(Status::STATUS_DISABLED); - $duplicateMock->expects($this->atLeastOnce())->method('setStoreId'); - $duplicateMock->expects($this->once()) - ->method('setCreatedAt') - ->with(null); - $duplicateMock->expects($this->once()) - ->method('setUpdatedAt') - ->with(null); - $duplicateMock->expects($this->once()) - ->method('setId') - ->with(null); - $duplicateMock->expects($this->once()) - ->method('setMetaTitle') - ->with(null); - $duplicateMock->expects($this->once()) - ->method('setMetaKeyword') - ->with(null); - $duplicateMock->expects($this->once()) - ->method('setMetaDescription') - ->with(null); - $duplicateMock->expects($this->atLeastOnce()) - ->method('getStoreIds')->willReturn([]); - $duplicateMock->expects($this->atLeastOnce()) - ->method('setData') - ->willReturn($duplicateMock); - $this->copyConstructorMock->expects($this->once()) - ->method('build') - ->with($this->productMock, $duplicateMock); - $duplicateMock->expects($this->once()) - ->method('setUrlKey') - ->with('urk-key-2') - ->willReturn($duplicateMock); - $duplicateMock->expects($this->once()) - ->method('save'); - $this->metadata->expects($this->once()) - ->method('getLinkField') - ->willReturn('linkField'); - $duplicateMock->expects($this->never()) - ->method('getData'); - $this->optionRepositoryMock->expects($this->once()) - ->method('duplicate') - ->with($this->productMock, $duplicateMock); - - $this->assertEquals($duplicateMock, $this->_model->copy($this->productMock)); - } - - /** - * Test duplicate product with `UrlAlreadyExistsException` while copy stores url - * - * @return void - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testUrlAlreadyExistsExceptionWhileCopyStoresUrl(): void - { - $stockItem = $this->getMockBuilder(StockItemInterface::class) - ->getMock(); - $extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getStockItem', 'setData']) - ->getMockForAbstractClass(); - $extensionAttributes - ->expects($this->once()) - ->method('getStockItem') - ->willReturn($stockItem); - $extensionAttributes - ->expects($this->once()) - ->method('setData') - ->with('stock_item', null); - - $productData = [ - 'product data' => ['product data'], - ProductInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributes, - ]; - $this->productMock->expects($this->atLeastOnce())->method('getWebsiteIds'); - $this->productMock->expects($this->atLeastOnce())->method('getCategoryIds'); - $this->productMock->expects($this->any())->method('getData')->willReturnMap([ - ['', null, $productData], - ['linkField', null, '1'], - ]); - - $entityMock = $this->getMockForAbstractClass( - AbstractEntity::class, - [], - '', - false, - true, - true, - ['checkAttributeUniqueValue'] - ); - $entityMock->expects($this->exactly(11)) - ->method('checkAttributeUniqueValue') - ->willReturn(true, false); - - $attributeMock = $this->getMockForAbstractClass( - AbstractAttribute::class, - [], - '', - false, - true, - true, - ['getEntity'] - ); - $attributeMock->expects($this->any()) - ->method('getEntity') - ->willReturn($entityMock); - - $resourceMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product::class) - ->disableOriginalConstructor() - ->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute']) - ->getMock(); - $resourceMock->expects($this->any()) - ->method('getAttributeRawValue') - ->willReturn('urk-key-1'); - $resourceMock->expects($this->any()) - ->method('getAttribute') - ->willReturn($attributeMock); - - $this->productMock->expects($this->any())->method('getResource')->willReturn($resourceMock); - - $duplicateMock = $this->getMockBuilder(Product::class) - ->addMethods(['setIsDuplicate', 'setOriginalLinkId', 'setUrlKey']) - ->onlyMethods( - [ - 'setData', - 'setOptions', - 'getData', - 'setStatus', - 'setCreatedAt', - 'setUpdatedAt', - 'setId', - 'getEntityId', - 'save', - 'setStoreId', - 'getStoreIds' - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $this->productFactoryMock->expects($this->once())->method('create')->willReturn($duplicateMock); - - $duplicateMock->expects($this->once())->method('setOptions')->with([]); - $duplicateMock->expects($this->once())->method('setIsDuplicate')->with(true); - $duplicateMock->expects($this->once())->method('setOriginalLinkId')->with(1); - $duplicateMock->expects( - $this->once() - )->method( - 'setStatus' - )->with( - Status::STATUS_DISABLED - ); - $duplicateMock->expects($this->atLeastOnce())->method('setStoreId'); - $duplicateMock->expects($this->once())->method('setCreatedAt')->with(null); - $duplicateMock->expects($this->once())->method('setUpdatedAt')->with(null); - $duplicateMock->expects($this->once())->method('setId')->with(null); - $duplicateMock->expects($this->atLeastOnce())->method('getStoreIds')->willReturn([1]); - $duplicateMock->expects($this->atLeastOnce())->method('setData')->willReturn($duplicateMock); - $this->copyConstructorMock->expects($this->once())->method('build')->with($this->productMock, $duplicateMock); - $duplicateMock->expects( - $this->exactly(11) - )->method( - 'setUrlKey' - )->with( - $this->stringContains('urk-key-') - )->willReturn( - $duplicateMock - ); - $duplicateMock->expects($this->once())->method('save'); - - $this->scopeOverriddenValueMock->expects($this->once())->method('containsValue')->willReturn(true); - - $this->metadata->expects($this->any())->method('getLinkField')->willReturn('linkField'); - - $duplicateMock->expects($this->any())->method('getData')->willReturnMap([ - ['linkField', null, '2'], - ]); - - $this->expectException(UrlAlreadyExistsException::class); - $this->_model->copy($this->productMock); - } -} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 13bd29e83d87f..dd82bf277a33a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; +use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; @@ -18,6 +19,7 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; +use Magento\Framework\App\ObjectManager; use Magento\GraphQl\Model\Query\ContextInterface; /** @@ -55,6 +57,11 @@ class ProductSearch */ private $searchCriteriaBuilder; + /** + * @var Visibility + */ + private $catalogProductVisibility; + /** * @param CollectionFactory $collectionFactory * @param ProductSearchResultsInterfaceFactory $searchResultsFactory @@ -62,6 +69,7 @@ class ProductSearch * @param CollectionPostProcessor $collectionPostProcessor * @param SearchResultApplierFactory $searchResultsApplierFactory * @param ProductCollectionSearchCriteriaBuilder $searchCriteriaBuilder + * @param Visibility $catalogProductVisibility */ public function __construct( CollectionFactory $collectionFactory, @@ -69,7 +77,8 @@ public function __construct( CollectionProcessorInterface $collectionPreProcessor, CollectionPostProcessor $collectionPostProcessor, SearchResultApplierFactory $searchResultsApplierFactory, - ProductCollectionSearchCriteriaBuilder $searchCriteriaBuilder + ProductCollectionSearchCriteriaBuilder $searchCriteriaBuilder, + Visibility $catalogProductVisibility ) { $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; @@ -77,6 +86,7 @@ public function __construct( $this->collectionPostProcessor = $collectionPostProcessor; $this->searchResultApplierFactory = $searchResultsApplierFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->catalogProductVisibility = $catalogProductVisibility; } /** @@ -106,6 +116,7 @@ public function getList( $this->getSortOrderArray($searchCriteriaForCollection) )->apply(); + $collection->setVisibility($this->catalogProductVisibility->getVisibleInSiteIds()); $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); $collection->load(); $this->collectionPostProcessor->process($collection, $attributes); diff --git a/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php b/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php index 116a4529a8e60..f659df2ee0ecd 100644 --- a/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php +++ b/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php @@ -6,8 +6,8 @@ namespace Magento\CatalogRule\Cron; -use Magento\CatalogRule\Model\Indexer\PartialIndex; use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; +use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; /** * Daily update catalog price rule by cron @@ -20,24 +20,25 @@ class DailyCatalogUpdate protected $ruleProductProcessor; /** - * @var PartialIndex + * @var RuleCollectionFactory */ - private $partialIndex; + private $ruleCollectionFactory; /** * @param RuleProductProcessor $ruleProductProcessor - * @param PartialIndex $partialIndex + * @param RuleCollectionFactory $ruleCollectionFactory */ public function __construct( RuleProductProcessor $ruleProductProcessor, - PartialIndex $partialIndex + RuleCollectionFactory $ruleCollectionFactory ) { $this->ruleProductProcessor = $ruleProductProcessor; - $this->partialIndex = $partialIndex; + $this->ruleCollectionFactory = $ruleCollectionFactory; } /** * Daily update catalog price rule by cron + * * Update include interval 3 days - current day - 1 days before + 1 days after * This method is called from cron process, cron is working in UTC time and * we should generate data for interval -1 day ... +1 day @@ -46,8 +47,10 @@ public function __construct( */ public function execute() { - $this->ruleProductProcessor->isIndexerScheduled() - ? $this->partialIndex->partialUpdateCatalogRuleProductPrice() - : $this->ruleProductProcessor->markIndexerAsInvalid(); + $ruleCollection = $this->ruleCollectionFactory->create(); + $ruleCollection->addIsActiveFilter(); + if ($ruleCollection->getSize()) { + $this->ruleProductProcessor->markIndexerAsInvalid(); + } } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php b/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php deleted file mode 100644 index 12a77f81826d6..0000000000000 --- a/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php +++ /dev/null @@ -1,97 +0,0 @@ -resource = $resource; - $this->connection = $resource->getConnection(); - $this->indexBuilder = $indexBuilder; - } - - /** - * Synchronization replica table with original table "catalogrule_product_price" - * - * Used replica table for correctly working MySQL trigger - * - * @return void - */ - public function partialUpdateCatalogRuleProductPrice(): void - { - $this->indexBuilder->reindexFull(); - $indexTableName = $this->resource->getTableName('catalogrule_product_price'); - $select = $this->connection->select()->from( - ['crp' => $indexTableName], - 'product_id' - ); - $selectFields = $this->connection->select()->from( - ['crp' => $indexTableName], - [ - 'rule_date', - 'customer_group_id', - 'product_id', - 'rule_price', - 'website_id', - 'latest_start_date', - 'earliest_end_date', - ] - ); - $where = ['product_id' .' NOT IN (?)' => $select]; - //remove products that are no longer used in indexing - $this->connection->delete($this->resource->getTableName('catalogrule_product_price_replica'), $where); - //add updated products to indexing - $this->connection->query( - $this->connection->insertFromSelect( - $selectFields, - $this->resource->getTableName('catalogrule_product_price_replica'), - [ - 'rule_date', - 'customer_group_id', - 'product_id', - 'rule_price', - 'website_id', - 'latest_start_date', - 'earliest_end_date', - ], - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); - } -} diff --git a/app/code/Magento/CatalogRule/Test/Unit/Cron/DailyCatalogUpdateTest.php b/app/code/Magento/CatalogRule/Test/Unit/Cron/DailyCatalogUpdateTest.php index c3e596ca4961d..e1dd10f921155 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Cron/DailyCatalogUpdateTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Cron/DailyCatalogUpdateTest.php @@ -5,49 +5,71 @@ */ declare(strict_types=1); - namespace Magento\CatalogRule\Test\Unit\Cron; use Magento\CatalogRule\Cron\DailyCatalogUpdate; use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; +use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DailyCatalogUpdateTest extends TestCase { /** - * Processor - * * @var RuleProductProcessor|MockObject */ - protected $ruleProductProcessor; + private $ruleProductProcessor; + + /** + * @var RuleCollectionFactory|MockObject + */ + private $ruleCollectionFactory; /** - * Cron object - * * @var DailyCatalogUpdate */ - protected $cron; + private $cron; protected function setUp(): void { - $this->ruleProductProcessor = $this->createMock( - RuleProductProcessor::class - ); - - $this->cron = (new ObjectManager($this))->getObject( - DailyCatalogUpdate::class, - [ - 'ruleProductProcessor' => $this->ruleProductProcessor, - ] - ); + $this->ruleProductProcessor = $this->createMock(RuleProductProcessor::class); + $this->ruleCollectionFactory = $this->createMock(RuleCollectionFactory::class); + + $this->cron = new DailyCatalogUpdate($this->ruleProductProcessor, $this->ruleCollectionFactory); } - public function testDailyCatalogUpdate() + /** + * @dataProvider executeDataProvider + * @param int $activeRulesCount + * @param bool $isInvalidationNeeded + */ + public function testExecute(int $activeRulesCount, bool $isInvalidationNeeded) { - $this->ruleProductProcessor->expects($this->once())->method('markIndexerAsInvalid'); + $ruleCollection = $this->createMock(RuleCollection::class); + $this->ruleCollectionFactory->expects($this->once()) + ->method('create') + ->willReturn($ruleCollection); + $ruleCollection->expects($this->once()) + ->method('addIsActiveFilter') + ->willReturn($ruleCollection); + $ruleCollection->expects($this->once()) + ->method('getSize') + ->willReturn($activeRulesCount); + $this->ruleProductProcessor->expects($isInvalidationNeeded ? $this->once() : $this->never()) + ->method('markIndexerAsInvalid'); $this->cron->execute(); } + + /** + * @return array + */ + public function executeDataProvider(): array + { + return [ + [2, true], + [0, false], + ]; + } } diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 106e0ffabb2b2..daba451c79374 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -27,7 +27,6 @@ -
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml new file mode 100644 index 0000000000000..2e1c8d5a27886 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml @@ -0,0 +1,54 @@ + + + + + + + + + <description value="Verify that error message is correct for invalid a email entered with 'Sign in' form"/> + <stories value="Inconsistent customer email validation on frontend"/> + <severity value="MINOR"/> + <testCaseId value="MC-42729"/> + <group value="checkout"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">560</field> + </createData> + </before> + <after> + <!-- Delete created product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + </after> + + <!-- Add Simple Product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!-- Go to shopping cart --> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="clickProceedToCheckout"/> + <comment userInput="Adding the comment to replace waitForProceedToCheckout action for preserving Backward Compatibility" stepKey="waitForProceedToCheckout"/> + + <!-- Try to login using invalid email and Sign In link from checkout page --> + <click selector="{{StorefrontCustomerSignInLinkSection.signInLink}}" stepKey="clickOnCustomizeAndAddToCartButton"/> + <fillField selector="{{StorefrontCustomerSignInLinkSection.email}}" userInput="invalid @example.com" stepKey="fillEmail"/> + <fillField selector="{{StorefrontCustomerSignInLinkSection.password}}" userInput="Password123" stepKey="fillPassword"/> + <click selector="{{StorefrontCustomerSignInLinkSection.signInBtn}}" stepKey="clickSignInBtn"/> + + <waitForElementVisible selector="#login-email-error" stepKey="waitForFormValidation"/> + <see selector="#login-email-error" userInput="Please enter a valid email address (Ex: johndoe@domain.com)." stepKey="seeTheCorrectErrorMessageIsDisplayed"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index 9c00050d886e8..ad44c00763dcd 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -45,13 +45,6 @@ define([ resolveEstimationAddress: function () { var address; - if (checkoutData.getShippingAddressFromData()) { - address = addressConverter.formAddressDataToQuoteAddress(checkoutData.getShippingAddressFromData()); - selectShippingAddress(address); - } else { - this.resolveShippingAddress(); - } - if (quote.isVirtual()) { if (checkoutData.getBillingAddressFromData()) { address = addressConverter.formAddressDataToQuoteAddress( @@ -61,6 +54,11 @@ define([ } else { this.resolveBillingAddress(); } + } else if (checkoutData.getShippingAddressFromData()) { + address = addressConverter.formAddressDataToQuoteAddress(checkoutData.getShippingAddressFromData()); + selectShippingAddress(address); + } else { + this.resolveShippingAddress(); } }, diff --git a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html index 4afaf3c89a5e0..97930a26a2f99 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html @@ -42,18 +42,20 @@ <div class="block-content" aria-labelledby="block-customer-login-heading"> <form data-role="login" data-bind="submit:login" - method="post"> + method="post" + novalidate="novalidate"> <div class="fieldset" data-bind="attr: {'data-hasrequired': $t('* Required Fields')}"> <div class="field field-email required"> <label class="label" for="login-email"><span data-bind="i18n: 'Email Address'"></span></label> <div class="control"> - <input type="email" - class="input-text" + <input name="username" id="login-email" - name="username" + type="email" + class="input-text" data-bind="attr: {autocomplete: autocomplete}" - data-validate="{required:true, 'validate-email':true}" /> + data-validate="{required:true, 'validate-email':true}" + /> </div> </div> <div class="field field-password required"> diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/ScopedOptionSelectBuilder.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/ScopedOptionSelectBuilder.php new file mode 100644 index 0000000000000..20d1ac0c2d376 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/ScopedOptionSelectBuilder.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute; + +use Magento\Catalog\Model\ResourceModel\Product\Website as ProductWebsiteResource; +use Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface; +use Magento\Framework\DB\Select; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Plugin for OptionSelectBuilderInterface to filter by website assignments. + */ +class ScopedOptionSelectBuilder +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ProductWebsiteResource + */ + private $productWebsiteResource; + + /** + * @param StoreManagerInterface $storeManager + * @param ProductWebsiteResource $productWebsiteResource + */ + public function __construct( + StoreManagerInterface $storeManager, + ProductWebsiteResource $productWebsiteResource + ) { + $this->storeManager = $storeManager; + $this->productWebsiteResource = $productWebsiteResource; + } + + /** + * Add website filter to select. + * + * @param OptionSelectBuilderInterface $subject + * @param Select $select + * @return Select + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetSelect(OptionSelectBuilderInterface $subject, Select $select) + { + $store = $this->storeManager->getStore(); + $select->joinInner( + ['entity_website' => $this->productWebsiteResource->getMainTable()], + 'entity_website.product_id = entity.entity_id AND entity_website.website_id = ' . $store->getWebsiteId(), + [] + ); + + return $select; + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index 3942ec52cbb8b..56418bbaad122 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> + <plugin name="option_select_website_filter" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\ScopedOptionSelectBuilder"/> </type> <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> <plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index cd6d78e5c3ffb..7a327305fd27c 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -120,10 +120,10 @@ public function addEavAttributes(array $attributeCodes) : void * Retrieve child products from for passed in parent id. * * @param int $id - * @param ContextInterface|null $context + * @param ContextInterface $context * @return array */ - public function getChildProductsByParentId(int $id, ContextInterface $context = null) : array + public function getChildProductsByParentId(int $id, ContextInterface $context) : array { $childrenMap = $this->fetch($context); @@ -137,10 +137,10 @@ public function getChildProductsByParentId(int $id, ContextInterface $context = /** * Fetch all children products from parent id's. * - * @param ContextInterface|null $context + * @param ContextInterface $context * @return array */ - private function fetch(ContextInterface $context = null) : array + private function fetch(ContextInterface $context) : array { if (empty($this->parentProducts) || !empty($this->childrenMap)) { return $this->childrenMap; @@ -151,6 +151,7 @@ private function fetch(ContextInterface $context = null) : array /** @var ChildCollection $childCollection */ $childCollection = $this->childCollectionFactory->create(); $childCollection->setProductFilter($product); + $childCollection->addWebsiteFilter($context->getExtensionAttributes()->getStore()->getWebsiteId()); $this->collectionProcessor->process( $childCollection, $this->searchCriteriaBuilder->create(), diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index 06206e35712ad..56cff716ab6f8 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -50,6 +50,7 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> + <plugin name="option_select_website_filter" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\ScopedOptionSelectBuilder"/> </type> <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml index 337410d61c06d..d91093034dd2c 100644 --- a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml +++ b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml @@ -45,5 +45,10 @@ <actualResult type="variable">isCookieSecure</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> + <executeJS function="return jQuery.mage.cookies.defaults.secure ? 'true' : 'false'" stepKey="isCookieSecure2"/> + <assertEquals stepKey="assertCookieIsSecure2"> + <actualResult type="variable">isCookieSecure2</actualResult> + <expectedResult type="string">true</expectedResult> + </assertEquals> </test> </tests> diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml index dccb432273b89..fbf90df6bf898 100644 --- a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml +++ b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml @@ -32,5 +32,10 @@ <actualResult type="variable">isCookieSecure</actualResult> <expectedResult type="string">false</expectedResult> </assertEquals> + <executeJS function="return jQuery.mage.cookies.defaults.secure ? 'true' : 'false'" stepKey="isCookieSecure2"/> + <assertEquals stepKey="assertCookieIsSecure2"> + <actualResult type="variable">isCookieSecure2</actualResult> + <expectedResult type="string">false</expectedResult> + </assertEquals> </test> </tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml index 4eb5e8aa02401..4dcedb12c96b2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -20,6 +20,7 @@ <element name="pager" type="block" selector=".pager"/> <element name="createdDate" type="text" selector=".block-order-details-comments .comment-date"/> <element name="orderPlacedBy" type="text" selector=".block-order-details-comments .comment-content"/> + <element name="orderComment" type="text" selector=".block-order-details-comments .comment-content"/> <element name="productName" type="text" selector="//td[@data-th='Product Name']"/> <element name="productRows" type="text" selector="#my-orders-table tbody tr"/> <element name="productNameByRow" type="text" parameterized="true" selector="#my-orders-table tbody:nth-of-type({{index}}) td.name"/> diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php b/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php index f5a70d2d09538..50317809ffcfe 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php @@ -58,7 +58,7 @@ public function build() 'type' => 'custom', 'tokenizer' => key($tokenizer), 'filter' => array_merge( - ['lowercase', 'keyword_repeat'], + ['lowercase', 'keyword_repeat', 'asciifolding'], array_keys($filter) ), 'char_filter' => array_keys($charFilter) @@ -67,16 +67,14 @@ public function build() 'prefix_search' => [ 'type' => 'custom', 'tokenizer' => key($tokenizer), - 'filter' => array_merge( - ['lowercase', 'keyword_repeat'] - ), + 'filter' => ['lowercase', 'keyword_repeat', 'asciifolding'], 'char_filter' => array_keys($charFilter) ], 'sku' => [ 'type' => 'custom', 'tokenizer' => 'keyword', 'filter' => array_merge( - ['lowercase', 'keyword_repeat'], + ['lowercase', 'keyword_repeat', 'asciifolding'], array_keys($filter) ), ], @@ -84,9 +82,7 @@ public function build() 'sku_prefix_search' => [ 'type' => 'custom', 'tokenizer' => 'keyword', - 'filter' => array_merge( - ['lowercase', 'keyword_repeat'] - ), + 'filter' => ['lowercase', 'keyword_repeat', 'asciifolding'] ] ], 'tokenizer' => $tokenizer, diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 54b8c1966ee12..07d05bae73d4c 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -69,22 +69,11 @@ public function apply() foreach ($items as $item) { $ids[] = (int)$item->getId(); } + $orderList = join(',', $ids); $this->collection->getSelect() ->where('e.entity_id IN (?)', $ids) - ->reset(\Magento\Framework\DB\Select::ORDER); - $sortOrder = $this->searchResult->getSearchCriteria() - ->getSortOrders(); - if (!empty($sortOrder['price']) && $this->collection->getLimitationFilters()->isUsingPriceIndex()) { - $sortDirection = $sortOrder['price']; - $this->collection->getSelect() - ->order( - new \Zend_Db_Expr("price_index.min_price = 0, price_index.min_price {$sortDirection}") - ); - } else { - $orderList = join(',', $ids); - $this->collection->getSelect() - ->order(new \Zend_Db_Expr("FIELD(e.entity_id,$orderList)")); - } + ->reset(\Magento\Framework\DB\Select::ORDER) + ->order(new \Zend_Db_Expr("FIELD(e.entity_id,$orderList)")); } /** diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 18435d85574f3..120d67931597f 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -36,6 +36,8 @@ use Magento\Variable\Model\Variable; use Magento\Variable\Model\VariableFactory; use Psr\Log\LoggerInterface; +use Magento\Store\Model\Information as StoreInformation; +use Magento\Framework\App\ObjectManager; /** * Core Email Template Filter Model @@ -201,6 +203,11 @@ class Filter extends Template */ private $pubDirectoryRead; + /** + * @var StoreInformation + */ + private $storeInformation; + /** * Filter constructor. @@ -222,6 +229,7 @@ class Filter extends Template * @param CssInliner $cssInliner * @param array $variables * @param array $directiveProcessors + * @param StoreInformation|null $storeInformation * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -242,7 +250,8 @@ public function __construct( Filesystem $pubDirectory, CssInliner $cssInliner, $variables = [], - array $directiveProcessors = [] + array $directiveProcessors = [], + ?StoreInformation $storeInformation = null ) { $this->_escaper = $escaper; $this->_assetRepo = $assetRepo; @@ -259,6 +268,8 @@ public function __construct( $this->cssProcessor = $cssProcessor; $this->pubDirectory = $pubDirectory; $this->configVariables = $configVariables; + $this->storeInformation = $storeInformation ?: + ObjectManager::getInstance()->get(StoreInformation::class); parent::__construct($string, $variables, $directiveProcessors, $variableResolver); } @@ -825,18 +836,29 @@ private function validateProtocolDirectiveHttpScheme(array $params) : void * * @param string[] $construction * @return string + * @throws NoSuchEntityException */ public function configDirective($construction) { $configValue = ''; $params = $this->getParameters($construction[2]); $storeId = $this->getStoreId(); + $store = $this->_storeManager->getStore($storeId); + $storeInformationObj = $this->storeInformation + ->getStoreInformationObject($store); if (isset($params['path']) && $this->isAvailableConfigVariable($params['path'])) { $configValue = $this->_scopeConfig->getValue( $params['path'], ScopeInterface::SCOPE_STORE, $storeId ); + if ($params['path'] == $this->storeInformation::XML_PATH_STORE_INFO_COUNTRY_CODE) { + $configValue = $storeInformationObj->getData('country'); + } elseif ($params['path'] == $this->storeInformation::XML_PATH_STORE_INFO_REGION_CODE) { + $configValue = $storeInformationObj->getData('region')? + $storeInformationObj->getData('region'): + $configValue; + } } return $configValue; } diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php index 97a684b92be5d..923193f6e6a92 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -14,6 +14,7 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\DataObject; use Magento\Framework\Exception\MailException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\App\State; @@ -35,12 +36,14 @@ use Magento\Framework\View\LayoutFactory; use Magento\Framework\View\LayoutInterface; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Variable\Model\Source\Variables; use Magento\Variable\Model\VariableFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Magento\Store\Model\Information as StoreInformation; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -148,6 +151,16 @@ class FilterTest extends TestCase */ private $directiveProcessors; + /** + * @var StoreInformation + */ + private $storeInformation; + + /** + * @var store + */ + private $store; + protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -232,6 +245,14 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(), ]; + + $this->store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeInformation = $this->getMockBuilder(StoreInformation::class) + ->disableOriginalConstructor() + ->getMock(); } /** @@ -260,7 +281,8 @@ protected function getModel($mockedMethods = null) $this->pubDirectory, $this->cssInliner, [], - $this->directiveProcessors + $this->directiveProcessors, + $this->storeInformation ] ) ->setMethods($mockedMethods) @@ -421,12 +443,10 @@ public function testConfigDirectiveAvailable() $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; $scopeConfigValue = 'value'; - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getId')->willReturn(1); + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->any())->method('getId')->willReturn(1); $this->configVariables->expects($this->once()) ->method('getData') @@ -435,6 +455,10 @@ public function testConfigDirectiveAvailable() ->method('getValue') ->willReturn($scopeConfigValue); + $this->storeInformation->expects($this->once()) + ->method('getStoreInformationObject') + ->willReturn(new DataObject([])); + $this->assertEquals($scopeConfigValue, $this->getModel()->configDirective($construction)); } @@ -445,11 +469,10 @@ public function testConfigDirectiveUnavailable() $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; $scopeConfigValue = ''; - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getId')->willReturn(1); + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->any())->method('getId')->willReturn(1); $this->configVariables->expects($this->once()) ->method('getData') @@ -458,9 +481,65 @@ public function testConfigDirectiveUnavailable() ->method('getValue') ->willReturn($scopeConfigValue); + $this->storeInformation->expects($this->once()) + ->method('getStoreInformationObject') + ->willReturn(new DataObject([])); + $this->assertEquals($scopeConfigValue, $this->getModel()->configDirective($construction)); } + /** + * @throws NoSuchEntityException + */ + public function testConfigDirectiveGetCountry() + { + $path = "general/store_information/country_id"; + $availableConfigs = [['value' => $path]]; + $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; + $expectedCountry = 'United States'; + + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->any())->method('getId')->willReturn(1); + + $this->configVariables->expects($this->once()) + ->method('getData') + ->willReturn($availableConfigs); + + $this->storeInformation->expects($this->once()) + ->method('getStoreInformationObject') + ->willReturn(new DataObject(['country_id' => 'US', 'country' => 'United States'])); + + $this->assertEquals($expectedCountry, $this->getModel()->configDirective($construction)); + } + + /** + * @throws NoSuchEntityException + */ + public function testConfigDirectiveGetRegion() + { + $path = "general/store_information/region_id"; + $availableConfigs = [['value' => $path]]; + $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; + $expectedRegion = 'Texas'; + + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->any())->method('getId')->willReturn(1); + + $this->configVariables->expects($this->once()) + ->method('getData') + ->willReturn($availableConfigs); + + $this->storeInformation->expects($this->once()) + ->method('getStoreInformationObject') + ->willReturn(new DataObject(['region_id' => '57', 'region' => 'Texas'])); + + $this->assertEquals($expectedRegion, $this->getModel()->configDirective($construction)); + } + /** * @throws MailException * @throws NoSuchEntityException diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index 35b07ebdb7c44..c649d0252c298 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -182,6 +182,7 @@ public function convertCustomerCartToGuest() $quote->getAddressesCollection()->walk('setCustomerId', ['customerId' => null]); $quote->getAddressesCollection()->walk('setEmail', ['email' => null]); $quote->collectTotals(); + $quote->getCustomer()->setId(null); $this->persistentSession->getSession()->removePersistentCookie(); $this->persistentSession->setSession(null); $this->quoteRepository->save($quote); diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index ece36673c1e82..f3c70a2c8550f 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -134,6 +134,7 @@ protected function setUp(): void 'setExtensionAttributes', '__wakeup', 'setCustomer', + 'getCustomer' ]) ->disableOriginalConstructor() ->getMock(); @@ -343,6 +344,13 @@ public function testConvertCustomerCartToGuest() ->method('setIsPersistent')->with(false)->willReturn($this->quoteMock); $this->quoteMock->expects($this->exactly(3)) ->method('getAddressesCollection')->willReturn($this->abstractCollectionMock); + $customerMock = $this->createMock(CustomerInterface::class); + $customerMock->expects($this->once()) + ->method('setId') + ->with(null) + ->willReturnSelf(); + $this->quoteMock->expects($this->once()) + ->method('getCustomer')->willReturn($customerMock); $this->abstractCollectionMock->expects($this->exactly(3))->method('walk')->with( $this->logicalOr( $this->equalTo('setCustomerAddressId'), diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml new file mode 100644 index 0000000000000..5b346040db818 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUploadSameVimeoVideoForMultipleProductsTest"> + <annotations> + <features value="ProductVideo"/> + <stories value="Upload product video"/> + <title value="Admin should be able to upload same Vimeo video for multiple products"/> + <description value="Admin should be able to upload same Vimeo video for multiple products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-42645"/> + <useCaseId value="MC-42448"/> + <group value="productVideo"/> + </annotations> + <before> + <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setYoutubeApiKeyConfig"/> + <createData entity="SimpleProduct2" stepKey="createProduct1"/> + <createData entity="SimpleProduct2" stepKey="createProduct2"/> + <!-- Login to Admin page --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <createData entity="DefaultProductVideoConfig" stepKey="setYoutubeApiKeyDefaultConfig"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <!-- Logout from Admin page --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!-- Open product 1 edit page --> + <amOnPage url="{{AdminProductEditPage.url($createProduct1.id$)}}" stepKey="goToProduct1EditPage"/> + <!-- Add product video --> + <actionGroup ref="AddProductVideoActionGroup" stepKey="addProductVideoToProduct1"> + <argument name="video" value="VimeoProductVideo"/> + </actionGroup> + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductFormOfProduct1"/> + + <!-- Open product 2 edit page --> + <amOnPage url="{{AdminProductEditPage.url($createProduct2.id$)}}" stepKey="goToProduct2EditPage"/> + <!-- Add product video --> + <actionGroup ref="AddProductVideoActionGroup" stepKey="saveProductFormOfProduct2"> + <argument name="video" value="VimeoProductVideo"/> + </actionGroup> + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductFormOfSecondSimpleProduct"/> + + </test> +</tests> diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml index b09f2fb376327..cf47b2f8e18c4 100755 --- a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml @@ -30,4 +30,8 @@ <requiredEntity type="payment_method">CashOnDeliveryPaymentMethod</requiredEntity> <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> </entity> + + <entity name="GetOrderData" type="CustomerCart"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + </entity> </entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml index f233954f2cdcf..95773d82beb29 100644 --- a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml @@ -60,4 +60,9 @@ <field key="method">string</field> </object> </operation> + + <operation name="GetOrderData" dataType="CustomerCart" type="get" auth="adminOauth" url="/V1/orders/{return}" method="GET"> + <contentType>application/json</contentType> + </operation> + </operations> diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php index ad305c8b7199f..3a4e8184f82cb 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php @@ -75,6 +75,7 @@ public function send(Shipment $shipment, $notify = true, $comment = '') 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), 'order_data' => [ 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), 'frontend_status_label' => $order->getFrontendStatusLabel() ] ]; diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenOrderViewPageByOrderIdActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenOrderViewPageByOrderIdActionGroup.xml new file mode 100644 index 0000000000000..5bd1d5907f69d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenOrderViewPageByOrderIdActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenOrderViewPageByOrderIdActionGroup"> + <annotations> + <description>Opens the Order View Page in Admin using order_id</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderViewPage.url(orderId)}}" stepKey="openOrderViewPage"/> + <waitForPageLoad stepKey="waitForOrderViewPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSalesOrderCommentsActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSalesOrderCommentsActionGroup.xml new file mode 100644 index 0000000000000..af0237d3a6606 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSalesOrderCommentsActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSalesOrderCommentsActionGroup"> + <annotations> + <description>Adding comments on admin sales order details page</description> + </annotations> + <arguments> + <argument name="comment" type="string" defaultValue=""/> + </arguments> + <fillField selector="{{AdminSalesOrderCommentsSection.historyComment}}" userInput="{{comment}}" stepKey="fillComment"/> + <checkOption selector="{{AdminSalesOrderCommentsSection.historyVisible}}" stepKey="checkVisibleOnStoreFront"/> + <click selector="{{AdminSalesOrderCommentsSection.submitOrderComment}}" stepKey="clickSaveCommentButton"/> + <waitForPageLoad stepKey="waitForSaveComments"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOpenMyOrdersPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOpenMyOrdersPageActionGroup.xml new file mode 100644 index 0000000000000..39f4e65f4c687 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOpenMyOrdersPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenMyOrdersPageActionGroup"> + <annotations> + <description>Opens the "My Account->"My Orders" Page</description> + </annotations> + + <amOnPage url="/sales/order/history/" stepKey="openMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderCommentsData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderCommentsData.xml new file mode 100644 index 0000000000000..b7c5bb457ca08 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderCommentsData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesOrderComments" type="order"> + <data key="commentWithHyperlink"><a href="https://business.adobe.com/products/magento/magento-commerce.html">Testing Html Tags</a></data> + </entity> + <entity name="anchorTagFragment" type="string"> + <data key="anchorTag">a href</data> + </entity> +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminSalesOrderCommentsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminSalesOrderCommentsSection.xml new file mode 100644 index 0000000000000..c7f2628a900e3 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminSalesOrderCommentsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSalesOrderCommentsSection"> + <element name="historyComment" type="textarea" selector="#history_comment"/> + <element name="historyVisible" type="checkbox" selector="#history_visible"/> + <element name="submitOrderComment" type="button" selector=".order-history-comments-actions>button"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml index 5f454152de20c..a4a24e77838db 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCashOnDeliveryPaymentMethodTest.xml @@ -22,19 +22,25 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!-- Enable Cash On Delivery payment method --> <magentoCLI command="config:set {{EnableCashOnDeliveryConfigData.path}} {{EnableCashOnDeliveryConfigData.value}}" stepKey="enableCashOnDeliveryPayment"/> - - <!--Set default flat rate shipping method settings--> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> - <!--Create simple customer--> <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> - <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="simpleProduct"> <field key="price">10.00</field> </createData> + + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="simpleCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="simpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> </before> <after> <magentoCLI command="config:set {{DisableCashOnDeliveryConfigData.path}} {{DisableCashOnDeliveryConfigData.value}}" stepKey="disableCashOnDeliveryPayment"/> @@ -43,39 +49,34 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!-- Create new customer order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> - <argument name="customer" value="$$simpleCustomer$$"/> - </actionGroup> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="navigateToNewOrderWithExistingCustomer"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="addSimpleProductToTheOrder"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectFlatRateShippingMethod"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPaymentOptions"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCashOnDeliveryPaymentOption"/> - <!-- Add Simple product to order --> - <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToTheOrder"> - <argument name="product" value="$$simpleProduct$$"/> - </actionGroup> - - <!-- Select FlatRate shipping method --> - <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <updateData createDataKey="createCustomerCart" entity="CashOnDeliveryOrderPaymentMethod" stepKey="submitOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> - <!-- Select Cash On Delivery payment method --> - <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> - <checkOption selector="{{AdminOrderFormPaymentSection.cashOnDeliveryOption}}" stepKey="selectCashOnDeliveryPaymentOption"/> - <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="verifyCreatedOrderInformation"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="orderId"/> - <!--Verify order information--> - <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> - <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <actionGroup ref="AdminOpenOrderViewPageByOrderIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="$createCustomerCart.return$"/> + </actionGroup> - <!-- Cancel the Order --> <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelPendingOrder"/> - - <!--Log in to Storefront as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> <argument name="Customer" value="$$simpleCustomer$$"/> </actionGroup> - - <!-- Assert Order status in frontend grid --> - <click selector="{{StorefrontCustomerSidebarSection.sidebarCurrentTab('My Orders')}}" stepKey="clickOnMyOrders"/> - <waitForPageLoad stepKey="waitForOrderDetailsToLoad"/> - <seeElement selector="{{StorefrontCustomerOrderViewSection.orderStatusInGrid('$orderId', 'Canceled')}}" stepKey="seeOrderStatusInGrid"/> + + <getData entity="GetOrderData" stepKey="getOrderData"> + <requiredEntity createDataKey="createCustomerCart"/> + </getData> + <actionGroup ref="StorefrontOpenMyOrdersPageActionGroup" stepKey="clickOnMyOrders"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForOrderDetailsToLoad"/> + <seeElement selector="{{StorefrontCustomerOrderViewSection.orderStatusInGrid('$getOrderData.increment_id$', 'Canceled')}}" stepKey="seeOrderStatusInGrid"/> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml new file mode 100644 index 0000000000000..2f4042d4d9760 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyOrderHistoryCommentsTest"> + <annotations> + <features value="Sales"/> + <stories value="Storefront Customer Order History Comments"/> + <title value="Verify Customer Order History Comments For Storefront My Account Sales Pages"/> + <description value="Verify that the Customer order History comments without the HTML tags to the My Orders pages on the Storefront"/> + <testCaseId value="MC-42694"/> + <useCaseId value="MC-42531"/> + <severity value="MINOR"/> + <group value="Sales"/> + </annotations> + <before> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Create product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + <after> + <!-- Customer log out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <!-- Admin log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <!-- Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Add product to cart --> + <actionGroup ref="StorefrontAddSimpleProductWithQtyActionGroup" stepKey="addSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="orderNumber"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="addFilterToGridAndOpenOrder"> + <argument name="orderId" value="{$orderNumber}"/> + </actionGroup> + + <actionGroup ref="AdminSalesOrderCommentsActionGroup" stepKey="fillAndSaveOrderComments"> + <argument name="comment" value="{{SalesOrderComments.commentWithHyperlink}}" /> + </actionGroup> + + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="navigateToCustomerDashboardPage"/> + <actionGroup ref="StorefrontClickViewOrderLinkOnMyOrdersPageActionGroup" stepKey="clickViewOrder"/> + + <dontSee userInput="{{anchorTagFragment.anchorTag}}" selector="{{StorefrontCustomerOrderViewSection.orderComment}}" stepKey="dontSeeExposedHtmlCode"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php index 91e7ae18d3550..a3bf17ec4b27f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php @@ -62,8 +62,9 @@ public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; - $customerName='Test Customer'; - $frontendStatusLabel='Processing'; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Processing'; + $isNotVirtual = true; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -76,6 +77,9 @@ public function testSendTrueWithoutCustomerCopy() $this->orderMock->expects($this->any()) ->method('getCustomerName') ->willReturn($customerName); + $this->orderMock->expects($this->any()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); $this->orderMock->expects($this->once()) ->method('getFrontendStatusLabel') ->willReturn($frontendStatusLabel); @@ -92,7 +96,8 @@ public function testSendTrueWithoutCustomerCopy() 'formattedBillingAddress' => 1, 'order_data' => [ 'customer_name' => $customerName, - 'frontend_status_label' => $frontendStatusLabel + 'frontend_status_label' => $frontendStatusLabel, + 'is_not_virtual' => $isNotVirtual, ] ] ); @@ -105,8 +110,9 @@ public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; - $customerName='Test Customer'; - $frontendStatusLabel='Processing'; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Processing'; + $isNotVirtual = true; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -122,6 +128,9 @@ public function testSendTrueWithCustomerCopy() $this->orderMock->expects($this->any()) ->method('getCustomerName') ->willReturn($customerName); + $this->orderMock->expects($this->any()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); $this->orderMock->expects($this->once()) ->method('getFrontendStatusLabel') ->willReturn($frontendStatusLabel); @@ -138,7 +147,8 @@ public function testSendTrueWithCustomerCopy() 'formattedBillingAddress' => 1, 'order_data' => [ 'customer_name' => $customerName, - 'frontend_status_label' => $frontendStatusLabel + 'frontend_status_label' => $frontendStatusLabel, + 'is_not_virtual' => $isNotVirtual, ] ] ); @@ -152,8 +162,9 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); - $customerName='Test Customer'; - $frontendStatusLabel='Complete'; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->identityContainerMock->expects($this->once()) ->method('isEnabled') @@ -161,6 +172,9 @@ public function testSendVirtualOrder() $this->orderMock->expects($this->any()) ->method('getCustomerName') ->willReturn($customerName); + $this->orderMock->expects($this->any()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); $this->orderMock->expects($this->once()) ->method('getFrontendStatusLabel') ->willReturn($frontendStatusLabel); @@ -177,7 +191,8 @@ public function testSendVirtualOrder() 'formattedBillingAddress' => 1, 'order_data' => [ 'customer_name' => $customerName, - 'frontend_status_label' => $frontendStatusLabel + 'frontend_status_label' => $frontendStatusLabel, + 'is_not_virtual' => $isNotVirtual ] ] diff --git a/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml b/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml index 6f2d8d87ade86..cb1e235566c8e 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml @@ -17,7 +17,7 @@ <?= /* @noEscape */ $block->formatDate($_historyItem->getCreatedAt(), \IntlDateFormatter::MEDIUM, true) ?> </dt> - <dd class="comment-content"><?= $block->escapeHtml($_historyItem->getComment()) ?></dd> + <dd class="comment-content"><?= $block->escapeHtml($_historyItem->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></dd> <?php endforeach; ?> </dl> </div> diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php index cfafe110df22b..0bca04a955817 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php @@ -80,6 +80,10 @@ public function execute() if (!$rule->getId()) { $result['error'] = __('Rule is not defined'); + } elseif ((int) $rule->getCouponType() !== \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO + && !$rule->getUseAutoGeneration()) { + $result['error'] = + __('The rule coupon settings changed. Please save the rule before using auto-generation.'); } else { try { $data = $this->getRequest()->getParams(); diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index df126f05819d0..7eb95da0d7500 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -104,5 +104,6 @@ <element name="generatedCouponByIndex" type="text" selector="#couponCodesGrid_table > tbody > tr:nth-child({{var}}) > td.col-code" parameterized="true"/> <element name="couponGridUsedHeader" type="text" selector="#couponCodesGrid thead th[data-sort='used']"/> <element name="fieldError" type="text" selector="//input[@name='{{fieldName}}']/following-sibling::label[@class='admin__field-error']" parameterized="true"/> + <element name="modalMessage" type="text" selector="aside.modal-popup div.modal-content div"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml new file mode 100644 index 0000000000000..a4318103c4c00 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Block the coupon generates until cart price rule is saved with Specific Coupon type and Use Auto Generation ticked"/> + <description + value="Block the coupon generates until cart price rule is saved with Specific Coupon type and Use Auto Generation ticked"/> + <severity value="MINOR"/> + <testCaseId value="MC-42602"/> + <useCaseId value="MC-42288"/> + <group value="salesRule"/> + </annotations> + + <before> + <createData entity="ApiCartRule" stepKey="createSalesRule"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Search Cart Price Rule and go to edit Cart Price Rule --> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="$$createSalesRule.name$$" + stepKey="fillFieldFilterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="clickSearchButton"/> + <see selector="{{AdminCartPriceRulesSection.nameColumns}}" userInput="$$createSalesRule.name$$" + stepKey="seeRuleName"/> + <click selector="{{AdminCartPriceRulesSection.rowContainingText($$createSalesRule.name$$)}}" + stepKey="goToEditRule"/> + + <!-- Choose coupon type specific coupon and tick auto generation checkbox --> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <checkOption selector="{{AdminCartPriceRulesFormSection.useAutoGeneration}}" stepKey="tickAutoGeneration"/> + + <!-- Navigate to Manage Coupon Codes section to generate 1 coupon code --> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" + dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" + stepKey="clickManageCouponCodes"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponQty}}" userInput="1" stepKey="fillFieldCouponQty"/> + <click selector="{{AdminCartPriceRulesFormSection.generateCouponsButton}}" stepKey="clickGenerateCoupon"/> + <see selector="{{AdminCartPriceRulesFormSection.modalMessage}}" userInput="The rule coupon settings changed. Please save the rule before using auto-generation." + stepKey="seeModalMessage"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php b/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php index 9c8761eb39738..fd0b864ab9201 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php @@ -137,9 +137,9 @@ protected function setUp(): void } /** - * testExecute + * @covers \Magento\SalesRule\Controller\Adminhtml\Promo\Quote::execute */ - public function testExecute() + public function testExecuteWithCouponTypeAuto() { $helperData = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() @@ -157,6 +157,8 @@ public function testExecute() ->method('isAjax') ->willReturn(true); $ruleMock = $this->getMockBuilder(Rule::class) + ->addMethods(['getCouponType']) + ->onlyMethods(['getId']) ->disableOriginalConstructor() ->getMock(); $this->registryMock->expects($this->once()) @@ -165,6 +167,9 @@ public function testExecute() $ruleMock->expects($this->once()) ->method('getId') ->willReturn(1); + $ruleMock->expects($this->once()) + ->method('getCouponType') + ->willReturn(\Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO); $this->requestMock->expects($this->once()) ->method('getParams') ->willReturn($requestData); @@ -202,4 +207,120 @@ public function testExecute() ->willReturn(__('%1 coupon(s) have been generated.', 2)); $this->model->execute(); } + + /** + * @covers \Magento\SalesRule\Controller\Adminhtml\Promo\Quote::execute + */ + public function testExecuteWithAutoGenerationEnabled() + { + $helperData = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(Data::class) + ->willReturn($helperData); + $requestData = [ + 'qty' => 2, + 'length' => 10, + 'rule_id' => 1 + ]; + $this->requestMock->expects($this->once()) + ->method('isAjax') + ->willReturn(true); + $ruleMock = $this->getMockBuilder(Rule::class) + ->addMethods(['getUseAutoGeneration']) + ->onlyMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $this->registryMock->expects($this->once()) + ->method('registry') + ->willReturn($ruleMock); + $ruleMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $ruleMock->expects($this->once()) + ->method('getUseAutoGeneration') + ->willReturn(1); + $this->requestMock->expects($this->once()) + ->method('getParams') + ->willReturn($requestData); + $requestData['quantity'] = isset($requestData['qty']) ? $requestData['qty'] : null; + $this->couponGenerationSpec->expects($this->once()) + ->method('create') + ->with(['data' => $requestData]) + ->willReturn(['some_data', 'some_data_2']); + $this->messageManager->expects($this->once()) + ->method('addSuccessMessage'); + $this->responseMock->expects($this->once()) + ->method('representJson') + ->with(); + $helperData->expects($this->once()) + ->method('jsonEncode') + ->with([ + 'messages' => __('%1 coupon(s) have been generated.', 2) + ]); + $layout = $this->getMockBuilder(Layout::class) + ->disableOriginalConstructor() + ->getMock(); + $this->view->expects($this->any()) + ->method('getLayout') + ->willReturn($layout); + $messageBlock = $this->getMockBuilder(Messages::class) + ->disableOriginalConstructor() + ->getMock(); + $layout->expects($this->once()) + ->method('initMessages'); + $layout->expects($this->once()) + ->method('getMessagesBlock') + ->willReturn($messageBlock); + $messageBlock->expects($this->once()) + ->method('getGroupedHtml') + ->willReturn(__('%1 coupon(s) have been generated.', 2)); + $this->model->execute(); + } + + /** + * @covers \Magento\SalesRule\Controller\Adminhtml\Promo\Quote::execute + */ + public function testExecuteWithCouponTypeNotAutoAndAutoGenerationNotEnabled() + { + $helperData = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(Data::class) + ->willReturn($helperData); + $this->requestMock->expects($this->once()) + ->method('isAjax') + ->willReturn(true); + $ruleMock = $this->getMockBuilder(Rule::class) + ->addMethods(['getUseAutoGeneration', 'getCouponType']) + ->onlyMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $this->registryMock->expects($this->once()) + ->method('registry') + ->willReturn($ruleMock); + $ruleMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $ruleMock->expects($this->once()) + ->method('getCouponType') + ->willReturn(\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON); + $ruleMock->expects($this->once()) + ->method('getUseAutoGeneration') + ->willReturn(0); + $this->responseMock->expects($this->once()) + ->method('representJson') + ->with(); + $helperData->expects($this->once()) + ->method('jsonEncode') + ->with([ + 'error' => + __('The rule coupon settings changed. Please save the rule before using auto-generation.') + ]); + $this->model->execute(); + } } diff --git a/app/code/Magento/SalesRule/i18n/en_US.csv b/app/code/Magento/SalesRule/i18n/en_US.csv index 83a5aa76ba0c8..d4f93c25dc46c 100644 --- a/app/code/Magento/SalesRule/i18n/en_US.csv +++ b/app/code/Magento/SalesRule/i18n/en_US.csv @@ -164,3 +164,4 @@ Apply,Apply "Apply to Shipping Amount","Apply to Shipping Amount" "Discard subsequent rules","Discard subsequent rules" "Default Rule Label for All Store Views","Default Rule Label for All Store Views" +"Rule is not saved with auto generate option enabled. Please save the rule and try again.", "Rule is not saved with auto generate option enabled. Please save the rule and try again." diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index d8645a86d2e20..51dd1d2d34f44 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -6,6 +6,7 @@ namespace Magento\Tax\Model\Sales\Total\Quote; +use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement; use Magento\Customer\Api\Data\AddressInterfaceFactory as CustomerAddressFactory; use Magento\Customer\Api\Data\AddressInterface as CustomerAddress; use Magento\Customer\Api\Data\RegionInterfaceFactory as CustomerAddressRegionFactory; @@ -144,6 +145,11 @@ class CommonTaxCollector extends AbstractTotal */ private $quoteDetailsItemExtensionFactory; + /** + * @var CustomerAccountManagement + */ + private $customerAccountManagement; + /** * Class constructor * @@ -156,6 +162,8 @@ class CommonTaxCollector extends AbstractTotal * @param CustomerAddressRegionFactory $customerAddressRegionFactory * @param TaxHelper|null $taxHelper * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory + * @param CustomerAccountManagement|null $customerAccountManagement + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Tax\Model\Config $taxConfig, @@ -166,7 +174,8 @@ public function __construct( CustomerAddressFactory $customerAddressFactory, CustomerAddressRegionFactory $customerAddressRegionFactory, TaxHelper $taxHelper = null, - QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null + QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null, + ?CustomerAccountManagement $customerAccountManagement = null ) { $this->taxCalculationService = $taxCalculationService; $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory; @@ -178,6 +187,8 @@ public function __construct( $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class); $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?: ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class); + $this->customerAccountManagement = $customerAccountManagement ?? + ObjectManager::getInstance()->get(CustomerAccountManagement::class); } /** @@ -411,7 +422,24 @@ public function mapItems( public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAddress $address) { $quoteDetails->setBillingAddress($this->mapAddress($address->getQuote()->getBillingAddress())); - $quoteDetails->setShippingAddress($this->mapAddress($address)); + if ($address->getAddressType() === QuoteAddress::ADDRESS_TYPE_BILLING + && !$address->getCountryId() + && $address->getQuote()->isVirtual() + && $address->getQuote()->getCustomerId() + ) { + $defaultBillingAddress = $this->customerAccountManagement->getDefaultBillingAddress( + $address->getQuote()->getCustomerId() + ); + $addressCopy = $address; + if ($defaultBillingAddress) { + $addressCopy = clone $address; + $addressCopy->importCustomerAddressData($defaultBillingAddress); + } + + $quoteDetails->setShippingAddress($this->mapAddress($addressCopy)); + } else { + $quoteDetails->setShippingAddress($this->mapAddress($address)); + } return $quoteDetails; } diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminCreateTaxRuleWithTwoTaxRatesActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminCreateTaxRuleWithTwoTaxRatesActionGroup.xml new file mode 100644 index 0000000000000..0232d2920370a --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminCreateTaxRuleWithTwoTaxRatesActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateTaxRuleWithTwoTaxRatesActionGroup" extends="AdminCreateTaxRuleActionGroup"> + <arguments> + <argument name="taxRate2"/> + </arguments> + <click selector="{{AdminTaxRulesSection.selectTaxRate(taxRate2.code)}}" stepKey="selectTaxRate2" after="selectTaxRate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml new file mode 100644 index 0000000000000..0ce06894ab007 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerOrderViewSection"> + <element name="totalsTax" type="text" selector=".totals-tax-summary .amount .price" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml new file mode 100644 index 0000000000000..484135f96735d --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest"> + <annotations> + <features value="Tax"/> + <stories value="Tax Calculation in Shopping Cart"/> + <title value="Tax for quote with virtual products only should be calculated based on customer default billing address"/> + <description value="Tax for quote with virtual products only should be calculated based on customer default billing address"/> + <severity value="CRITICAL"/> + <useCaseId value="MC-41945"/> + <testCaseId value="MC-42650"/> + <group value="Tax"/> + </annotations> + <before> + <!-- Login to admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Fill in rules to display tax in the cart --> + <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> + <!-- Create tax rate for TX --> + <createData entity="TaxRateTexas" stepKey="createTaxRateTX"/> + <!-- Create tax rule --> + <actionGroup ref="AdminCreateTaxRuleWithTwoTaxRatesActionGroup" stepKey="createTaxRule"> + <argument name="taxRate" value="$$createTaxRateTX$$"/> + <argument name="taxRate2" value="US_NY_Rate_1"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + <!-- Create a virtual product --> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> + <!-- Create customer --> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <!-- Ensure tax won't be shown in the cart --> + <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> + <!-- Delete tax rule --> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <!-- Delete tax rate for UK --> + <deleteData createDataKey="createTaxRateTX" stepKey="deleteTaxRateUK"/> + <!-- Delete virtual product --> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Logout from admin --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + </after> + <!-- Login with created Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Navigate to the product --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$createVirtualProduct$$"/> + </actionGroup> + <!--Add to cart --> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product2AddToCart"/> + <!--Click on mini cart--> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> + <!--Click on view and edit cart link--> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <waitForPageLoad stepKey="waitForViewAndEditCartToOpen"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.tax}}" stepKey="waitForOverviewVisible1" /> + <!-- Verify tax in shopping cart --> + <see selector="{{CheckoutPaymentSection.tax}}" userInput="$7.25" stepKey="verifyTaxInShoppingCartPage" /> + <!-- Navigate to payment page --> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goToCheckout"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.tax}}" stepKey="waitForOverviewVisible2"/> + <!-- Verify tax on payment page --> + <see selector="{{CheckoutPaymentSection.tax}}" userInput="$7.25" stepKey="verifyTaxOnPaymentPage"/> + <!-- Place order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlacePurchaseOrder"/> + <!-- Navigate to order details page --> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <actionGroup ref="StorefrontOpenOrderFromSuccessPageActionGroup" stepKey="openOrderFromSuccessPage"> + <argument name="orderNumber" value="{$grabOrderNumber}"/> + </actionGroup> + <!-- Verify tax on order view page --> + <see selector="{{StorefrontCustomerOrderViewSection.totalsTax}}" userInput="$7.25" stepKey="verifyTaxOnOrderViewPage"/> + </test> +</tests> diff --git a/app/code/Magento/Theme/ViewModel/Block/SessionConfig.php b/app/code/Magento/Theme/ViewModel/Block/SessionConfig.php new file mode 100644 index 0000000000000..d697c0a61b448 --- /dev/null +++ b/app/code/Magento/Theme/ViewModel/Block/SessionConfig.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\ViewModel\Block; + +use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Provide cookie configuration + */ +class SessionConfig implements ArgumentInterface +{ + /** + * Session config + * + * @var ConfigInterface + */ + private $sessionConfig; + + /** + * Constructor + * + * @param ConfigInterface $sessionConfig + */ + public function __construct( + ConfigInterface $sessionConfig + ) { + $this->sessionConfig = $sessionConfig; + } + /** + * Get session.cookie_secure + * + * @return bool + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + */ + public function getCookieSecure(): bool + { + return $this->sessionConfig->getCookieSecure(); + } +} diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 933d4e588787c..321e6938388ef 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -12,7 +12,11 @@ <block name="require.js" class="Magento\Framework\View\Element\Template" template="Magento_Theme::page/js/require_js.phtml" /> <referenceContainer name="after.body.start"> <block class="Magento\RequireJs\Block\Html\Head\Config" name="requirejs-config"/> - <block class="Magento\Framework\View\Element\Js\Cookie" name="js_cookies" template="Magento_Theme::js/cookie.phtml"/> + <block class="Magento\Framework\View\Element\Js\Cookie" name="js_cookies" template="Magento_Theme::js/cookie.phtml"> + <arguments> + <argument name="session_config" xsi:type="object">Magento\Theme\ViewModel\Block\SessionConfig</argument> + </arguments> + </block> <block class="Magento\Theme\Block\Html\Notices" name="global_notices" template="Magento_Theme::html/notices.phtml"/> </referenceContainer> <referenceBlock name="top.links"> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/cookie.phtml b/app/code/Magento/Theme/view/frontend/templates/js/cookie.phtml index 7ecfd18d0d3b0..10cf6957fa4c3 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/cookie.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/cookie.phtml @@ -18,7 +18,7 @@ "expires": null, "path": "<?= $block->escapeJs($block->getPath()) ?>", "domain": "<?= $block->escapeJs($block->getDomain()) ?>", - "secure": false, + "secure": <?= $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false'; ?>, "lifetime": "<?= $block->escapeJs($block->getLifetime()) ?>" } } diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js b/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js index 33acba6103b10..ba6489a0291b8 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js @@ -21,7 +21,85 @@ define([ * @returns {Array} */ function compact(container) { - return container.filter(utils.isObject); + return _.values(container).filter(utils.isObject); + } + + /** + * Defines index of an item in a specified container. + * + * @param {*} item - Item whose index should be defined. + * @param {Array} container - Container upon which to perform search. + * @returns {Number} + */ + function _findIndex(item, container) { + var index = _.findKey(container, function (value) { + return value === item; + }); + + if (typeof index === 'undefined') { + index = _.findKey(container, function (value) { + return value && value.name === item; + }); + } + + return typeof index === 'undefined' ? -1 : index; + } + + /** + * Inserts specified item into container at a specified position. + * + * @param {*} item - Item to be inserted into container. + * @param {Array} container - Container of items. + * @param {*} [position=-1] - Position at which item should be inserted. + * Position can represent: + * - specific index in container + * - item which might already be present in container + * - structure with one of these properties: after, before + * @returns {Boolean|*} + * - true if element has changed its' position + * - false if nothing has changed + * - inserted value if it wasn't present in container + */ + function _insertAt(item, container, position) { + var currentIndex = _findIndex(item, container), + newIndex, + target; + + if (typeof position === 'undefined') { + position = -1; + } else if (typeof position === 'string') { + position = isNaN(+position) ? position : +position; + } + + newIndex = position; + + if (~currentIndex) { + target = container.splice(currentIndex, 1)[0]; + + if (typeof item === 'string') { + item = target; + } + } + + if (typeof position !== 'number') { + target = position.after || position.before || position; + + newIndex = _findIndex(target, container); + + if (~newIndex && (position.after || newIndex >= currentIndex)) { + newIndex++; + } + } + + if (newIndex < 0) { + newIndex += container.length + 1; + } + + container[newIndex] ? + container.splice(newIndex, 0, item) : + container[newIndex] = item; + + return !~currentIndex ? item : currentIndex !== newIndex; } return Element.extend({ @@ -90,8 +168,8 @@ define([ elems.map(function (item) { return item.elem ? - utils.insert(item.elem, container, item.position) : - utils.insert(item, container, position); + _insertAt(item.elem, container, item.position) : + _insertAt(item, container, position); }).forEach(function (item) { if (item === true) { update = true; @@ -257,9 +335,11 @@ define([ * @param {Object} elem - Element to insert. */ _insert: function (elem) { - var index = this._elems.indexOf(elem.name); + var index = _.findKey(this._elems, function (value) { + return value === elem.name; + }); - if (~index) { + if (typeof index !== 'undefined') { this._elems[index] = elem; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php new file mode 100644 index 0000000000000..4acc2d21cde02 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogGraphQl; + +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test class to verify product search, used for GraphQL resolver + * for configurable product returns only visible products. + */ +class ProductSearchTest extends GraphQlAbstract +{ + /** + * @var ObjectManager|null + */ + private $objectManager; + + /** + * @var GetCustomerAuthenticationHeader + */ + private $getCustomerAuthenticationHeader; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->getCustomerAuthenticationHeader = $this->objectManager->get(GetCustomerAuthenticationHeader::class); + } + + /** + * Test for checking if graphQL query fpr configurable product returns + * expected visible items + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php + */ + public function testCheckIfConfigurableProductVisibilityReturnsExpectedItem(): void + { + $productName = 'Configurable Product'; + $productSku = 'configurable'; + $query = $this->getProductSearchQuery($productName, $productSku); + + $response = $this->graphQlQuery($query); + + $this->assertNotEmpty($response['products']); + $this->assertEquals(1, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['items']); + $this->assertEquals($productName, $response['products']['items'][0]['name']); + $this->assertEquals($productSku, $response['products']['items'][0]['sku']); + } + + /** + * Get a query which user filter for product sku and search by product name + * + * @param string $productName + * @param string $productSku + * @return string + */ + private function getProductSearchQuery(string $productName, string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}, search: "$productName", sort: {}, pageSize: 200, currentPage: 1) { + total_count + page_info { + total_pages + current_page + page_size + } + items { + name + sku + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php index e8d789f6d35e4..62fa74ab2ffbf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductMultipleStoreViewTest.php @@ -36,6 +36,42 @@ public function testConfigurableProductAssignedToOneWebsite() self::assertContains('Option 2', $secondWebsiteVariants[1]['attributes'][0]); } + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites.php + * @dataProvider childrenAssignedToDifferentWebsitesDataProvider + * @param string $store + * @param string $childSku + * @param string $attributeLabel + */ + public function testConfigurableProductWithChildrenAssignedToDifferentWebsites( + string $store, + string $childSku, + string $attributeLabel + ) { + $headers = ['Store' => $store]; + $query = $this->getQuery('configurable'); + $response = $this->graphQlQuery($query, [], '', $headers); + self::assertCount(1, $response['products']['items']); + $product = $response['products']['items'][0]; + self::assertCount(1, $product['variants']); + $variant = $response['products']['items'][0]['variants'][0]; + self::assertEquals($childSku, $variant['product']['sku']); + self::assertCount(1, $variant['attributes']); + $attribute = $variant['attributes'][0]; + self::assertEquals($attributeLabel, $attribute['label']); + } + + /** + * @return array + */ + public function childrenAssignedToDifferentWebsitesDataProvider(): array + { + return [ + ['default', 'simple_option_2', 'Option 2'], + ['fixture_second_store', 'simple_option_1', 'Option 1'], + ]; + } + /** * @param string $sku * @return string diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/SkuTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/SkuTest.php index f557919897869..d70f1404b33d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/SkuTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/SkuTest.php @@ -49,15 +49,14 @@ public function testGenerateUniqueLongSku() ); $product = $repository->get('simple'); $product->setSku('0123456789012345678901234567890123456789012345678901234567890123'); - + $product->save(); /** @var \Magento\Catalog\Model\Product\Copier $copier */ $copier = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\Product\Copier::class ); - $copier->copy($product); + $duplicate = $copier->copy($product); $this->assertEquals('0123456789012345678901234567890123456789012345678901234567890123', $product->getSku()); - $product->getResource()->getAttribute('sku')->getBackend()->beforeSave($product); - $this->assertEquals('01234567890123456789012345678901234567890123456789012345678901-1', $product->getSku()); + $this->assertEquals('01234567890123456789012345678901234567890123456789012345678901-1', $duplicate->getSku()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php index 66e60a24e0a07..7ab9a7a50186f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceTest.php @@ -133,4 +133,20 @@ public function testGetMinPriceForComposite(): void $product = $collection->getFirstItem(); $this->assertEquals(20, $product->getData('min_price')); } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/simple_product_with_tier_price_equal_zero.php + */ + public function testGetMinPriceWhenTierPriceEqualZero() + { + $product = $this->productRepository->get('simple-2'); + $collection = Bootstrap::getObjectManager()->create(Collection::class); + $collection->addIdFilter($product->getId()); + $collection->addPriceData(0); + $collection->load(); + $product = $collection->getFirstItem(); + $this->assertEquals(0, $product->getData('tier_price')); + $this->assertEquals(0, $product->getData('min_price')); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index ceb3a2c7a94b2..d8275d78bfb79 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Copier; use Magento\Catalog\Model\Product\Visibility; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\CouldNotSaveException; @@ -19,6 +20,7 @@ use Magento\Framework\Exception\StateException; use Magento\Framework\Math\Random; use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; @@ -252,30 +254,71 @@ protected function _copyFileToBaseTmpMediaPath($sourceFile) } /** - * Test duplicate method + * Test Duplicate of product * + * Product assigned to default and custom scope is used. After duplication the copied product + * should retain store view specific data + * + * @magentoDataFixture Magento/Catalog/_files/product_multistore_different_short_description.php * @magentoAppIsolation enabled * @magentoAppArea adminhtml + * @magentoDbIsolation disabled */ public function testDuplicate() { - $this->_model = $this->productRepository->get('simple'); - - // fixture - /** @var \Magento\Catalog\Model\Product\Copier $copier */ + $fixtureProductSku = 'simple-different-short-description'; + $fixtureCustomStoreCode = 'fixturestore'; + $defaultStoreId = Store::DEFAULT_STORE_ID; + /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); + $customStoreId = $storeRepository->get($fixtureCustomStoreCode)->getId(); + $defaultScopeProduct = $this->productRepository->get($fixtureProductSku, true, $defaultStoreId); + $customScopeProduct = $this->productRepository->get($fixtureProductSku, true, $customStoreId); + /** @var Copier $copier */ $copier = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product\Copier::class + Copier::class ); - $duplicate = $copier->copy($this->_model); + $duplicate = $copier->copy($defaultScopeProduct); + + /* Fetch duplicate after cloning */ + $defaultScopeDuplicate = $this->productRepository->getById($duplicate->getId(), true, $defaultStoreId); + $customScopeDuplicate = $this->productRepository->getById($duplicate->getId(), true, $customStoreId); + try { - $this->assertNotEmpty($duplicate->getId()); - $this->assertNotEquals($duplicate->getId(), $this->_model->getId()); - $this->assertNotEquals($duplicate->getSku(), $this->_model->getSku()); + $this->assertNotEquals( + $customScopeDuplicate->getId(), $customScopeProduct->getId(), + 'Duplicate product Id should not equal to source product Id' + ); + $this->assertNotEquals( + $customScopeDuplicate->getSku(), $customScopeProduct->getSku(), + 'Duplicate product SKU should not equal to source product SKU' + ); + $this->assertNotEquals( + $customScopeDuplicate->getShortDescription(), $defaultScopeDuplicate->getShortDescription(), + 'Short description of the duplicated product on custom scope should not equal to ' . + 'duplicate product description on default scope' + ); + $this->assertEquals( + $customScopeProduct->getShortDescription(), $customScopeDuplicate->getShortDescription(), + 'Short description of the duplicated product on custom scope should equal to ' . + 'source product description on custom scope' + ); + $this->assertEquals( + $customScopeProduct->getStoreId(), $customScopeDuplicate->getStoreId(), + 'Store Id of the duplicated product on custom scope should equal to ' . + 'store Id of source product on custom scope' + ); $this->assertEquals( - Status::STATUS_DISABLED, - $duplicate->getStatus() + $defaultScopeProduct->getStoreId(), $defaultScopeDuplicate->getStoreId(), + 'Store Id of the duplicated product on default scope should equal to ' . + 'store Id of source product on default scope' ); - $this->assertEquals(\Magento\Store\Model\Store::DEFAULT_STORE_ID, $duplicate->getStoreId()); + + $this->assertEquals( + Status::STATUS_DISABLED, $defaultScopeDuplicate->getStatus(), + 'Duplicate should be disabled' + ); + $this->_undo($duplicate); } catch (\Exception $e) { $this->_undo($duplicate); @@ -293,9 +336,9 @@ public function testDuplicateSkuGeneration() $this->_model = $this->productRepository->get('simple'); $this->assertEquals('simple', $this->_model->getSku()); - /** @var \Magento\Catalog\Model\Product\Copier $copier */ + /** @var Copier $copier */ $copier = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product\Copier::class + Copier::class ); $duplicate = $copier->copy($this->_model); $this->assertEquals('simple-5', $duplicate->getSku()); @@ -311,7 +354,7 @@ protected function _undo($duplicate) \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class )->getStore()->setId( - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ); $duplicate->delete(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php index 552dd3fbbfdd5..885a7dff06633 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php @@ -206,26 +206,23 @@ public function testGetProductsWithTierPrice() /** * Test addAttributeToSort() with attribute 'is_saleable' works properly on frontend. * - * @dataProvider addAttributeToSortDataProvider + * @dataProvider addIsSaleableAttributeToSortDataProvider * @magentoDataFixture Magento/Catalog/_files/multiple_products_with_non_saleable_product.php * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 * @magentoAppIsolation enabled * @magentoAppArea frontend */ - public function testAddAttributeToSort(string $productSku, string $order) + public function testAddIsSaleableAttributeToSort(string $productSku, string $order) { - /** @var Collection $productCollection */ $this->collection->addAttributeToSort('is_saleable', $order); - self::assertEquals(2, $this->collection->count()); - self::assertSame($productSku, $this->collection->getFirstItem()->getSku()); + $this->assertEquals(2, $this->collection->count()); + $this->assertEquals($productSku, $this->collection->getFirstItem()->getSku()); } /** - * Provide test data for testAddAttributeToSort(). - * * @return array */ - public function addAttributeToSortDataProvider() + public function addIsSaleableAttributeToSortDataProvider(): array { return [ [ @@ -239,6 +236,42 @@ public function addAttributeToSortDataProvider() ]; } + /** + * Test addAttributeToSort() with attribute 'price' works properly on frontend. + * + * @dataProvider addPriceAttributeToSortDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/simple_product_with_tier_price_equal_zero.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + * @magentoAppArea frontend + */ + public function testAddPriceAttributeToSort(string $productSku, string $order) + { + $this->processor->getIndexer()->reindexAll(); + $this->collection->setStoreId(1); + $this->collection->addAttributeToSort('price', $order); + $this->assertEquals(2, $this->collection->count()); + $this->assertEquals($productSku, $this->collection->getFirstItem()->getSku()); + } + + /** + * @return array + */ + public function addPriceAttributeToSortDataProvider(): array + { + return [ + [ + 'product_sku' => 'simple', + 'order' => Collection::SORT_ORDER_DESC, + ], + [ + 'product_sku' => 'simple-2', + 'order' => Collection::SORT_ORDER_ASC, + ] + ]; + } + /** * Checks a case if table for join specified as an array. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_price_equal_zero.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_price_equal_zero.php new file mode 100644 index 0000000000000..f287223954c1b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_price_equal_zero.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductTierPriceExtension; +use Magento\Catalog\Api\Data\ProductTierPriceInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_without_custom_options.php'); + +$objectManager = Bootstrap::getObjectManager(); + +$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class) + ->get('admin'); +$tierPriceExtensionAttributes = $objectManager->create(ProductTierPriceExtension::class) + ->setWebsiteId($adminWebsite->getId()); +$tierPrices = []; +$tierPrice = $objectManager->create(ProductTierPriceInterface::class) + ->setCustomerGroupId(0) + ->setQty(1) + ->setValue(0) + ->setExtensionAttributes($tierPriceExtensionAttributes); +$tierPrices[] = $tierPrice; + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple-2', false, null, true); +$product->setTierPrices($tierPrices); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_price_equal_zero_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_price_equal_zero_rollback.php new file mode 100644 index 0000000000000..c98913e63d367 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_price_equal_zero_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_without_custom_options_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductMultipleStoresTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductMultipleStoresTest.php index 892941162f588..17ca3d5b5b0ca 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductMultipleStoresTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductMultipleStoresTest.php @@ -16,7 +16,7 @@ * Integration test for \Magento\CatalogImportExport\Model\Import\Product class. * * @magentoAppIsolation enabled - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea adminhtml * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php @@ -27,7 +27,6 @@ class ProductMultipleStoresTest extends ProductTestBase /** * @magentoDataFixture Magento/Store/_files/website.php * @magentoDataFixture Magento/Store/_files/core_fixturestore.php - * @magentoDbIsolation disabled */ public function testProductWithMultipleStoresInDifferentBunches() { @@ -124,6 +123,7 @@ function (ProductInterface $item) { * Test import product into multistore system when media is disabled. * * @magentoDataFixture Magento/CatalogImportExport/Model/Import/_files/custom_category_store_media_disabled.php + * @magentoDbIsolation enabled * @return void */ public function testProductsWithMultipleStoresWhenMediaIsDisabled(): void @@ -165,7 +165,6 @@ function ($value) { * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDataFixture Magento/Catalog/Model/Layer/Filter/_files/attribute_with_option.php * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_attribute.php - * @magentoDbIsolation disabled */ public function testProductsWithMultipleStores() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php index b40c5a2a476b2..b3ce40437c53c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php @@ -16,6 +16,7 @@ * Integration test for \Magento\CatalogImportExport\Model\Import\Product class. * * @magentoAppArea adminhtml + * @magentoDbIsolation disabled * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php */ @@ -126,6 +127,12 @@ public function testSaveCustomOptions(string $importFile, string $sku, int $expe $customOptionValues = $this->getCustomOptionValues($sku); $this->createImportModel($pathToFile)->importData(); $this->assertEquals($customOptionValues, $this->getCustomOptionValues($sku)); + + // Cleanup imported products + try { + $this->productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductUrlKeyTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductUrlKeyTest.php index b050368b76239..a69603aca1e74 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductUrlKeyTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductUrlKeyTest.php @@ -18,6 +18,7 @@ /** * Integration test for \Magento\CatalogImportExport\Model\Import\Product class. * + * @magentoDbIsolation disabled * @magentoAppArea adminhtml * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites.php new file mode 100644 index 0000000000000..b13f29614a5ec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductExtensionFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductFactory; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $productAttributeRepository */ +$productAttributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +$attribute = $productAttributeRepository->get('test_configurable'); +$options = $attribute->getOptions(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); +$secondWebsite = $websiteRepository->get('test'); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +$attributeValues = []; +$associatedProductIds = []; +$rootCategoryId = $baseWebsite->getDefaultStore()->getRootCategoryId(); +array_shift($options); + +foreach ($options as $key => $option) { + $product = $productFactory->create(); + $product->setTypeId(ProductType::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$key % 2 ? $baseWebsite->getId() : $secondWebsite->getId()]) + ->setName('Configurable Option ' . $option->getLabel()) + ->setSku(strtolower(str_replace(' ', '_', 'simple ' . $option->getLabel()))) + ->setPrice(150) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$rootCategoryId]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$product = $productFactory->create(); +/** @var ProductExtensionFactory $extensionAttributesFactory */ +$extensionAttributesFactory = $objectManager->get(ProductExtensionFactory::class); +$extensionConfigurableAttributes = $product->getExtensionAttributes() ?: $extensionAttributesFactory->create(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$rootCategoryId]) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites_rollback.php new file mode 100644 index 0000000000000..2633abdf167f5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_children_on_different_websites_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\ConfigurableProduct\Model\DeleteConfigurableProduct; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DeleteConfigurableProduct $deleteConfigurableProduct */ +$deleteConfigurableProduct = $objectManager->get(DeleteConfigurableProduct::class); +$deleteConfigurableProduct->execute('configurable'); + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php index 6df4d8fbb2d92..e344b61b3826f 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php @@ -174,17 +174,30 @@ public function testSortCaseSensitive(): void * * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest_configurable * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoDataFixture Magento/Catalog/_files/products.php + * @dataProvider searchSpecificProductDataProvider + * @param string $searchName + * @param string $sku + * @param int $expectedCount */ - public function testSearchSpecificProduct() + public function testSearchSpecificProduct(string $searchName, string $sku, int $expectedCount) { $this->reindexAll(); - $result = $this->searchByName('12345'); - self::assertCount(1, $result); + $result = $this->searchByName($searchName); + self::assertCount($expectedCount, $result); - $specificProduct = $this->productRepository->get('configurable_12345'); + $specificProduct = $this->productRepository->get($sku); self::assertEquals($specificProduct->getId(), $result[0]['_id']); } + public function searchSpecificProductDataProvider(): array + { + return [ + 'search by numeric name' => ['12345', 'configurable_12345', 1], + 'search by name with diacritics' => ['Cùstöm Dèsign', 'custom-design-simple-product', 1], + ]; + } + /** * @param string $text * @return array diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/DataConverter/DataConverterTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/DataConverter/DataConverterTest.php index 9ffdd6993644f..5f64b81802656 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/DataConverter/DataConverterTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/DataConverter/DataConverterTest.php @@ -3,9 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\DB\DataConverter; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\DB\FieldDataConversionException; use Magento\Framework\DB\FieldDataConverter; use Magento\Framework\DB\Select; use Magento\Framework\DB\Select\QueryModifierInterface; @@ -14,11 +19,13 @@ use Magento\Framework\DB\Query\Generator; use Magento\Framework\DB\Query\BatchIterator; use Magento\Framework\ObjectManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; -class DataConverterTest extends \PHPUnit\Framework\TestCase +class DataConverterTest extends TestCase { /** - * @var InQueryModifier|\PHPUnit\Framework\MockObject\MockObject + * @var InQueryModifier|MockObject */ private $queryModifierMock; @@ -28,22 +35,22 @@ class DataConverterTest extends \PHPUnit\Framework\TestCase private $dataConverter; /** - * @var BatchIterator|\PHPUnit\Framework\MockObject\MockObject + * @var BatchIterator|MockObject */ private $iteratorMock; /** - * @var Generator|\PHPUnit\Framework\MockObject\MockObject + * @var Generator|MockObject */ private $queryGeneratorMock; /** - * @var Select|\PHPUnit\Framework\MockObject\MockObject + * @var Select|MockObject */ private $selectByRangeMock; /** - * @var Mysql|\PHPUnit\Framework\MockObject\MockObject + * @var Mysql|MockObject */ private $adapterMock; @@ -114,18 +121,22 @@ function () use (&$iterationComplete) { $this->adapterMock = $this->getMockBuilder(Mysql::class) ->disableOriginalConstructor() - ->setMethods(['fetchPairs', 'quoteInto', 'update']) + ->setMethods(['fetchPairs', 'fetchAll', 'quoteInto', 'update', 'prepareSqlCondition']) ->getMock(); $this->adapterMock->expects($this->any()) ->method('quoteInto') ->willReturn('field=value'); + $batchIteratorFactory = $this->createMock(\Magento\Framework\DB\Query\BatchRangeIteratorFactory::class); + $batchIteratorFactory->method('create')->willReturn($this->iteratorMock); + $this->fieldDataConverter = $this->objectManager->create( FieldDataConverter::class, [ 'queryGenerator' => $this->queryGeneratorMock, - 'dataConverter' => $this->dataConverter + 'dataConverter' => $this->dataConverter, + 'batchIteratorFactory' => $batchIteratorFactory, ] ); } @@ -136,7 +147,7 @@ function () use (&$iterationComplete) { */ public function testDataConvertErrorReporting() { - $this->expectException(\Magento\Framework\DB\FieldDataConversionException::class); + $this->expectException(FieldDataConversionException::class); $this->expectExceptionMessage('Error converting field `value` in table `table` where `id`=2 using'); $rows = [ @@ -179,4 +190,158 @@ public function testAlreadyConvertedDataSkipped() $this->fieldDataConverter->convert($this->adapterMock, 'table', 'id', 'value', $this->queryModifierMock); } + + public function testAlreadyConvertedDataSkippedWithCompositeIdentifier(): void + { + $rows = [ + [ + 'key_one' => 1, + 'key_two' => 1, + 'value' => '[]', + ], + [ + 'key_one' => 1, + 'key_two' => 2, + 'value' => '{}', + ], + [ + 'key_one' => 3, + 'key_two' => 3, + 'value' => 'N;', + ], + [ + 'key_one' => 4, + 'key_two' => 1, + 'value' => '{"valid": "json value"}', + ] + ]; + + $this->adapterMock->expects($this->any()) + ->method('prepareSqlCondition') + ->willReturnCallback( + function ($column, $value) { + return "$column = $value"; + } + ); + + $this->adapterMock->expects($this->any()) + ->method('fetchAll') + ->with($this->selectByRangeMock) + ->willReturn($rows); + + $this->adapterMock->expects($this->once()) + ->method('update') + ->with('table', ['value' => 'null'], 'key_one = 3 AND key_two = 3'); + + $this->fieldDataConverter->convert($this->adapterMock, 'table', 'id1,id2', 'value', $this->queryModifierMock); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture createFixtureTable + */ + public function testTableWithCompositeIdentifier(): void + { + $resource = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $tableName = 'test_fixture_table'; + $keyOneValues = range(1, 9); + $keyTwoValues = [3, 6, 9]; + $records = []; + foreach ($keyOneValues as $keyOneValue) { + foreach (array_slice($keyTwoValues, 0, rand(1, 3)) as $keyTwoValue) { + $records[] = [ + 'key_one' => $keyOneValue, + 'key_two' => $keyTwoValue, + // phpcs:ignore + 'value' => serialize(['key_one' => $keyOneValue, 'key_two' => $keyTwoValue]), + ]; + } + } + // phpcs:ignore + $repeatedVal = serialize([]); + $records[] = [ + 'key_one' => 10, + 'key_two' => 3, + 'value' => $repeatedVal, + ]; + $records[] = [ + 'key_one' => 10, + 'key_two' => 6, + 'value' => $repeatedVal, + ]; + $records[] = [ + 'key_one' => 11, + 'key_two' => 6, + 'value' => $repeatedVal, + ]; + + $resource->getConnection()->insertMultiple($tableName, $records); + + $expected = []; + + foreach ($records as $record) { + $record['value'] = $this->dataConverter->convert($record['value']); + $expected[] = $record; + } + + $batchSize = 5; + $fieldDataConverter = $this->objectManager->create( + FieldDataConverter::class, + [ + 'dataConverter' => $this->dataConverter, + 'envBatchSize' => $batchSize + ] + ); + $fieldDataConverter->convert($resource->getConnection(), $tableName, 'key_one,key_two', 'value'); + $actual = $resource->getConnection()->fetchAll( + $resource->getConnection()->select()->from($tableName) + ); + $this->assertEquals($expected, $actual, json_encode($records)); + } + + public static function createFixtureTable(): void + { + $resource = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $tableName = 'test_fixture_table'; + $table = $resource->getConnection() + ->newTable( + $tableName + ) + ->addColumn( + 'key_one', + Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false] + ) + ->addColumn( + 'key_two', + Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false] + ) + ->addColumn( + 'value', + Table::TYPE_TEXT, + null, + ['nullable' => true] + ) + ->addIndex( + $tableName . '_index_key_one_key_two', + [ + 'key_one', + 'key_two', + ], + [ + 'type' => AdapterInterface::INDEX_TYPE_PRIMARY + ] + ); + $resource->getConnection()->createTable($table); + } + + public static function createFixtureTableRollback(): void + { + $resource = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $tableName = 'test_fixture_table'; + $resource->getConnection()->dropTable($tableName); + } } diff --git a/lib/internal/Magento/Framework/Amqp/Queue.php b/lib/internal/Magento/Framework/Amqp/Queue.php index 84689c294af0c..7d950f70e557e 100644 --- a/lib/internal/Magento/Framework/Amqp/Queue.php +++ b/lib/internal/Magento/Framework/Amqp/Queue.php @@ -14,8 +14,6 @@ use Psr\Log\LoggerInterface; /** - * Class Queue - * * @api * @since 103.0.0 */ @@ -41,6 +39,13 @@ class Queue implements QueueInterface */ private $logger; + /** + * The prefetch value is used to specify how many messages that are being sent to the consumer at the same time. + * @see https://www.rabbitmq.com/consumer-prefetch.html + * @var int + */ + private $prefetchCount; + /** * Initialize dependencies. * @@ -48,17 +53,20 @@ class Queue implements QueueInterface * @param EnvelopeFactory $envelopeFactory * @param string $queueName * @param LoggerInterface $logger + * @param int $prefetchCount */ public function __construct( Config $amqpConfig, EnvelopeFactory $envelopeFactory, $queueName, - LoggerInterface $logger + LoggerInterface $logger, + $prefetchCount = 100 ) { $this->amqpConfig = $amqpConfig; $this->queueName = $queueName; $this->envelopeFactory = $envelopeFactory; $this->logger = $logger; + $this->prefetchCount = (int)$prefetchCount; } /** @@ -144,6 +152,7 @@ public function subscribe($callback) $channel = $this->amqpConfig->getChannel(); // @codingStandardsIgnoreStart + $channel->basic_qos(0, $this->prefetchCount, false); $channel->basic_consume($this->queueName, '', false, false, false, false, $callbackConverter); // @codingStandardsIgnoreEnd while (count($channel->callbacks)) { diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/QueueTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/QueueTest.php new file mode 100644 index 0000000000000..b4e0b6c1fa53c --- /dev/null +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/QueueTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Amqp\Test\Unit; + +use Magento\Framework\Amqp\Config; +use Magento\Framework\Amqp\Queue; +use Magento\Framework\MessageQueue\EnvelopeFactory; +use PhpAmqpLib\Channel\AMQPChannel; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class QueueTest extends TestCase +{ + private const PREFETCH_COUNT = 100; + /** + * @var Config|MockObject + */ + private $config; + + /** + * @var EnvelopeFactory|MockObject + */ + private $envelopeFactory; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Queue + */ + private $model; + + protected function setUp(): void + { + $this->config = $this->createMock(Config::class); + $this->envelopeFactory = $this->createMock(EnvelopeFactory::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->model = new Queue( + $this->config, + $this->envelopeFactory, + 'testQueue', + $this->logger, + self::PREFETCH_COUNT + ); + } + + /** + * Test verifies that prefetch value is used to specify how many messages + * are being sent to the consumer at the same time. + */ + public function testSubscribe() + { + $callback = function () { + }; + $amqpChannel = $this->createMock(AMQPChannel::class); + $amqpChannel->expects($this->once()) + ->method('basic_qos') + ->with(0, self::PREFETCH_COUNT, false); + $this->config->expects($this->once()) + ->method('getChannel') + ->willReturn($amqpChannel); + + $this->model->subscribe($callback); + } +} diff --git a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php index 35f115634ccda..fb0533bdf9c49 100644 --- a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php +++ b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php @@ -68,7 +68,7 @@ protected function _set($key, $value) protected function _getDataObjectType() { $dataObjectType = ''; - $pattern = '/(?<data_object>.*?)Builder(\\Interceptor)?/'; + $pattern = '/(?<data_object>.*?)Builder(\\\\Interceptor)?/'; if (preg_match($pattern, get_class($this), $match)) { $dataObjectType = $match['data_object']; } diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/AbstractSimpleObjectBuilderTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/AbstractSimpleObjectBuilderTest.php new file mode 100644 index 0000000000000..2393c2577f42b --- /dev/null +++ b/lib/internal/Magento/Framework/Api/Test/Unit/AbstractSimpleObjectBuilderTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Api\Test\Unit; + +use Magento\Framework\Api\ObjectFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AbstractSimpleObjectBuilderTest extends TestCase +{ + /** + * @var MockObject|ObjectFactory + */ + private $objectFactoryMock; + + /** + * @var StubAbstractSimpleObjectBuilder + */ + private $stubSimpleObjectBuilder; + + protected function setUp(): void + { + $this->objectFactoryMock = $this->createMock(ObjectFactory::class); + $this->stubSimpleObjectBuilder = new StubAbstractSimpleObjectBuilder($this->objectFactoryMock); + } + + public function testCreate() + { + $stubSimpleObjectMock = $this->createMock(StubAbstractSimpleObject::class); + $this->objectFactoryMock->expects($this->once()) + ->method('create') + ->with(StubAbstractSimpleObject::class, ['data' => []]) + ->willReturn($stubSimpleObjectMock); + $object = $this->stubSimpleObjectBuilder->create(); + $this->assertInstanceOf(StubAbstractSimpleObject::class, $object); + } +} diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder.php b/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder.php new file mode 100644 index 0000000000000..a8f1984750bc3 --- /dev/null +++ b/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Api\Test\Unit; + +use Magento\Framework\Api\AbstractSimpleObjectBuilder; + +class StubAbstractSimpleObjectBuilder extends AbstractSimpleObjectBuilder +{ +} diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder/Interceptor.php b/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder/Interceptor.php new file mode 100644 index 0000000000000..974a0cfc83da2 --- /dev/null +++ b/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder/Interceptor.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Api\Test\Unit\StubAbstractSimpleObjectBuilder; + +class Interceptor extends \Magento\Framework\Api\Test\Unit\StubAbstractSimpleObjectBuilder +{ +} diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder/InterceptorTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder/InterceptorTest.php new file mode 100644 index 0000000000000..21c9df40f437c --- /dev/null +++ b/lib/internal/Magento/Framework/Api/Test/Unit/StubAbstractSimpleObjectBuilder/InterceptorTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Api\Test\Unit\StubAbstractSimpleObjectBuilder; + +use Magento\Framework\Api\ObjectFactory; +use Magento\Framework\Api\Test\Unit\StubAbstractSimpleObject; +use Magento\Framework\Api\Test\Unit\StubAbstractSimpleObjectBuilder; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class InterceptorTest extends TestCase +{ + /** + * @var MockObject|ObjectFactory + */ + private $objectFactoryMock; + + /** + * @var StubAbstractSimpleObjectBuilder + */ + private $stubSimpleObjectBuilderInterceptor; + + protected function setUp(): void + { + $this->objectFactoryMock = $this->createMock(ObjectFactory::class); + $this->stubSimpleObjectBuilderInterceptor = new Interceptor($this->objectFactoryMock); + } + + public function testCreate() + { + $stubSimpleObjectMock = $this->createMock(StubAbstractSimpleObject::class); + $this->objectFactoryMock->expects($this->once()) + ->method('create') + ->with(StubAbstractSimpleObject::class, ['data' => []]) + ->willReturn($stubSimpleObjectMock); + $object = $this->stubSimpleObjectBuilderInterceptor->create(); + $this->assertInstanceOf(StubAbstractSimpleObject::class, $object); + } +} diff --git a/lib/internal/Magento/Framework/DB/FieldDataConverter.php b/lib/internal/Magento/Framework/DB/FieldDataConverter.php index c25e7aff1762e..f60624880c919 100644 --- a/lib/internal/Magento/Framework/DB/FieldDataConverter.php +++ b/lib/internal/Magento/Framework/DB/FieldDataConverter.php @@ -5,9 +5,11 @@ */ namespace Magento\Framework\DB; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\DataConverter\DataConversionException; use Magento\Framework\DB\DataConverter\DataConverterInterface; +use Magento\Framework\DB\Query\BatchRangeIteratorFactory; use Magento\Framework\DB\Query\Generator; use Magento\Framework\DB\Select\QueryModifierInterface; @@ -46,6 +48,11 @@ class FieldDataConverter */ private $envBatchSize; + /** + * @var BatchRangeIteratorFactory + */ + private $batchIteratorFactory; + /** * Constructor * @@ -53,17 +60,21 @@ class FieldDataConverter * @param DataConverterInterface $dataConverter * @param SelectFactory $selectFactory * @param string|null $envBatchSize + * @param BatchRangeIteratorFactory|null $batchIteratorFactory */ public function __construct( Generator $queryGenerator, DataConverterInterface $dataConverter, SelectFactory $selectFactory, - $envBatchSize = null + $envBatchSize = null, + ?BatchRangeIteratorFactory $batchIteratorFactory = null ) { $this->queryGenerator = $queryGenerator; $this->dataConverter = $dataConverter; $this->selectFactory = $selectFactory; $this->envBatchSize = $envBatchSize; + $this->batchIteratorFactory = $batchIteratorFactory + ?? ObjectManager::getInstance()->get(BatchRangeIteratorFactory::class); } /** @@ -84,6 +95,31 @@ public function convert( $field, QueryModifierInterface $queryModifier = null ) { + $identifiers = explode(',', $identifier); + if (count($identifiers) > 1) { + $this->processTableWithCompositeIdentifier($connection, $table, $identifiers, $field, $queryModifier); + } else { + $this->processTableWithUniqueIdentifier($connection, $table, $identifier, $field, $queryModifier); + } + } + + /** + * Convert table (with unique identifier) field data from one representation to another + * + * @param AdapterInterface $connection + * @param string $table + * @param string $identifier + * @param string $field + * @param QueryModifierInterface|null $queryModifier + * @return void + */ + private function processTableWithUniqueIdentifier( + AdapterInterface $connection, + $table, + $identifier, + $field, + QueryModifierInterface $queryModifier = null + ): void { $select = $this->selectFactory->create($connection) ->from($table, [$identifier, $field]) ->where($field . ' IS NOT NULL'); @@ -122,6 +158,82 @@ public function convert( } } + /** + * Convert table (with composite identifier) field data from one representation to another + * + * @param AdapterInterface $connection + * @param string $table + * @param array $identifiers + * @param string $field + * @param QueryModifierInterface|null $queryModifier + * @return void + */ + private function processTableWithCompositeIdentifier( + AdapterInterface $connection, + $table, + $identifiers, + $field, + QueryModifierInterface $queryModifier = null + ): void { + $columns = $identifiers; + $columns[] = $field; + $select = $this->selectFactory->create($connection) + ->from($table, $columns) + ->where($field . ' IS NOT NULL'); + if ($queryModifier) { + $queryModifier->modify($select); + } + $iterator = $this->batchIteratorFactory->create( + [ + 'batchSize' => $this->getBatchSize(), + 'select' => $select, + 'correlationName' => $table, + 'rangeField' => $identifiers, + 'rangeFieldAlias' => '', + ] + ); + foreach ($iterator as $selectByRange) { + $rows = []; + foreach ($connection->fetchAll($selectByRange) as $row) { + $value = $row[$field]; + unset($row[$field]); + $constraints = []; + foreach ($row as $col => $val) { + $constraints[] = $connection->prepareSqlCondition($col, $val); + } + $rows[implode(' AND ', $constraints)] = $value; + } + $uniqueFieldDataArray = array_unique($rows); + foreach ($uniqueFieldDataArray as $uniqueFieldData) { + $constraints = array_keys($rows, $uniqueFieldData); + try { + $convertedValue = $this->dataConverter->convert($uniqueFieldData); + if ($uniqueFieldData === $convertedValue) { + // Skip for data rows that have been already converted + continue; + } + $bind = [$field => $convertedValue]; + foreach ($constraints as $where) { + $connection->update($table, $bind, $where); + } + + } catch (DataConversionException $e) { + throw new \Magento\Framework\DB\FieldDataConversionException( + sprintf( + \Magento\Framework\DB\FieldDataConversionException::MESSAGE_PATTERN, + $field, + $table, + implode(', ', $identifiers), + '(' . implode(') OR (', $constraints) . ')', + get_class($this->dataConverter), + $e->getMessage() + ) + ); + } + } + } + } + /** * Get batch size from environment variable or default * diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 5e0bf593fef49..067e2611b406c 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -803,7 +803,8 @@ public static function getNewFileName($destinationFile) $fileInfo = pathinfo($destinationFile); $index = 1; while ($fileExists($fileInfo['dirname'] . '/' . $fileInfo['basename'])) { - $fileInfo['basename'] = $fileInfo['filename'] . '_' . $index++ . '.' . $fileInfo['extension']; + $fileInfo['basename'] = $fileInfo['filename'] . '_' . ($index++); + $fileInfo['basename'] .= isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : ''; } return $fileInfo['basename']; diff --git a/pub/index.php b/pub/index.php index 9e91f3bfa5488..2cde8b91aa5a2 100644 --- a/pub/index.php +++ b/pub/index.php @@ -20,6 +20,7 @@ <p>{$e->getMessage()}</p> </div> HTML; + http_response_code(500); exit(1); }