diff --git a/.editorconfig b/.editorconfig index 37cfad78c270b..1a9acd92fc0fc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,9 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.{yml,yaml,json}] +indent_size = 2 + +[{composer, auth}.json] +indent_size = 4 diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml index e3b2ea7e24c83..53a73446a29d1 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml @@ -15,5 +15,7 @@ + + diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php index e7fdf6f9e8132..3c235f03ef929 100644 --- a/app/code/Magento/Analytics/Model/ReportUrlProvider.php +++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php @@ -47,6 +47,13 @@ class ReportUrlProvider */ private $urlReportConfigPath = 'analytics/url/report'; + /** + * Path to Advanced Reporting documentation URL. + * + * @var string + */ + private $urlReportDocConfigPath = 'analytics/url/documentation'; + /** * @param AnalyticsToken $analyticsToken * @param OTPRequest $otpRequest @@ -80,13 +87,15 @@ public function getUrl() )); } - $url = $this->config->getValue($this->urlReportConfigPath); if ($this->analyticsToken->isTokenExist()) { + $url = $this->config->getValue($this->urlReportConfigPath); $otp = $this->otpRequest->call(); if ($otp) { $query = http_build_query(['otp' => $otp], '', '&'); $url .= '?' . $query; } + } else { + $url = $this->config->getValue($this->urlReportDocConfigPath); } return $url; diff --git a/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml index 51d77228c8dcf..ac4fca843a36b 100644 --- a/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml +++ b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml @@ -15,6 +15,6 @@ - + diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php index 60dcfde64f16d..fd13028b92d90 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php @@ -43,20 +43,10 @@ class ReportUrlProviderTest extends TestCase */ private $flagManagerMock; - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; - /** * @var ReportUrlProvider */ - private $reportUrlProvider; - - /** - * @var string - */ - private $urlReportConfigPath = 'path/url/report'; + private $model; /** * @return void @@ -71,16 +61,15 @@ protected function setUp(): void $this->flagManagerMock = $this->createMock(FlagManager::class); - $this->objectManagerHelper = new ObjectManagerHelper($this); + $objectManagerHelper = new ObjectManagerHelper($this); - $this->reportUrlProvider = $this->objectManagerHelper->getObject( + $this->model = $objectManagerHelper->getObject( ReportUrlProvider::class, [ 'config' => $this->configMock, 'analyticsToken' => $this->analyticsTokenMock, 'otpRequest' => $this->otpRequestMock, 'flagManager' => $this->flagManagerMock, - 'urlReportConfigPath' => $this->urlReportConfigPath, ] ); } @@ -88,10 +77,12 @@ protected function setUp(): void /** * @param bool $isTokenExist * @param string|null $otp If null OTP was not received. + * @param string $configPath + * @return void * * @dataProvider getUrlDataProvider */ - public function testGetUrl($isTokenExist, $otp) + public function testGetUrl(bool $isTokenExist, ?string $otp, string $configPath): void { $reportUrl = 'https://example.com/report'; $url = ''; @@ -99,7 +90,7 @@ public function testGetUrl($isTokenExist, $otp) $this->configMock ->expects($this->once()) ->method('getValue') - ->with($this->urlReportConfigPath) + ->with($configPath) ->willReturn($reportUrl); $this->analyticsTokenMock ->expects($this->once()) @@ -114,18 +105,19 @@ public function testGetUrl($isTokenExist, $otp) if ($isTokenExist && $otp) { $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&'); } - $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl()); + + $this->assertSame($url ?: $reportUrl, $this->model->getUrl()); } /** * @return array */ - public function getUrlDataProvider() + public function getUrlDataProvider(): array { return [ - 'TokenDoesNotExist' => [false, null], - 'TokenExistAndOtpEmpty' => [true, null], - 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'], + 'TokenDoesNotExist' => [false, null, 'analytics/url/documentation'], + 'TokenExistAndOtpEmpty' => [true, null, 'analytics/url/report'], + 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab', 'analytics/url/report'], ]; } @@ -140,6 +132,6 @@ public function testGetUrlWhenSubscriptionUpdateRunning() ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) ->willReturn('http://store.com'); $this->expectException(SubscriptionUpdateException::class); - $this->reportUrlProvider->getUrl(); + $this->model->getUrl(); } } diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml index b6194ba12993f..fed3dd0155c87 100644 --- a/app/code/Magento/Analytics/etc/config.xml +++ b/app/code/Magento/Analytics/etc/config.xml @@ -15,6 +15,7 @@ https://advancedreporting.rjmetrics.com/otp https://advancedreporting.rjmetrics.com/report https://advancedreporting.rjmetrics.com/report + https://docs.magento.com/user-guide/reports/advanced-reporting.html Magento Analytics user diff --git a/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailsModalSection.xml b/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailsModalSection.xml new file mode 100644 index 0000000000000..0ef6a8981172b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailsModalSection.xml @@ -0,0 +1,16 @@ + + + + +
+ + + +
+
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml index 0d629e31d6d91..5da9e33dfee36 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/container.phtml new file mode 100644 index 0000000000000..6da55e4f8f8b1 --- /dev/null +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/container.phtml @@ -0,0 +1,7 @@ + +getChildHtml(); ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml b/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml index b259e3280bfd5..d69196a61c59d 100644 --- a/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml +++ b/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml @@ -58,7 +58,7 @@ status - componentType = column, index = ${ $.index }:visible + ns = ${ $.ns }, index = ${ $.index }:visible diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php index 6ab039aa27849..efb7d6dbbeff3 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php @@ -42,6 +42,8 @@ public function __construct( } /** + * Construct block + * * @return void */ protected function _construct() @@ -51,6 +53,14 @@ protected function _construct() parent::_construct(); + $this->buttonList->update('save', 'label', __('Save Attribute')); + $this->buttonList->update('save', 'class', 'save primary'); + $this->buttonList->update( + 'save', + 'data_attribute', + ['mage-init' => ['button' => ['event' => 'save', 'target' => '#edit_form']]] + ); + if ($this->getRequest()->getParam('popup')) { $this->buttonList->remove('back'); if ($this->getRequest()->getParam('product_tab') != 'variations') { @@ -64,6 +74,8 @@ protected function _construct() 100 ); } + $this->buttonList->update('reset', 'level', 10); + $this->buttonList->update('save', 'class', 'save action-secondary'); } else { $this->addButton( 'save_and_edit_button', @@ -79,14 +91,6 @@ protected function _construct() ); } - $this->buttonList->update('save', 'label', __('Save Attribute')); - $this->buttonList->update('save', 'class', 'save primary'); - $this->buttonList->update( - 'save', - 'data_attribute', - ['mage-init' => ['button' => ['event' => 'save', 'target' => '#edit_form']]] - ); - $entityAttribute = $this->_coreRegistry->registry('entity_attribute'); if (!$entityAttribute || !$entityAttribute->getIsUserDefined()) { $this->buttonList->remove('delete'); @@ -96,14 +100,14 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function addButton($buttonId, $data, $level = 0, $sortOrder = 0, $region = 'toolbar') { if ($this->getRequest()->getParam('popup')) { $region = 'header'; } - parent::addButton($buttonId, $data, $level, $sortOrder, $region); + return parent::addButton($buttonId, $data, $level, $sortOrder, $region); } /** diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 6cec9bf3ef88a..b181a5392905b 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -367,7 +367,7 @@ public function getIdentities() $identities[] = $item->getIdentities(); } } - $identities = array_merge(...$identities); + $identities = array_merge([], ...$identities); return $identities; } diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php index 387fac770c5bc..42f610f89768d 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php @@ -143,11 +143,11 @@ public function getItems() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getItems() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } /** diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php index ac66392efe5dc..adcb1b5666560 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php @@ -267,10 +267,10 @@ public function getItemLimit($type = '') */ public function getIdentities() { - $identities = array_map(function (DataObject $item) { - return $item->getIdentities(); - }, $this->getItems()) ?: [[]]; - - return array_merge(...$identities); + $identities = []; + foreach ($this->getItems() as $item) { + $identities[] = $item->getIdentities(); + } + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index 1dcbf60db15c3..de92546a8dd88 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -12,7 +12,10 @@ namespace Magento\Catalog\Block\Product\View\Options; +use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface; +use Magento\Framework\App\ObjectManager; /** * Product options section abstract block. @@ -47,20 +50,29 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template */ protected $_catalogHelper; + /** + * @var CalculateCustomOptionCatalogRule + */ + private $calculateCustomOptionCatalogRule; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper * @param \Magento\Catalog\Helper\Data $catalogData * @param array $data + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Pricing\Helper\Data $pricingHelper, \Magento\Catalog\Helper\Data $catalogData, - array $data = [] + array $data = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->pricingHelper = $pricingHelper; $this->_catalogHelper = $catalogData; + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule + ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct($context, $data); } @@ -162,6 +174,19 @@ protected function _formatPrice($value, $flag = true) $priceStr = $sign; $customOptionPrice = $this->getProduct()->getPriceInfo()->getPrice('custom_option_price'); + $isPercent = (bool) $value['is_percent']; + + if (!$isPercent) { + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( + $this->getProduct(), + (float)$value['pricing_value'], + $isPercent + ); + if ($catalogPriceValue !== null) { + $value['pricing_value'] = $catalogPriceValue; + } + } + $context = [CustomOptionPriceInterface::CONFIGURATION_OPTION_FLAG => true]; $optionAmount = $customOptionPrice->getCustomAmount($value['pricing_value'], null, $context); $priceStr .= $this->getLayout()->getBlock('product.price.render.default')->renderAmount( diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index d948daed1c7d9..f0e0ff73b838c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; use Magento\Backend\Helper\Js; +use Magento\Catalog\Api\Data\CategoryLinkInterfaceFactory; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory; use Magento\Catalog\Api\Data\ProductLinkTypeInterface; @@ -115,6 +116,11 @@ class Helper */ private $dateTimeFilter; + /** + * @var CategoryLinkInterfaceFactory + */ + private $categoryLinkFactory; + /** * Constructor * @@ -132,6 +138,7 @@ class Helper * @param FormatInterface|null $localeFormat * @param ProductAuthorization|null $productAuthorization * @param DateTimeFilter|null $dateTimeFilter + * @param CategoryLinkInterfaceFactory|null $categoryLinkFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -148,7 +155,8 @@ public function __construct( AttributeFilter $attributeFilter = null, FormatInterface $localeFormat = null, ?ProductAuthorization $productAuthorization = null, - ?DateTimeFilter $dateTimeFilter = null + ?DateTimeFilter $dateTimeFilter = null, + ?CategoryLinkInterfaceFactory $categoryLinkFactory = null ) { $this->request = $request; $this->storeManager = $storeManager; @@ -166,6 +174,7 @@ public function __construct( $this->localeFormat = $localeFormat ?: $objectManager->get(FormatInterface::class); $this->productAuthorization = $productAuthorization ?? $objectManager->get(ProductAuthorization::class); $this->dateTimeFilter = $dateTimeFilter ?? $objectManager->get(DateTimeFilter::class); + $this->categoryLinkFactory = $categoryLinkFactory ?? $objectManager->get(CategoryLinkInterfaceFactory::class); } /** @@ -238,6 +247,7 @@ public function initializeFromData(Product $product, array $productData) $product = $this->setProductLinks($product); $product = $this->fillProductOptions($product, $productOptions); + $this->setCategoryLinks($product); $product->setCanSaveCustomOptions( !empty($productData['affect_product_custom_options']) && !$product->getOptionsReadonly() @@ -484,4 +494,30 @@ function ($valueData) { return $product->setOptions($customOptions); } + + /** + * Set category links based on initialized category ids + * + * @param Product $product + */ + private function setCategoryLinks(Product $product): void + { + $extensionAttributes = $product->getExtensionAttributes(); + $categoryLinks = []; + foreach ((array) $extensionAttributes->getCategoryLinks() as $categoryLink) { + $categoryLinks[$categoryLink->getCategoryId()] = $categoryLink; + } + + $newCategoryLinks = []; + foreach ($product->getCategoryIds() as $categoryId) { + $categoryLink = $categoryLinks[$categoryId] ?? + $this->categoryLinkFactory->create() + ->setCategoryId($categoryId) + ->setPosition(0); + $newCategoryLinks[] = $categoryLink; + } + + $extensionAttributes->setCategoryLinks(!empty($newCategoryLinks) ? $newCategoryLinks : null); + $product->setExtensionAttributes($extensionAttributes); + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index 5c3e27334cb66..97b57317851fc 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -15,7 +15,8 @@ use Magento\Framework\App\Request\DataPersistorInterface; /** - * Class Save + * Product save controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface @@ -141,10 +142,6 @@ public function execute() $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); - $this->categoryLinkManagement->assignProductToCategories( - $product->getSku(), - $product->getCategoryIds() - ); $productId = $product->getEntityId(); $productAttributeSetId = $product->getAttributeSetId(); $productTypeId = $product->getTypeId(); diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 0a562a9a80c89..f3e3caf309059 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -22,6 +22,8 @@ use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Config\DataInterfaceFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; @@ -32,7 +34,6 @@ use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Ui\DataProvider\Modifier\PoolInterface; -use Magento\Framework\AuthorizationInterface; use Magento\Ui\DataProvider\ModifierPoolDataProvider; /** @@ -153,6 +154,11 @@ class DataProvider extends ModifierPoolDataProvider */ private $categoryFactory; + /** + * @var DataInterfaceFactory + */ + private $uiConfigFactory; + /** * @var ScopeOverriddenValue */ @@ -177,6 +183,7 @@ class DataProvider extends ModifierPoolDataProvider * @var AuthorizationInterface */ private $auth; + /** * @var Image */ @@ -202,6 +209,7 @@ class DataProvider extends ModifierPoolDataProvider * @param ArrayManager|null $arrayManager * @param FileInfo|null $fileInfo * @param Image|null $categoryImage + * @param DataInterfaceFactory|null $uiConfigFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -223,7 +231,8 @@ public function __construct( ScopeOverriddenValue $scopeOverriddenValue = null, ArrayManager $arrayManager = null, FileInfo $fileInfo = null, - ?Image $categoryImage = null + ?Image $categoryImage = null, + ?DataInterfaceFactory $uiConfigFactory = null ) { $this->eavValidationRules = $eavValidationRules; $this->collection = $categoryCollectionFactory->create(); @@ -240,6 +249,10 @@ public function __construct( $this->arrayManager = $arrayManager ?: ObjectManager::getInstance()->get(ArrayManager::class); $this->fileInfo = $fileInfo ?: ObjectManager::getInstance()->get(FileInfo::class); $this->categoryImage = $categoryImage ?? ObjectManager::getInstance()->get(Image::class); + $this->uiConfigFactory = $uiConfigFactory ?? ObjectManager::getInstance()->create( + DataInterfaceFactory::class, + ['instanceName' => \Magento\Ui\Config\Data::class] + ); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); } @@ -611,7 +624,7 @@ private function convertValues($category, $categoryData): array $categoryData[$attributeCode][0]['url'] = $this->categoryImage->getUrl($category, $attributeCode); - $categoryData[$attributeCode][0]['size'] = isset($stat) ? $stat['size'] : 0; + $categoryData[$attributeCode][0]['size'] = $stat['size']; $categoryData[$attributeCode][0]['type'] = $mime; } } @@ -645,56 +658,42 @@ public function getDefaultMetaData($result) */ protected function getFieldsMap() { - return [ - 'general' => [ - 'parent', - 'path', - 'is_active', - 'include_in_menu', - 'name', - ], - 'content' => [ - 'image', - 'description', - 'landing_page', - ], - 'display_settings' => [ - 'display_mode', - 'is_anchor', - 'available_sort_by', - 'use_config.available_sort_by', - 'default_sort_by', - 'use_config.default_sort_by', - 'filter_price_range', - 'use_config.filter_price_range', - ], - 'search_engine_optimization' => [ - 'url_key', - 'url_key_create_redirect', - 'url_key_group', - 'meta_title', - 'meta_keywords', - 'meta_description', - ], - 'assign_products' => [ - ], - 'design' => [ - 'custom_use_parent_settings', - 'custom_apply_to_products', - 'custom_design', - 'page_layout', - 'custom_layout_update', - 'custom_layout_update_file' - ], - 'schedule_design_update' => [ - 'custom_design_from', - 'custom_design_to', - ], - 'category_view_optimization' => [ - ], - 'category_permissions' => [ - ], - ]; + $referenceName = 'category_form'; + $config = $this->uiConfigFactory + ->create(['componentName' => $referenceName]) + ->get($referenceName); + + if (empty($config)) { + return []; + } + + $fieldsMap = []; + + foreach ($config['children'] as $group => $node) { + // Skip disabled components (required for Commerce Edition) + if ($node['arguments']['data']['config']['componentDisabled'] ?? false) { + continue; + } + + $fields = []; + + foreach ($node['children'] as $childName => $childNode) { + if (!empty($childNode['children'])) { + // nodes need special handling + foreach (array_keys($childNode['children']) as $grandchildName) { + $fields[] = $grandchildName; + } + } else { + $fields[] = $childName; + } + } + + if (!empty($fields)) { + $fieldsMap[$group] = $fields; + } + } + + return $fieldsMap; } /** diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index b0c8d56057431..6aff6488164f9 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -5,7 +5,17 @@ */ namespace Magento\Catalog\Model; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Uploader; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Name; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\MediaStorage\Model\File\UploaderFactory; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * Catalog image uploader @@ -13,92 +23,84 @@ class ImageUploader { /** - * Core file storage database - * - * @var \Magento\MediaStorage\Helper\File\Storage\Database + * @var Database */ protected $coreFileStorageDatabase; /** - * Media directory object (writable). - * - * @var \Magento\Framework\Filesystem\Directory\WriteInterface + * @var WriteInterface */ protected $mediaDirectory; /** - * Uploader factory - * - * @var \Magento\MediaStorage\Model\File\UploaderFactory + * @var UploaderFactory */ private $uploaderFactory; /** - * Store manager - * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; /** - * Base tmp path - * * @var string */ protected $baseTmpPath; /** - * Base path - * * @var string */ protected $basePath; /** - * Allowed extensions - * * @var string */ protected $allowedExtensions; /** - * List of allowed image mime types - * * @var string[] */ private $allowedMimeTypes; /** - * ImageUploader constructor + * @var Name + */ + private $fileNameLookup; + + /** + * ImageUploader constructor. * - * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Psr\Log\LoggerInterface $logger + * @param Database $coreFileStorageDatabase + * @param Filesystem $filesystem + * @param UploaderFactory $uploaderFactory + * @param StoreManagerInterface $storeManager + * @param LoggerInterface $logger * @param string $baseTmpPath * @param string $basePath * @param string[] $allowedExtensions * @param string[] $allowedMimeTypes + * @param Name|null $fileNameLookup + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, - \Magento\Framework\Filesystem $filesystem, - \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Psr\Log\LoggerInterface $logger, + Database $coreFileStorageDatabase, + Filesystem $filesystem, + UploaderFactory $uploaderFactory, + StoreManagerInterface $storeManager, + LoggerInterface $logger, $baseTmpPath, $basePath, $allowedExtensions, - $allowedMimeTypes = [] + $allowedMimeTypes = [], + Name $fileNameLookup = null ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; - $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->uploaderFactory = $uploaderFactory; $this->storeManager = $storeManager; $this->logger = $logger; @@ -106,13 +108,13 @@ public function __construct( $this->basePath = $basePath; $this->allowedExtensions = $allowedExtensions; $this->allowedMimeTypes = $allowedMimeTypes; + $this->fileNameLookup = $fileNameLookup ?? ObjectManager::getInstance()->get(Name::class); } /** * Set base tmp path * * @param string $baseTmpPath - * * @return void */ public function setBaseTmpPath($baseTmpPath) @@ -124,7 +126,6 @@ public function setBaseTmpPath($baseTmpPath) * Set base path * * @param string $basePath - * * @return void */ public function setBasePath($basePath) @@ -136,7 +137,6 @@ public function setBasePath($basePath) * Set allowed extensions * * @param string[] $allowedExtensions - * * @return void */ public function setAllowedExtensions($allowedExtensions) @@ -179,7 +179,6 @@ public function getAllowedExtensions() * * @param string $path * @param string $imageName - * * @return string */ public function getFilePath($path, $imageName) @@ -194,7 +193,7 @@ public function getFilePath($path, $imageName) * @param bool $returnRelativePath * @return string * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function moveFileFromTmp($imageName, $returnRelativePath = false) { @@ -203,7 +202,7 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false) $baseImagePath = $this->getFilePath( $basePath, - Uploader::getNewFileName( + $this->fileNameLookup->getNewFileName( $this->mediaDirectory->getAbsolutePath( $this->getFilePath($basePath, $imageName) ) @@ -222,10 +221,7 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false) ); } catch (\Exception $e) { $this->logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( - __('Something went wrong while saving the file(s).'), - $e - ); + throw new LocalizedException(__('Something went wrong while saving the file(s).'), $e); } return $returnRelativePath ? $baseImagePath : $imageName; @@ -235,10 +231,9 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false) * Checking file for save and save it to tmp dir * * @param string $fileId - * * @return string[] * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function saveFileToTmpDir($fileId) { @@ -249,15 +244,13 @@ public function saveFileToTmpDir($fileId) $uploader->setAllowedExtensions($this->getAllowedExtensions()); $uploader->setAllowRenameFiles(true); if (!$uploader->checkMimeType($this->allowedMimeTypes)) { - throw new \Magento\Framework\Exception\LocalizedException(__('File validation failed.')); + throw new LocalizedException(__('File validation failed.')); } $result = $uploader->save($this->mediaDirectory->getAbsolutePath($baseTmpPath)); unset($result['path']); if (!$result) { - throw new \Magento\Framework\Exception\LocalizedException( - __('File can not be saved to the destination folder.') - ); + throw new LocalizedException(__('File can not be saved to the destination folder.')); } /** @@ -277,7 +270,7 @@ public function saveFileToTmpDir($fileId) $this->coreFileStorageDatabase->saveFile($relativePath); } catch (\Exception $e) { $this->logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Something went wrong while saving the file(s).'), $e ); diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php index 936e6163cbcc5..0c0c72b0322dc 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php @@ -55,7 +55,10 @@ public function afterGetTableName( string $result, $modelEntity ) { - if (!is_array($modelEntity) && $modelEntity === AbstractAction::MAIN_INDEX_TABLE) { + if (!is_array($modelEntity) && + $modelEntity === AbstractAction::MAIN_INDEX_TABLE && + $this->storeManager->getStore()->getId() + ) { $catalogCategoryProductDimension = new Dimension( \Magento\Store\Model\Store::ENTITY, $this->storeManager->getStore()->getId() diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index edd68422ec4ac..861f7c9c1c50e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -270,14 +270,14 @@ private function getCategoryIdsFromIndex(array $productIds): array ); $categoryIds[] = $storeCategories; } - $categoryIds = array_merge(...$categoryIds); + $categoryIds = array_merge([], ...$categoryIds); $parentCategories = [$categoryIds]; foreach ($categoryIds as $categoryId) { $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); $parentCategories[] = $parentIds; } - $categoryIds = array_unique(array_merge(...$parentCategories)); + $categoryIds = array_unique(array_merge([], ...$parentCategories)); return $categoryIds; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php index 99d75186eca8c..a0af09edf14c5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -261,7 +261,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $select->from( ['et' => $entityTemporaryTableName], - array_merge(...$allColumns) + array_merge([], ...$allColumns) )->joinInner( ['e' => $this->resource->getTableName('catalog_product_entity')], 'e.entity_id = et.entity_id', @@ -306,7 +306,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $allColumns[] = $columnValueNames; } } - $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge(...$allColumns), false); + $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge([], ...$allColumns), false); $this->_connection->query($sql); } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 7c463267e5a58..82d252acd9909 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductLinkRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; @@ -202,7 +203,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * Catalog product status * - * @var \Magento\Catalog\Model\Product\Attribute\Source\Status + * @var Status */ protected $_catalogProductStatus; @@ -408,7 +409,7 @@ public function __construct( \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory, \Magento\Catalog\Model\Product\OptionFactory $catalogProductOptionFactory, \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility, - \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, + Status $catalogProductStatus, \Magento\Catalog\Model\Product\Media\Config $catalogProductMediaConfig, Product\Type $catalogProductType, \Magento\Framework\Module\Manager $moduleManager, @@ -668,7 +669,7 @@ public function getTypeId() public function getStatus() { $status = $this->_getData(self::STATUS); - return $status !== null ? $status : \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED; + return $status !== null ? $status : Status::STATUS_ENABLED; } /** @@ -835,10 +836,7 @@ public function getStoreIds() $storeIds[] = $websiteStores; } } - if ($storeIds) { - $storeIds = array_merge(...$storeIds); - } - $this->setStoreIds($storeIds); + $this->setStoreIds(array_merge([], ...$storeIds)); } return $this->getData('store_ids'); } @@ -1033,7 +1031,7 @@ public function priceReindexCallback() */ public function eavReindexCallback() { - if ($this->isObjectNew() || $this->isDataChanged($this)) { + if ($this->isObjectNew() || $this->isDataChanged()) { $this->_productEavIndexerProcessor->reindexRow($this->getEntityId()); } } @@ -1103,7 +1101,7 @@ public function afterDeleteCommit() protected function _afterLoad() { if (!$this->hasData(self::STATUS)) { - $this->setData(self::STATUS, \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $this->setData(self::STATUS, Status::STATUS_ENABLED); } parent::_afterLoad(); return $this; @@ -1179,7 +1177,7 @@ public function getTierPrice($qty = null) /** * Get formatted by currency product price * - * @return array|double + * @return array|double * @since 102.0.6 */ public function getFormattedPrice() @@ -1780,7 +1778,7 @@ public function isSaleable() */ public function isInStock() { - return $this->getStatus() == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED; + return $this->getStatus() == Status::STATUS_ENABLED; } /** @@ -2341,7 +2339,7 @@ public function getProductEntitiesInfo($columns = null) */ public function isDisabled() { - return $this->getStatus() == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED; + return $this->getStatus() == Status::STATUS_DISABLED; } /** @@ -2357,6 +2355,22 @@ public function getImage() return parent::getImage(); } + /** + * Get identities for related to product categories + * + * @param array $categoryIds + * @return array + */ + private function getProductCategoryIdentities(array $categoryIds): array + { + $identities = []; + foreach ($categoryIds as $categoryId) { + $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + } + + return $identities; + } + /** * Get identities * @@ -2365,17 +2379,24 @@ public function getImage() public function getIdentities() { $identities = [self::CACHE_TAG . '_' . $this->getId()]; - if ($this->getIsChangedCategories()) { - foreach ($this->getAffectedCategoryIds() as $categoryId) { - $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + + $isStatusChanged = $this->getOrigData(self::STATUS) != $this->getData(self::STATUS) && !$this->isObjectNew(); + if ($isStatusChanged || $this->getStatus() == Status::STATUS_ENABLED) { + if ($this->getIsChangedCategories()) { + $identities = array_merge( + $identities, + $this->getProductCategoryIdentities($this->getAffectedCategoryIds()) + ); } - } - if (($this->getOrigData('status') != $this->getData('status')) || $this->isStockStatusChanged()) { - foreach ($this->getCategoryIds() as $categoryId) { - $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + if ($isStatusChanged || $this->isStockStatusChanged()) { + $identities = array_merge( + $identities, + $this->getProductCategoryIdentities($this->getCategoryIds()) + ); } } + if ($this->_appState->getAreaCode() == \Magento\Framework\App\Area::AREA_FRONTEND) { $identities[] = self::CACHE_TAG; } diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index e83982b8ce672..44d6fb04b01b0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -16,8 +16,11 @@ use Magento\Catalog\Model\Product\Option\Type\File; use Magento\Catalog\Model\Product\Option\Type\Select; use Magento\Catalog\Model\Product\Option\Type\Text; +use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractExtensibleModel; @@ -123,6 +126,11 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ private $customOptionValuesFactory; + /** + * @var CalculateCustomOptionCatalogRule + */ + private $calculateCustomOptionCatalogRule; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -138,6 +146,7 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory * @param array $optionGroups * @param array $optionTypesToGroups + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -154,14 +163,17 @@ public function __construct( array $data = [], ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null, array $optionGroups = [], - array $optionTypesToGroups = [] + array $optionTypesToGroups = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; $this->string = $string; $this->validatorPool = $validatorPool; $this->customOptionValuesFactory = $customOptionValuesFactory ?: - \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? + ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); $this->optionGroups = $optionGroups ?: [ self::OPTION_GROUP_DATE => Date::class, self::OPTION_GROUP_FILE => File::class, @@ -462,11 +474,21 @@ public function afterSave() */ public function getPrice($flag = false) { - if ($flag && $this->getPriceType() == self::$typePercent) { - $basePrice = $this->getProduct()->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getValue(); - $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100); + if ($flag && $this->getPriceType() === self::$typePercent) { + $price = $this->calculateCustomOptionCatalogRule->execute( + $this->getProduct(), + (float)$this->getData(self::KEY_PRICE), + $this->getPriceType() === Value::TYPE_PERCENT + ); + + if ($price === null) { + $basePrice = $this->getProduct()->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getValue(); + $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100); + } + return $price; } + return $this->_getData(self::KEY_PRICE); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 6ac48c565e842..8001d692c011b 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -72,6 +72,13 @@ public function validateUserValue($values) $dateValid = true; if ($this->_dateExists()) { if ($this->useCalendar()) { + /* Fixed validation if the date was not saved correctly after re-saved the order + for example: "09\/24\/2020,2020-09-24 00:00:00" */ + if (is_string($value) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4},+(\w|\W)*$/', $value)) { + $value = [ + 'date' => preg_replace('/,([^,]+),?$/', '', $value), + ]; + } $dateValid = isset($value['date']) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4}$/', $value['date']); } else { $dateValid = isset( @@ -184,8 +191,10 @@ public function prepareForCart() $date = (new \DateTime())->setTimestamp($timestamp); $result = $date->format('Y-m-d H:i:s'); + $originDate = (isset($value['date']) && $value['date'] != '') ? $value['date'] : null; + // Save date in internal format to avoid locale date bugs - $this->_setInternalInRequest($result); + $this->_setInternalInRequest($result, $originDate); return $result; } else { @@ -352,9 +361,10 @@ public function getYearEnd() * Save internal value of option in infoBuy_request * * @param string $internalValue Datetime value in internal format + * @param string|null $originDate date value in origin format * @return void */ - protected function _setInternalInRequest($internalValue) + protected function _setInternalInRequest($internalValue, $originDate = null) { $requestOptions = $this->getRequest()->getOptions(); if (!isset($requestOptions[$this->getOption()->getId()])) { @@ -364,6 +374,9 @@ protected function _setInternalInRequest($internalValue) $requestOptions[$this->getOption()->getId()] = []; } $requestOptions[$this->getOption()->getId()]['date_internal'] = $internalValue; + if ($originDate) { + $requestOptions[$this->getOption()->getId()]['date'] = $originDate; + } $this->getRequest()->setOptions($requestOptions); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index 16fdd4cdeeb1c..e819f36b5cf7d 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -13,6 +13,8 @@ use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Option\Value; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use Magento\Framework\App\ObjectManager; /** * Catalog product option default type @@ -60,21 +62,30 @@ class DefaultType extends \Magento\Framework\DataObject */ protected $_checkoutSession; + /** + * @var CalculateCustomOptionCatalogRule + */ + private $calculateCustomOptionCatalogRule; + /** * Construct * * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param array $data + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - array $data = [] + array $data = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->_checkoutSession = $checkoutSession; parent::__construct($data); $this->_scopeConfig = $scopeConfig; + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() + ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -341,7 +352,20 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$option->getPrice(), + $option->getPriceType() === Value::TYPE_PERCENT + ); + if ($catalogPriceValue !== null) { + return $catalogPriceValue; + } else { + return $this->_getChargeableOptionPrice( + $option->getPrice(), + $option->getPriceType() === Value::TYPE_PERCENT, + $basePrice + ); + } } /** diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index d2766b1bbb054..580ef7689ff4e 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -6,7 +6,11 @@ namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Catalog\Model\Product\Option\Value; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Catalog\Model\Product\Option; /** * Catalog product option select type @@ -37,6 +41,11 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType */ private $singleSelectionTypes; + /** + * @var CalculateCustomOptionCatalogRule + */ + private $calculateCustomOptionCatalogRule; + /** * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -44,6 +53,7 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType * @param \Magento\Framework\Escaper $escaper * @param array $data * @param array $singleSelectionTypes + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, @@ -51,7 +61,8 @@ public function __construct( \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\Escaper $escaper, array $data = [], - array $singleSelectionTypes = [] + array $singleSelectionTypes = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->string = $string; $this->_escaper = $escaper; @@ -61,6 +72,8 @@ public function __construct( 'drop_down' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, 'radio' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, ]; + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() + ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -248,11 +261,7 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->_getChargeableOptionPrice( - $_result->getPrice(), - $_result->getPriceType() == 'percent', - $basePrice - ); + $result += $this->getCalculatedOptionValue($option, $_result, $basePrice); } else { if ($this->getListener()) { $this->getListener()->setHasError(true)->setMessage($this->_getWrongConfigurationMessage()); @@ -263,11 +272,20 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->_getChargeableOptionPrice( - $_result->getPrice(), - $_result->getPriceType() == 'percent', - $basePrice + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$_result->getPrice(), + $_result->getPriceType() === Value::TYPE_PERCENT ); + if ($catalogPriceValue !== null) { + $result = $catalogPriceValue; + } else { + $result = $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice + ); + } } else { if ($this->getListener()) { $this->getListener()->setHasError(true)->setMessage($this->_getWrongConfigurationMessage()); @@ -329,4 +347,31 @@ protected function _isSingleSelection() { return in_array($this->getOption()->getType(), $this->singleSelectionTypes, true); } + + /** + * Returns calculated price of option + * + * @param Option $option + * @param Option\Value $result + * @param float $basePrice + * @return float + */ + protected function getCalculatedOptionValue(Option $option, Value $result, float $basePrice) : float + { + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$result->getPrice(), + $result->getPriceType() === Value::TYPE_PERCENT + ); + if ($catalogPriceValue !== null) { + $optionCalculatedValue = $catalogPriceValue; + } else { + $optionCalculatedValue = $this->_getChargeableOptionPrice( + $result->getPrice(), + $result->getPriceType() === Value::TYPE_PERCENT, + $basePrice + ); + } + return $optionCalculatedValue; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 313513a9151dc..12b418c33deec 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -10,6 +10,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use Magento\Framework\App\ObjectManager; use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; use Magento\Catalog\Pricing\Price\RegularPrice; @@ -69,6 +71,11 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu */ private $customOptionPriceCalculator; + /** + * @var CalculateCustomOptionCatalogRule + */ + private $calculateCustomOptionCatalogRule; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -77,6 +84,7 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\Model\Context $context, @@ -85,11 +93,14 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - CustomOptionPriceCalculator $customOptionPriceCalculator = null + CustomOptionPriceCalculator $customOptionPriceCalculator = null, + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->_valueCollectionFactory = $valueCollectionFactory; $this->customOptionPriceCalculator = $customOptionPriceCalculator - ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); + ?? ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule + ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct( $context, @@ -253,7 +264,16 @@ public function saveValues() public function getPrice($flag = false) { if ($flag) { - return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( + $this->getProduct(), + (float)$this->getData(self::KEY_PRICE), + $this->getPriceType() === self::TYPE_PERCENT + ); + if ($catalogPriceValue!==null) { + return $catalogPriceValue; + } else { + return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); + } } return $this->_getData(self::KEY_PRICE); } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 36ef1826462b0..7d458401c950e 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -12,9 +12,6 @@ use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; use Magento\Catalog\Model\ProductIdLocatorInterface; -/** - * Tier price storage. - */ class TierPriceStorage implements TierPriceStorageInterface { /** @@ -220,7 +217,7 @@ private function retrieveAffectedIds(array $skus): array $affectedIds[] = array_keys($productId); } - return $affectedIds ? array_unique(array_merge(...$affectedIds)) : []; + return array_unique(array_merge([], ...$affectedIds)); } /** diff --git a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php index 4bc400605a429..1d5ef722db8b1 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php +++ b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php @@ -103,7 +103,7 @@ private function extractRequestedLinkTypes(array $criteria): array if (count($linkTypesToLoad) === 1) { $linkTypesToLoad = $linkTypesToLoad[0]; } else { - $linkTypesToLoad = array_merge(...$linkTypesToLoad); + $linkTypesToLoad = array_merge([], ...$linkTypesToLoad); } $linkTypesToLoad = array_flip($linkTypesToLoad); $linkTypes = array_filter( diff --git a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php index a5e1d05409e43..db9f4de142956 100644 --- a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php +++ b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php @@ -5,13 +5,15 @@ */ namespace Magento\Catalog\Model; -use Magento\Catalog\Api\Data\ProductOptionExtensionFactory; use Magento\Catalog\Api\Data\ProductOptionInterface; use Magento\Catalog\Model\CustomOptions\CustomOption; use Magento\Catalog\Model\CustomOptions\CustomOptionFactory; use Magento\Framework\DataObject; use Magento\Framework\DataObject\Factory as DataObjectFactory; +/** + * Processor for product options + */ class ProductOptionProcessor implements ProductOptionProcessorInterface { /** @@ -88,7 +90,8 @@ public function convertToProductOption(DataObject $request) if (!empty($options) && is_array($options)) { $data = []; foreach ($options as $optionId => $optionValue) { - if (is_array($optionValue)) { + + if (is_array($optionValue) && !$this->isDateWithDateInternal($optionValue)) { $optionValue = $this->processFileOptionValue($optionValue); $optionValue = implode(',', $optionValue); } @@ -126,6 +129,8 @@ private function processFileOptionValue(array $optionValue) } /** + * Get url builder + * * @return \Magento\Catalog\Model\Product\Option\UrlBuilder * * @deprecated 101.0.0 @@ -138,4 +143,15 @@ private function getUrlBuilder() } return $this->urlBuilder; } + + /** + * Check if the option has a date_internal and date + * + * @param array $optionValue + * @return bool + */ + private function isDateWithDateInternal(array $optionValue): bool + { + return array_key_exists('date_internal', $optionValue) && array_key_exists('date', $optionValue); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php index 17ca389777c5b..c7c08bc805a1d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php @@ -33,7 +33,7 @@ public function build(int $productId, int $storeId) : array foreach ($this->linkedProductSelectBuilder as $productSelectBuilder) { $selects[] = $productSelectBuilder->build($productId, $storeId); } - $selects = array_merge(...$selects); + $selects = array_merge([], ...$selects); return $selects; } diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php new file mode 100644 index 0000000000000..1090867aa51a5 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -0,0 +1,87 @@ +priceModifier = $priceModifier; + $this->priceCurrency = $priceCurrency; + } + + /** + * Calculate prices of custom options of the product with catalog rules applied. + * + * @param Product $product + * @param float $optionPriceValue + * @param bool $isPercent + * @return float|null + */ + public function execute( + Product $product, + float $optionPriceValue, + bool $isPercent + ): ?float { + $regularPrice = (float)$product->getPriceInfo() + ->getPrice(RegularPrice::PRICE_CODE) + ->getValue(); + $catalogRulePrice = $this->priceModifier->modifyPrice( + $regularPrice, + $product + ); + // Apply catalog price rules to product options only if catalog price rules are applied to product. + if ($catalogRulePrice < $regularPrice) { + $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice); + $totalCatalogRulePrice = $this->priceModifier->modifyPrice( + $regularPrice + $optionPrice, + $product + ); + $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; + return $this->priceCurrency->convertAndRound($finalOptionPrice); + } + + return null; + } + + /** + * Calculate option price without catalog price rule discount. + * + * @param float $optionPriceValue + * @param bool $isPercent + * @param float $basePrice + * @return float + */ + private function getOptionPriceWithoutPriceRule(float $optionPriceValue, bool $isPercent, float $basePrice): float + { + return $isPercent ? $basePrice * $optionPriceValue / 100 : $optionPriceValue; + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.xml new file mode 100644 index 0000000000000..189370b03fcee --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Click on new value of selector attribute and fill the values for storefront view, and admin product edit page + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.xml new file mode 100644 index 0000000000000..d00a0a01c78b9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Assert columns label, visible, searchable and comparable for attribute on the product attribute grid + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.xml new file mode 100644 index 0000000000000..faf9d4f40648d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + Assert if product attribute present on the product Create/Edit page + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml new file mode 100644 index 0000000000000..90cc7666eb92f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + Clicks on 'Update attributes' from dropdown actions list on product grid page. Products should be selected via mass action before + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDisableActiveCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDisableActiveCategoryActionGroup.xml new file mode 100644 index 0000000000000..cadef1a23f0c9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDisableActiveCategoryActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Disable an active category + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDisableIncludeInMenuConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDisableIncludeInMenuConfigActionGroup.xml new file mode 100644 index 0000000000000..c83e83e9d064e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDisableIncludeInMenuConfigActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Set "Include in Menu" option to No for Category + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminIncludeInMenuExcludedCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminIncludeInMenuExcludedCategoryActionGroup.xml new file mode 100644 index 0000000000000..dceac49f58f63 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminIncludeInMenuExcludedCategoryActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Include to menu the excluded category + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml new file mode 100644 index 0000000000000..811eb6ee5f1f7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + Clicks on 'Save' button on products mass attributes update page. + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..214a062704282 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.xml @@ -0,0 +1,30 @@ + + + + + + + On the Create/Edit product page create new Attribute + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIncludedToMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIncludedToMenuActionGroup.xml new file mode 100644 index 0000000000000..978af87b9f1c3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIncludedToMenuActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Verify the category is included to menu + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsEnabledActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsEnabledActionGroup.xml new file mode 100644 index 0000000000000..1cea3c5dfa041 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsEnabledActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Verify the category is enabled + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotIncludeInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotIncludeInMenuActionGroup.xml new file mode 100644 index 0000000000000..704cf2eca6209 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotIncludeInMenuActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Verify the category is not included in menu + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryPageTitleActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryPageTitleActionGroup.xml new file mode 100644 index 0000000000000..acbda5720191f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryPageTitleActionGroup.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml index 2de7bf19fd378..58c64b6273f79 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml @@ -9,7 +9,7 @@
- + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml index ad4ab57f8de2c..d7dc38a4a88f6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml @@ -23,5 +23,7 @@ + +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml index 5efd04eacb719..e4b33ac795559 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -18,10 +18,11 @@ - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml index 5bdd3bd5abcc6..1ca051e2f6669 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml @@ -48,12 +48,13 @@ - - + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index 540db609f550b..8f2b789639e7f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -26,8 +26,8 @@ - - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml index 5ec493aef0cea..848035b911aab 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -14,10 +14,10 @@ - + - - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml index 758dcee69525e..10cba3ab209ef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -8,16 +8,16 @@ - + - + <title value="DEPRECATED: Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> <description value="login as admin and create configurable product attribute with Dropdown field"/> <severity value="BLOCKER"/> <testCaseId value="MC-10905"/> <group value="mtf_migrated"/> <skip> - <issueId value="MC-15474"/> + <issueId value="DEPRECATED">Use AdminVerifyCreateCustomProductAttributeTest</issueId> </skip> </annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml new file mode 100644 index 0000000000000..dc34607f2a771 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml @@ -0,0 +1,58 @@ +<?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="AdminMassProductAttributeUpdateAddedToQueueTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product attributes"/> + <title value="Check functionality of RabbitMQ"/> + <description value="Mass product attribute update task should be added to the queue"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28990"/> + <useCaseId value="MC-29179"/> + <group value="catalog"/> + <group value="asynchronousOperations"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="createFirstProduct"/> + <createData entity="ApiProductWithDescription" stepKey="createSecondProduct"/> + <createData entity="ApiProductNameWithNoSpaces" stepKey="createThirdProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> + <argument name="keyword" value="api-simple-product"/> + </actionGroup> + <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="selectThirdProduct"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="selectSecondProduct"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('3')}}" stepKey="selectFirstProduct"/> + <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="goToUpdateProductAttributesPage"/> + <checkOption selector="{{AdminEditProductAttributesSection.changeAttributeShortDescriptionToggle}}" stepKey="toggleToChangeShortDescription"/> + <fillField selector="{{AdminEditProductAttributesSection.attributeShortDescription}}" userInput="Test Update" stepKey="fillShortDescriptionField"/> + <actionGroup ref="AdminSaveProductsMassAttributesUpdateActionGroup" stepKey="saveMassAttributeUpdate"/> + <see selector="{{AdminSystemMessagesSection.info}}" userInput="Task "Update attributes for 3 selected products": 1 item(s) have been scheduled for update." stepKey="seeInfoMessage"/> + <click selector="{{AdminSystemMessagesSection.viewDetailsLink}}" stepKey="seeDetails"/> + <see selector="{{AdminBulkDetailsModalSection.descriptionValue}}" userInput="Update attributes for 3 selected products" stepKey="seeDescription"/> + <see selector="{{AdminBulkDetailsModalSection.summaryValue}}" userInput="Pending, in queue..." stepKey="seeSummary"/> + <grabTextFrom selector="{{AdminBulkDetailsModalSection.startTimeValue}}" stepKey="grabStartTimeValue"/> + <assertRegExp stepKey="assertStartTime"> + <expectedResult type="string">/\d{1,2}\/\d{2}\/\d{4}\s\d{1,2}:\d{2}:\d{2}\s(AM|PM)/</expectedResult> + <actualResult type="variable">grabStartTimeValue</actualResult> + </assertRegExp> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index 5d257bf0648cd..0af3911474813 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -33,6 +33,9 @@ <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> <argument name="indexerValue" value="catalog_product_category"/> </actionGroup> + <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCatalogSearch"> + <argument name="indexerValue" value="catalogsearch_fulltext"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml index 21f2b622c2ebd..ea50a17b47b44 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -30,26 +30,35 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Update category and make category inactive--> - <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="navigateToCreatedCategory"> + <argument name="Category" value="$$createDefaultCategory$$"/> + </actionGroup> + <actionGroup ref="AdminDisableActiveCategoryActionGroup" stepKey="disableCategory"/> <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> - <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> + <actionGroup ref="AssertAdminCategoryPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="categoryName" value="$$createDefaultCategory.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryIsInactiveActionGroup" stepKey="seeDisabledCategory"/> <!--Verify Inactive Category is store front page--> - <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> - <waitForPageLoad stepKey="waitForPageToBeLoaded"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> - <waitForPageLoad time="15" stepKey="wait"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStoreFront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryNameInMenu"> + <argument name="categoryName" value="$$createDefaultCategory.name$$"/> + </actionGroup> <!--Verify Inactive Category in category page --> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> - <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="assertCategoryInTree" /> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory1"/> - <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle1" /> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="assertCategoryIsInactive"/> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="goToAdminCategoryIndexPage"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsListedInCategoriesTreeActionGroup" stepKey="seeCategoryInTree"> + <argument name="categoryName" value="$$createDefaultCategory.name$$"/> + </actionGroup> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$createDefaultCategory$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryPageTitleActionGroup" stepKey="seeCategoryPageTitle"> + <argument name="categoryName" value="$$createDefaultCategory.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryIsInactiveActionGroup" stepKey="assertCategoryIsInactive"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml new file mode 100644 index 0000000000000..5cf3d9e38ddd4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml @@ -0,0 +1,92 @@ +<?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="AdminVerifyCreateCustomProductAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create product Attribute"/> + <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> + <description value="login as admin and create simple product with attribute Dropdown field"/> + <severity value="MAJOR"/> + <testCaseId value="MC-26027"/> + <group value="mtf_migrated"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteCreatedAttribute"> + <argument name="productAttributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> + </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToProductPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <actionGroup ref="AdminStartCreateProductAttributeOnProductPageActionGroup" stepKey="createDropdownAttribute"> + <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}" /> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}" /> + <argument name="inputType" value="Dropdown" /> + </actionGroup> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="waitForStoreFrontProperties"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> + <actionGroup ref="AdminAddOptionForDropdownAttributeActionGroup" stepKey="createDropdownOption"> + <argument name="storefrontViewAttributeValue" value="{{ProductAttributeOption8.label}}"/> + <argument name="adminAttributeLabel" value="{{ProductAttributeOption8.label}}"/> + </actionGroup> + <checkOption selector="{{AdminCreateNewProductAttributeSection.defaultRadioButton('1')}}" stepKey="selectRadioButton"/> + <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveTheProduct"/> + <actionGroup ref="AdminAssertProductAttributeOnProductEditPageActionGroup" stepKey="adminProductAssertAttribute"> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SearchAttributeByCodeOnProductAttributeGridActionGroup" stepKey="searchAttributeByCodeOnProductAttributeGrid"> + <argument name="productAttributeCode" value="{{newProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AdminAssertProductAttributeInAttributeGridActionGroup" stepKey="assertAttributeOnProductAttributesGrid"> + <argument name="productAttributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductPageOnStorefront"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup" stepKey="checkProductPriceAndNameInStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + <argument name="expectedPrice" value="$createProduct.price$"/> + </actionGroup> + <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToAttribute"/> + <actionGroup ref="CheckAttributeInMoreInformationTabActionGroup" stepKey="checkAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="attributeValue" value="{{ProductAttributeOption8.value}}"/> + </actionGroup> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductAttribute"> + <argument name="phrase" value="{{ProductAttributeOption8.value}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeWithOptionInLayeredNavigation"> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="attributeOptionLabel" value="{{ProductAttributeOption8.value}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="assertProductPresentOnSearchPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 55d697e35deba..5211e0b2812ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -133,8 +133,7 @@ <waitForPageLoad stepKey="waitForFiltersClear"/> <!--Create Cart Price Rule--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <waitForPageLoad stepKey="waitForPageDiscountPageIsLoaded"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="ship" stepKey="fillRuleName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml index 54a9e5a244427..59e3700acf5c3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml @@ -18,63 +18,83 @@ <testCaseId value="MAGETWO-72238"/> <group value="category"/> </annotations> + <before> + <!-- Create a category --> + <createData entity="ApiCategory" stepKey="simpleCategory"/> + <!-- Create second category, having as parent the 1st one --> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory"> + <requiredEntity createDataKey="simpleCategory"/> + </createData> + </before> <after> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage2"/> - - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> - <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCategory"> - <argument name="categoryEntity" value="SimpleSubCategory"/> - </actionGroup> + <deleteData createDataKey="simpleSubCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="simpleCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage1"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <!--Create new category under Default Category--> - <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory1"> - <argument name="categoryEntity" value="SimpleSubCategory"/> - </actionGroup> - <!--Create another subcategory under created category--> - <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory2"> - <argument name="categoryEntity" value="SubCategoryWithParent"/> - </actionGroup> + <!--Go to storefront and verify visibility of categories--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSimpleSubCategoryOnStorefront1"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront1"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCreatedCategoryOnStorefront"> + <argument name="categoryName" value="$$simpleCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryOnStorefront"> + <argument name="categoryName" value="$$simpleSubCategory.name$$"/> + </actionGroup> + <!--Set Include in menu to No on created category under Default Category --> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage2"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategory1"/> - <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="setNoToIncludeInMenuSelect"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton1"/> - <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="seeCheckboxEnableCategoryIsChecked"/> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="dontSeeCheckboxIncludeInMenuIsChecked"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openParentCategoryViaAdmin"> + <argument name="Category" value="$$simpleCategory$$"/> + </actionGroup> + <actionGroup ref="AdminDisableIncludeInMenuConfigActionGroup" stepKey="setNoToIncludeInMenuSelect"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AssertAdminCategoryIsEnabledActionGroup" stepKey="assertParentCategoryIsActive"/> + <actionGroup ref="AssertAdminCategoryIsNotIncludeInMenuActionGroup" stepKey="assertParentCategoryIsNotIncludeInMenu"/> + <!--Go to storefront and verify visibility of categories--> - <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSimpleSubCategoryOnStorefront1"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront2"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPageSecondTime"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeParentCategoryOnStorefront"> + <argument name="categoryName" value="$$simpleCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategory"> + <argument name="categoryName" value="$$simpleSubCategory.name$$"/> + </actionGroup> + <!--Set Enable category to No and Include in menu to Yes on created category under Default Category --> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage3"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategory2"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="SetNoToEnableCategorySelect"/> - <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="SetYesToIncludeInMenuSelect"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton2"/> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontSeeCheckboxEnableCategoryIsChecked"/> - <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="seeCheckboxIncludeInMenuIsChecked"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openParentCategoryViaAdminSecondTime"> + <argument name="Category" value="$$simpleCategory$$"/> + </actionGroup> + <actionGroup ref="AdminDisableActiveCategoryActionGroup" stepKey="SetNoToEnableCategorySelect"/> + <actionGroup ref="AdminIncludeInMenuExcludedCategoryActionGroup" stepKey="SetToYesIncludeInMenuSelect"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveParentCategory"/> + <actionGroup ref="AssertAdminCategoryIsInactiveActionGroup" stepKey="seeCategoryIsDisabled"/> + <actionGroup ref="AssertAdminCategoryIncludedToMenuActionGroup" stepKey="seeCheckboxIncludeInMenuIsChecked"/> + <!--Go to storefront and verify visibility of categories--> - <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage3"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSimpleSubCategoryOnStorefront2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront3"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPageThirdTime"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryInMenuOnStorefront"> + <argument name="categoryName" value="$$simpleCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryInMenuOnStorefront"> + <argument name="categoryName" value="$$simpleSubCategory.name$$"/> + </actionGroup> + <!--Set Enable category to No and Include in menu to No on created category under Default Category --> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage4"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategory3"/> - <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="setNoToIncludeInMenuSelect2"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton3"/> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontSeeCheckboxEnableCategoryIsChecked2"/> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="dontSeeCheckboxIncludeInMenuIsChecked2"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openParentCategoryViaAdminThirdTime"> + <argument name="Category" value="$$simpleCategory$$"/> + </actionGroup> + <actionGroup ref="AdminDisableIncludeInMenuConfigActionGroup" stepKey="setNoToIncludeInMenuForParentCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveChanges"/> + <actionGroup ref="AssertAdminCategoryIsInactiveActionGroup" stepKey="assertCategoryIsDisabled"/> + <actionGroup ref="AssertAdminCategoryIsNotIncludeInMenuActionGroup" stepKey="assertParentCategoryIsNotIncludeToMenu"/> + <!--Go to storefront and verify visibility of categories--> - <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage4"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSimpleSubCategoryOnStorefront3"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront4"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPageFourthTime"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryOnStorefront"> + <argument name="categoryName" value="$$simpleCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryInMenu"> + <argument name="categoryName" value="$$simpleSubCategory.name$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index 521468cd82927..886b03e0f3c1e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -7,7 +7,10 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; +use Magento\Catalog\Api\Data\CategoryLinkInterface; +use Magento\Catalog\Api\Data\CategoryLinkInterfaceFactory; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; use Magento\Catalog\Api\Data\ProductLinkTypeInterface; use Magento\Catalog\Api\ProductRepositoryInterface as ProductRepository; @@ -125,17 +128,13 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(ProductRepository::class) - ->disableOriginalConstructor() - ->getMock(); + $this->productRepositoryMock = $this->createMock(ProductRepository::class); $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->setMethods(['getPost']) ->getMockForAbstractClass(); - $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) - ->getMockForAbstractClass(); - $this->stockFilterMock = $this->getMockBuilder(StockDataFilter::class) - ->disableOriginalConstructor() - ->getMock(); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->stockFilterMock = $this->createMock(StockDataFilter::class); + $this->productMock = $this->getMockBuilder(Product::class) ->setMethods( [ @@ -150,30 +149,34 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $productExtensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) + ->setMethods(['getCategoryLinks', 'setCategoryLinks']) + ->getMockForAbstractClass(); + $this->productMock->setExtensionAttributes($productExtensionAttributes); + $this->customOptionFactoryMock = $this->getMockBuilder(ProductCustomOptionInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->productLinksMock = $this->getMockBuilder(ProductLinks::class) - ->disableOriginalConstructor() - ->getMock(); - $this->linkTypeProviderMock = $this->getMockBuilder(LinkTypeProvider::class) - ->disableOriginalConstructor() - ->getMock(); + $this->productLinksMock = $this->createMock(ProductLinks::class); + $this->linkTypeProviderMock = $this->createMock(LinkTypeProvider::class); $this->productLinksMock->expects($this->any()) ->method('initializeLinks') ->willReturn($this->productMock); - $this->attributeFilterMock = $this->getMockBuilder(AttributeFilter::class) - ->setMethods(['prepareProductAttributes']) - ->disableOriginalConstructor() - ->getMock(); - $this->localeFormatMock = $this->getMockBuilder(Format::class) - ->setMethods(['getNumber']) - ->disableOriginalConstructor() - ->getMock(); + $this->attributeFilterMock = $this->createMock(AttributeFilter::class); + $this->localeFormatMock = $this->createMock(Format::class); $this->dateTimeFilterMock = $this->createMock(DateTime::class); + $categoryLinkFactoryMock = $this->getMockBuilder(CategoryLinkInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $categoryLinkFactoryMock->method('create') + ->willReturnCallback(function () { + return $this->createMock(CategoryLinkInterface::class); + }); + $this->helper = $this->objectManager->getObject( Helper::class, [ @@ -187,13 +190,12 @@ protected function setUp(): void 'linkTypeProvider' => $this->linkTypeProviderMock, 'attributeFilter' => $this->attributeFilterMock, 'localeFormat' => $this->localeFormatMock, - 'dateTimeFilter' => $this->dateTimeFilterMock + 'dateTimeFilter' => $this->dateTimeFilterMock, + 'categoryLinkFactory' => $categoryLinkFactoryMock, ] ); - $this->linkResolverMock = $this->getMockBuilder(Resolver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->linkResolverMock = $this->createMock(Resolver::class); $helperReflection = new \ReflectionClass(get_class($this->helper)); $resolverProperty = $helperReflection->getProperty('linkResolver'); $resolverProperty->setAccessible(true); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php index e2c37c904ee82..5b2334cd55f05 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php @@ -20,6 +20,8 @@ use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\RequestInterface; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Config\Data; +use Magento\Framework\Config\DataInterfaceFactory; use Magento\Framework\Registry; use Magento\Framework\Stdlib\ArrayUtils; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -70,6 +72,11 @@ class DataProviderTest extends TestCase */ private $categoryFactory; + /** + * @var DataInterfaceFactory|MockObject + */ + private $uiConfigFactory; + /** * @var Collection|MockObject */ @@ -151,6 +158,15 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $dataMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiConfigFactory = $this->getMockBuilder(DataInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiConfigFactory->method('create') + ->willReturn($dataMock); + $this->fileInfo = $this->getMockBuilder(FileInfo::class) ->disableOriginalConstructor() ->getMock(); @@ -198,6 +214,7 @@ private function getModel() 'eavConfig' => $this->eavConfig, 'request' => $this->request, 'categoryFactory' => $this->categoryFactory, + 'uiConfigFactory' => $this->uiConfigFactory, 'pool' => $this->modifierPool, 'auth' => $this->auth, 'arrayUtils' => $this->arrayUtils, diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php new file mode 100644 index 0000000000000..c5018f1aa6313 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Plugin; + +use Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; + +class TableResolverTest extends TestCase +{ + /** + * Tests replacing catalog_category_product_index table name + * + * @param int $storeId + * @param string $tableName + * @param string $expected + * @dataProvider afterGetTableNameDataProvider + */ + public function testAfterGetTableName(int $storeId, string $tableName, string $expected): void + { + $storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + + $storeMock = $this->getMockBuilder(Store::class) + ->onlyMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $storeMock->method('getId') + ->willReturn($storeId); + + $storeManagerMock->method('getStore')->willReturn($storeMock); + + $tableResolverMock = $this->getMockBuilder(IndexScopeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $tableResolverMock->method('resolve')->willReturn('catalog_category_product_index_store1'); + + $subjectMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $model = new TableResolver($storeManagerMock, $tableResolverMock); + + $this->assertEquals( + $expected, + $model->afterGetTableName($subjectMock, $tableName, 'catalog_category_product_index') + ); + } + + /** + * Data provider for testAfterGetTableName + * + * @return array + */ + public function afterGetTableNameDataProvider(): array + { + return [ + [ + 'storeId' => 1, + 'tableName' => 'catalog_category_product_index', + 'expected' => 'catalog_category_product_index_store1' + ], + [ + 'storeId' => 0, + 'tableName' => 'catalog_category_product_index', + 'expected' => 'catalog_category_product_index' + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php index 629500ca91cdc..053d7d6e97826 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\Product\Filter\DateTime; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; use Magento\Framework\Stdlib\DateTime\Timezone; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; @@ -43,7 +44,7 @@ function () { ); $timezone = $objectManager->getObject( Timezone::class, - ['localeResolver' => $localeResolver] + ['localeResolver' => $localeResolver, 'dateFormatterFactory' => new DateFormatterFactory()] ); $stdlibDateTimeFilter = $objectManager->getObject( \Magento\Framework\Stdlib\DateTime\Filter\DateTime::class, diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php index e46884d1637da..d4c1db4ec1b28 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php @@ -12,12 +12,12 @@ use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory; -use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; - use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use PHPUnit\Framework\MockObject\MockObject; /** * Test for \Magento\Catalog\Model\Product\Option\Value class. @@ -30,17 +30,20 @@ class ValueTest extends TestCase private $model; /** - * @var CustomOptionPriceCalculator + * @var CalculateCustomOptionCatalogRule|MockObject */ - private $customOptionPriceCalculatorMock; + private $calculateCustomOptionCatalogRule; + /** + * @inheritDoc + */ protected function setUp(): void { $mockedResource = $this->getMockedResource(); $mockedCollectionFactory = $this->getMockedValueCollectionFactory(); - $this->customOptionPriceCalculatorMock = $this->createMock( - CustomOptionPriceCalculator::class + $this->calculateCustomOptionCatalogRule = $this->createMock( + CalculateCustomOptionCatalogRule::class ); $helper = new ObjectManager($this); @@ -49,7 +52,7 @@ protected function setUp(): void [ 'resource' => $mockedResource, 'valueCollectionFactory' => $mockedCollectionFactory, - 'customOptionPriceCalculator' => $this->customOptionPriceCalculatorMock, + 'calculateCustomOptionCatalogRule' => $this->calculateCustomOptionCatalogRule ] ); $this->model->setOption($this->getMockedOption()); @@ -77,8 +80,8 @@ public function testGetPrice() $this->assertEquals($price, $this->model->getPrice(false)); $percentPrice = 100.0; - $this->customOptionPriceCalculatorMock->expects($this->atLeastOnce()) - ->method('getOptionPriceByPriceCode') + $this->calculateCustomOptionCatalogRule->expects($this->atLeastOnce()) + ->method('execute') ->willReturn($percentPrice); $this->assertEquals($percentPrice, $this->model->getPrice(true)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 48a081aaeda54..42d0778daa4af 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -313,10 +313,7 @@ protected function setUp(): void $contextMock = $this->createPartialMock( Context::class, - ['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'], - [], - '', - false + ['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'] ); $contextMock->expects($this->any())->method('getAppState')->willReturn($this->appStateMock); $contextMock->expects($this->any()) @@ -541,7 +538,7 @@ public function testGetStoreSingleSiteModelIds( /** * @return array */ - public function getSingleStoreIds() + public function getSingleStoreIds(): array { return [ [ @@ -619,7 +616,7 @@ public function testGetCategoryCollectionCollectionNull($initCategoryCollection, $result = $product->getCategoryCollection(); - $productIdCachedActual = $this->getPropertyValue($product, '_productIdCached', $productIdCached); + $productIdCachedActual = $this->getPropertyValue($product, '_productIdCached'); $this->assertEquals($getIdResult, $productIdCachedActual); $this->assertEquals($initCategoryCollection, $result); } @@ -627,7 +624,7 @@ public function testGetCategoryCollectionCollectionNull($initCategoryCollection, /** * @return array */ - public function getCategoryCollectionCollectionNullDataProvider() + public function getCategoryCollectionCollectionNullDataProvider(): array { return [ [ @@ -742,7 +739,7 @@ public function testReindex($productChanged, $isScheduled, $productFlatCount, $c /** * @return array */ - public function getProductReindexProvider() + public function getProductReindexProvider(): array { return [ 'set 1' => [true, false, 1, 1], @@ -774,12 +771,18 @@ public function testPriceReindexCallback() /** * @dataProvider getIdentitiesProvider * @param array $expected - * @param array $origData + * @param array|null $origData * @param array $data * @param bool $isDeleted - */ - public function testGetIdentities($expected, $origData, $data, $isDeleted = false) - { + * @param bool $isNew + */ + public function testGetIdentities( + array $expected, + ?array $origData, + array $data, + bool $isDeleted = false, + bool $isNew = false + ) { $this->model->setIdFieldName('id'); if (is_array($origData)) { foreach ($origData as $key => $value) { @@ -790,13 +793,14 @@ public function testGetIdentities($expected, $origData, $data, $isDeleted = fals $this->model->setData($key, $value); } $this->model->isDeleted($isDeleted); + $this->model->isObjectNew($isNew); $this->assertEquals($expected, $this->model->getIdentities()); } /** * @return array */ - public function getIdentitiesProvider() + public function getIdentitiesProvider(): array { $extensionAttributesMock = $this->getMockBuilder(ExtensionAttributesInterface::class) ->disableOriginalConstructor() @@ -814,90 +818,129 @@ public function getIdentitiesProvider() ['id' => 1, 'name' => 'value', 'category_ids' => [1]], ], 'new product' => $this->getNewProductProviderData(), + 'new disabled product' => $this->getNewDisabledProductProviderData(), 'status and category change' => [ [0 => 'cat_p_1', 1 => 'cat_c_p_1', 2 => 'cat_c_p_2'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 2], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_DISABLED], + [ + 'id' => 1, + 'name' => 'value', + 'category_ids' => [2], + 'status' => Status::STATUS_ENABLED, + 'affected_category_ids' => [1, 2], + 'is_changed_categories' => true + ], + ], + 'category change for disabled product' => [ + [0 => 'cat_p_1'], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_DISABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [2], - 'status' => 1, + 'status' => Status::STATUS_DISABLED, 'affected_category_ids' => [1, 2], 'is_changed_categories' => true ], ], - 'status change only' => [ + 'status change to disabled' => [ + [0 => 'cat_p_1', 1 => 'cat_c_p_7'], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_ENABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_DISABLED], + ], + 'status change to enabled' => [ [0 => 'cat_p_1', 1 => 'cat_c_p_7'], - ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => 1], - ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => 2], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_DISABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_ENABLED], ], 'status changed, category unassigned' => $this->getStatusAndCategoryChangesData(), 'no status changes' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], ], - 'no stock status changes' => [ + 'no stock status changes' => $this->getNoStockStatusChangesData($extensionAttributesMock), + 'no stock status data 1' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, - 'stock_data' => ['is_in_stock' => true], + 'status' => Status::STATUS_ENABLED, ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], ], - 'no stock status data 1' => [ + 'no stock status data 2' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, - ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, + 'status' => Status::STATUS_ENABLED, + 'stock_data' => ['is_in_stock' => true], ], ], - 'no stock status data 2' => [ + 'stock status changes for enabled product' => $this->getStatusStockProviderData($extensionAttributesMock), + 'stock status changes for disabled product' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_DISABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, - 'stock_data' => ['is_in_stock' => true], + 'status' => Status::STATUS_DISABLED, + 'stock_data' => ['is_in_stock' => false], + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], ], - 'stock status changes' => $this->getStatusStockProviderData($extensionAttributesMock), ]; } /** * @return array */ - private function getStatusAndCategoryChangesData() + private function getStatusAndCategoryChangesData(): array { return [ [0 => 'cat_p_1', 1 => 'cat_c_p_5'], - ['id' => 1, 'name' => 'value', 'category_ids' => [5], 'status' => 2], + ['id' => 1, 'name' => 'value', 'category_ids' => [5], 'status' => Status::STATUS_DISABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'is_changed_categories' => true, 'affected_category_ids' => [5] ], ]; } + /** + * @param MockObject $extensionAttributesMock + * @return array + */ + private function getNoStockStatusChangesData($extensionAttributesMock): array + { + return [ + [0 => 'cat_p_1'], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], + [ + 'id' => 1, + 'name' => 'value', + 'category_ids' => [1], + 'status' => Status::STATUS_ENABLED, + 'stock_data' => ['is_in_stock' => true], + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, + ], + ]; + } + /** * @return array */ - private function getNewProductProviderData() + private function getNewProductProviderData(): array { return [ ['cat_p_1', 'cat_c_p_1'], @@ -908,7 +951,30 @@ private function getNewProductProviderData() 'category_ids' => [1], 'affected_category_ids' => [1], 'is_changed_categories' => true - ] + ], + false, + true, + ]; + } + + /** + * @return array + */ + private function getNewDisabledProductProviderData(): array + { + return [ + ['cat_p_1'], + null, + [ + 'id' => 1, + 'name' => 'value', + 'category_ids' => [1], + 'status' => Status::STATUS_DISABLED, + 'affected_category_ids' => [1], + 'is_changed_categories' => true + ], + false, + true, ]; } @@ -916,16 +982,16 @@ private function getNewProductProviderData() * @param MockObject $extensionAttributesMock * @return array */ - private function getStatusStockProviderData($extensionAttributesMock) + private function getStatusStockProviderData($extensionAttributesMock): array { return [ [0 => 'cat_p_1', 1 => 'cat_c_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'stock_data' => ['is_in_stock' => false], ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], @@ -1440,7 +1506,7 @@ public function testGetCustomAttributes() /** * @return array */ - public function priceDataProvider() + public function priceDataProvider(): array { return [ 'receive empty array' => [[]], diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 97a787c87bfa8..8a116282e2578 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -80,6 +80,9 @@ <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> <type name="Magento\Catalog\Model\Category\DataProvider"> + <arguments> + <argument name="uiConfigFactory" xsi:type="object">uiComponentConfigFactory</argument> + </arguments> <plugin name="set_page_layout_default_value" type="Magento\Catalog\Model\Plugin\SetPageLayoutDefaultValue" /> </type> <type name="Magento\Theme\Block\Html\Topmenu"> diff --git a/app/code/Magento/Catalog/etc/view.xml b/app/code/Magento/Catalog/etc/view.xml index 0740ab5b154f6..910a3be8055da 100644 --- a/app/code/Magento/Catalog/etc/view.xml +++ b/app/code/Magento/Catalog/etc/view.xml @@ -8,9 +8,5 @@ <view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd"> <vars module="Magento_Catalog"> <var name="product_image_white_borders">1</var> - <!-- Variable to enable lazy loading for catalog product images without borders. - If you enable this setting your small size images without borders may be stretched in template. - So be sure you have correct image sizes. --> - <var name="enable_lazy_loading_for_images_without_borders">0</var> </vars> </view> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 0ac6bc88df8ce..8abfe368909e4 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -8,16 +8,6 @@ /** @var $block \Magento\Catalog\Block\Product\Image */ /** @var $escaper \Magento\Framework\Escaper */ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ -/** - * Enable lazy loading for images with borders and if variable enable_lazy_loading_for_images_without_borders - * is enabled in view.xml. Otherwise small size images without borders may be distorted. So max-width is used for them - * to prevent stretching and lazy loading does not work. - */ -$borders = (bool)$block->getVar('product_image_white_borders', 'Magento_Catalog'); -$enableLazyLoadingWithoutBorders = (bool)$block->getVar( - 'enable_lazy_loading_for_images_without_borders', - 'Magento_Catalog' -); $width = (int)$block->getWidth(); $paddingBottom = $block->getRatio() * 100; ?> @@ -29,13 +19,8 @@ $paddingBottom = $block->getRatio() * 100; <?php endforeach; ?> src="<?= $escaper->escapeUrl($block->getImageUrl()) ?>" loading="lazy" - <?php if ($borders || $enableLazyLoadingWithoutBorders): ?> - width="<?= $escaper->escapeHtmlAttr($block->getWidth()) ?>" - height="<?= $escaper->escapeHtmlAttr($block->getHeight()) ?>" - <?php else: ?> - max-width="<?= $escaper->escapeHtmlAttr($block->getWidth()) ?>" - max-height="<?= $escaper->escapeHtmlAttr($block->getHeight()) ?>" - <?php endif; ?> + width="<?= $escaper->escapeHtmlAttr($block->getWidth()) ?>" + height="<?= $escaper->escapeHtmlAttr($block->getHeight()) ?>" alt="<?= $escaper->escapeHtmlAttr($block->getLabel()) ?>"/></span> </span> <?php diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php index e78224ba0af38..9bc0d87061a01 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -110,6 +110,11 @@ public function resolve( } $product = $value['model']; + + if ($product->hasData('can_show_price') && $product->getData('can_show_price') === false) { + return []; + } + $productId = $product->getId(); $this->tiers->addProductFilter($productId); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 140659abfbfe6..d46776bfe498e 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -64,6 +64,13 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode 'attribute_label' => 'a.frontend_label', ] ) + ->joinLeft( + ['attribute_label' => $this->resourceConnection->getTableName('eav_attribute_label')], + "a.attribute_id = attribute_label.attribute_id AND attribute_label.store_id = {$storeId}", + [ + 'attribute_store_label' => 'attribute_label.value', + ] + ) ->joinLeft( ['options' => $this->resourceConnection->getTableName('eav_attribute_option')], 'a.attribute_id = options.attribute_id', @@ -119,7 +126,8 @@ private function formatResult(\Magento\Framework\DB\Select $select): array $result[$option['attribute_code']] = [ 'attribute_id' => $option['attribute_id'], 'attribute_code' => $option['attribute_code'], - 'attribute_label' => $option['attribute_label'], + 'attribute_label' => $option['attribute_store_label'] + ? $option['attribute_store_label'] : $option['attribute_label'], 'options' => [], ]; } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 105e91320de49..5fce0fcdf3ca2 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -155,6 +155,10 @@ function (AggregationValueInterface $value) { return []; } - return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes); + return $this->attributeOptionProvider->getOptions( + \array_merge([], ...$attributeOptionIds), + $storeId, + $attributes + ); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php index ff661236be62f..ac3f396b45ef8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php @@ -36,7 +36,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array foreach ($this->builders as $builder) { $layers[] = $builder->build($aggregation, $storeId); } - $layers = \array_merge(...$layers); + $layers = \array_merge([], ...$layers); return \array_filter($layers); } diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 0bfd9d58ec969..34f5dd831686c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -88,7 +88,7 @@ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): } } if ($fragmentFields) { - $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + $selectedFields = array_merge([], $selectedFields, ...$fragmentFields); } $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index dc93005983776..4350b6dd85266 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -7,17 +7,17 @@ namespace Magento\CatalogGraphQl\Model\Category; -use Magento\Catalog\Api\CategoryListInterface; -use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategorySearchResultsInterface; +use Magento\Catalog\Api\Data\CategorySearchResultsInterfaceFactory; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface; +use Magento\CatalogGraphQl\Model\Category\Filter\SearchCriteria; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Sort; -use Magento\Search\Model\Query; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; /** * Category filter allows filtering category results by attributes. @@ -25,38 +25,57 @@ class CategoryFilter { /** - * @var string + * @var CollectionFactory */ - private const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; + private $categoryCollectionFactory; /** - * @var ScopeConfigInterface + * @var CollectionProcessorInterface */ - private $scopeConfig; + private $collectionProcessor; /** - * @var CategoryListInterface + * @var JoinProcessorInterface */ - private $categoryList; + private $extensionAttributesJoinProcessor; /** - * @var Builder + * @var CategorySearchResultsInterfaceFactory */ - private $searchCriteriaBuilder; + private $categorySearchResultsFactory; /** - * @param ScopeConfigInterface $scopeConfig - * @param CategoryListInterface $categoryList - * @param Builder $searchCriteriaBuilder + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @var SearchCriteria + */ + private $searchCriteria; + + /** + * @param CollectionFactory $categoryCollectionFactory + * @param CollectionProcessorInterface $collectionProcessor + * @param JoinProcessorInterface $extensionAttributesJoinProcessor + * @param CategorySearchResultsInterfaceFactory $categorySearchResultsFactory + * @param CategoryRepositoryInterface $categoryRepository + * @param SearchCriteria $searchCriteria */ public function __construct( - ScopeConfigInterface $scopeConfig, - CategoryListInterface $categoryList, - Builder $searchCriteriaBuilder + CollectionFactory $categoryCollectionFactory, + CollectionProcessorInterface $collectionProcessor, + JoinProcessorInterface $extensionAttributesJoinProcessor, + CategorySearchResultsInterfaceFactory $categorySearchResultsFactory, + CategoryRepositoryInterface $categoryRepository, + SearchCriteria $searchCriteria ) { - $this->scopeConfig = $scopeConfig; - $this->categoryList = $categoryList; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->categoryCollectionFactory = $categoryCollectionFactory; + $this->collectionProcessor = $collectionProcessor; + $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; + $this->categorySearchResultsFactory = $categorySearchResultsFactory; + $this->categoryRepository = $categoryRepository; + $this->searchCriteria = $searchCriteria; } /** @@ -64,21 +83,25 @@ public function __construct( * * @param array $criteria * @param StoreInterface $store + * @param array $attributeNames + * @param ContextInterface $context * @return int[] * @throws InputException */ - public function getResult(array $criteria, StoreInterface $store) + public function getResult(array $criteria, StoreInterface $store, array $attributeNames, ContextInterface $context) { - $categoryIds = []; - $criteria[Filter::ARGUMENT_NAME] = $this->formatMatchFilters($criteria['filters'], $store); - $criteria[Filter::ARGUMENT_NAME][CategoryInterface::KEY_IS_ACTIVE] = ['eq' => 1]; - $criteria[Sort::ARGUMENT_NAME][CategoryInterface::KEY_POSITION] = ['ASC']; - $searchCriteria = $this->searchCriteriaBuilder->build('categoryList', $criteria); - $pageSize = $criteria['pageSize'] ?? 20; - $currentPage = $criteria['currentPage'] ?? 1; - $searchCriteria->setPageSize($pageSize)->setCurrentPage($currentPage); + $searchCriteria = $this->searchCriteria->buildCriteria($criteria, $store); + $collection = $this->categoryCollectionFactory->create(); + $this->extensionAttributesJoinProcessor->process($collection); + $this->collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); + + /** @var CategorySearchResultsInterface $searchResult */ + $categories = $this->categorySearchResultsFactory->create(); + $categories->setSearchCriteria($searchCriteria); + $categories->setItems($collection->getItems()); + $categories->setTotalCount($collection->getSize()); - $categories = $this->categoryList->getList($searchCriteria); + $categoryIds = []; foreach ($categories->getItems() as $category) { $categoryIds[] = (int)$category->getId(); } @@ -106,35 +129,4 @@ public function getResult(array $criteria, StoreInterface $store) ] ]; } - - /** - * Format match filters to behave like fuzzy match - * - * @param array $filters - * @param StoreInterface $store - * @return array - * @throws InputException - */ - private function formatMatchFilters(array $filters, StoreInterface $store): array - { - $minQueryLength = $this->scopeConfig->getValue( - Query::XML_PATH_MIN_QUERY_LENGTH, - ScopeInterface::SCOPE_STORE, - $store - ); - - foreach ($filters as $filter => $condition) { - $conditionType = current(array_keys($condition)); - if ($conditionType === 'match') { - $searchValue = trim(str_replace(self::SPECIAL_CHARACTERS, '', $condition[$conditionType])); - $matchLength = strlen($searchValue); - if ($matchLength < $minQueryLength) { - throw new InputException(__('Invalid match filter. Minimum length is %1.', $minQueryLength)); - } - unset($filters[$filter]['match']); - $filters[$filter]['like'] = '%' . $searchValue . '%'; - } - } - return $filters; - } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php b/app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php new file mode 100644 index 0000000000000..aea34f19fea16 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Category\Filter; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Sort; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; +use Magento\Search\Model\Query; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Utility to help transform raw criteria data into SearchCriteriaInterface + */ +class SearchCriteria +{ + /** + * @var string + */ + private const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var Builder + */ + private $searchCriteriaBuilder; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Builder $searchCriteriaBuilder + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Builder $searchCriteriaBuilder + ) { + $this->scopeConfig = $scopeConfig; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Transform raw criteria data into SearchCriteriaInterface + * + * @param array $criteria + * @param StoreInterface $store + * @return SearchCriteriaInterface + * @throws InputException + */ + public function buildCriteria(array $criteria, StoreInterface $store): SearchCriteriaInterface + { + $criteria[Filter::ARGUMENT_NAME] = $this->formatMatchFilters($criteria['filters'], $store); + $criteria[Filter::ARGUMENT_NAME][CategoryInterface::KEY_IS_ACTIVE] = ['eq' => 1]; + $criteria[Sort::ARGUMENT_NAME][CategoryInterface::KEY_POSITION] = ['ASC']; + + $searchCriteria = $this->searchCriteriaBuilder->build('categoryList', $criteria); + $pageSize = $criteria['pageSize'] ?? 20; + $currentPage = $criteria['currentPage'] ?? 1; + $searchCriteria->setPageSize($pageSize)->setCurrentPage($currentPage); + + return $searchCriteria; + } + + /** + * Format match filters to behave like fuzzy match + * + * @param array $filters + * @param StoreInterface $store + * @return array + * @throws InputException + */ + private function formatMatchFilters(array $filters, StoreInterface $store): array + { + $minQueryLength = $this->scopeConfig->getValue( + Query::XML_PATH_MIN_QUERY_LENGTH, + ScopeInterface::SCOPE_STORE, + $store + ); + + foreach ($filters as $filter => $condition) { + $conditionType = current(array_keys($condition)); + if ($conditionType === 'match') { + $searchValue = trim(str_replace(self::SPECIAL_CHARACTERS, '', $condition[$conditionType])); + $matchLength = strlen($searchValue); + if ($matchLength < $minQueryLength) { + throw new InputException(__('Invalid match filter. Minimum length is %1.', $minQueryLength)); + } + unset($filters[$filter]['match']); + $filters[$filter]['like'] = '%' . $searchValue . '%'; + } + } + return $filters; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php new file mode 100644 index 0000000000000..c8f9ad5de008f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface as SearchCriteriaCollectionProcessor; + +/** + * Apply pre-defined catalog filtering + * + * {@inheritdoc} + */ +class CatalogProcessor implements CollectionProcessorInterface +{ + /** @var SearchCriteriaCollectionProcessor */ + private $collectionProcessor; + + /** + * @param SearchCriteriaCollectionProcessor $collectionProcessor + */ + public function __construct( + SearchCriteriaCollectionProcessor $collectionProcessor + ) { + $this->collectionProcessor = $collectionProcessor; + } + + /** + * Process collection to add additional joins, attributes, and clauses to a category collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + array $attributeNames, + ContextInterface $context = null + ): Collection { + $this->collectionProcessor->process($searchCriteria, $collection); + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php new file mode 100644 index 0000000000000..5e79064e9acfa --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Add additional joins, attributes, and clauses to a category collection. + */ +interface CollectionProcessorInterface +{ + /** + * Process collection to add additional joins, attributes, and clauses to a category collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + array $attributeNames, + ContextInterface $context = null + ): Collection; +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php new file mode 100644 index 0000000000000..0ab76606f5dce --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Composite collection processor + * + * {@inheritdoc} + */ +class CompositeCollectionProcessor implements CollectionProcessorInterface +{ + /** + * @var CollectionProcessorInterface[] + */ + private $collectionProcessors; + + /** + * @param CollectionProcessorInterface[] $collectionProcessors + */ + public function __construct(array $collectionProcessors = []) + { + $this->collectionProcessors = $collectionProcessors; + } + + /** + * Process collection to add additional joins, attributes, and clauses to a category collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + array $attributeNames, + ContextInterface $context = null + ): Collection { + foreach ($this->collectionProcessors as $collectionProcessor) { + $collection = $collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); + } + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php index eb6708dc48f01..4d7ce13fd23cc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php @@ -70,7 +70,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } try { - $filterResult = $this->categoryFilter->getResult($args, $store); + $filterResult = $this->categoryFilter->getResult($args, $store, [], $context); } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php index f32c5a1f38425..13db03bb2766b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -65,7 +65,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $args['filters']['ids'] = ['eq' => $store->getRootCategoryId()]; } try { - $filterResults = $this->categoryFilter->getResult($args, $store); + $filterResults = $this->categoryFilter->getResult($args, $store, [], $context); $rootCategoryIds = $filterResults['category_ids']; } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php index dbb52f2010930..805571d58d634 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -66,10 +67,12 @@ public function resolve( $returnArray = []; if (isset($requestedFields['minimum_price'])) { - $returnArray['minimum_price'] = $this->getMinimumProductPrice($product, $store); + $returnArray['minimum_price'] = $this->canShowPrice($product) ? + $this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult(); } if (isset($requestedFields['maximum_price'])) { - $returnArray['maximum_price'] = $this->getMaximumProductPrice($product, $store); + $returnArray['maximum_price'] = $this->canShowPrice($product) ? + $this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult(); } return $returnArray; } @@ -130,4 +133,39 @@ private function formatPrice(float $regularPrice, float $finalPrice, StoreInterf 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice), ]; } + + /** + * Check if the product is allowed to show price + * + * @param ProductInterface $product + * @return bool + */ + private function canShowPrice($product): bool + { + if ($product->hasData('can_show_price') && $product->getData('can_show_price') === false) { + return false; + } + + return true; + } + + /** + * Format empty result + * + * @return array + */ + private function formatEmptyResult(): array + { + return [ + 'regular_price' => [ + 'value' => null, + 'currency' => null + ], + 'final_price' => [ + 'value' => null, + 'currency' => null + ], + 'discount' => null + ]; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php index 1b42b0fde2bcb..c80cc3744876f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php @@ -28,7 +28,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var PricingSpecialPrice $specialPrice */ $specialPrice = $product->getPriceInfo()->getPrice(PricingSpecialPrice::PRICE_CODE); - if ($specialPrice->getValue()) { + if ((!$product->hasData('can_show_price') + || ($product->hasData('can_show_price') && $product->getData('can_show_price') === true) + ) + && $specialPrice->getValue()) { return $specialPrice->getValue(); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 1a244b8a10546..ba158fab0120c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -8,14 +8,11 @@ namespace Magento\CatalogGraphQl\Model\Resolver; use Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Filter; -use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Layer\Resolver; use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; @@ -57,17 +54,7 @@ public function resolve( array $value = null, array $args = null ) { - if ($args['currentPage'] < 1) { - throw new GraphQlInputException(__('currentPage value must be greater than 0.')); - } - if ($args['pageSize'] < 1) { - throw new GraphQlInputException(__('pageSize value must be greater than 0.')); - } - if (!isset($args['search']) && !isset($args['filter'])) { - throw new GraphQlInputException( - __("'search' or 'filter' input argument is required.") - ); - } + $this->validateInput($args); $searchResult = $this->searchQuery->getResult($args, $info, $context); @@ -94,4 +81,29 @@ public function resolve( return $data; } + + /** + * Validate input arguments + * + * @param array $args + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + */ + private function validateInput(array $args) + { + if (isset($args['searchAllowed']) && $args['searchAllowed'] === false) { + throw new GraphQlAuthorizationException(__('Product search has been disabled.')); + } + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + if (!isset($args['search']) && !isset($args['filter'])) { + throw new GraphQlInputException( + __("'search' or 'filter' input argument is required.") + ); + } + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php index f709f8cd6eb72..8d584d15fff0e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php @@ -64,7 +64,7 @@ public function apply(Filter $filter, AbstractDb $collection) $collection->addCategoryFilter($category); } - $categoryProductIds = array_unique(array_merge(...$categoryProducts)); + $categoryProductIds = array_unique(array_merge([], ...$categoryProducts)); $collection->addIdFilter($categoryProductIds); return true; } diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 03f9d7ad03f04..fd3a834bff160 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface" type="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CompositeCollectionProcessor"/> + <preference for="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface" type="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CompositeCollectionProcessor"/> <type name="Magento\EavGraphQl\Model\Resolver\Query\Type"> <arguments> <argument name="customTypes" xsi:type="array"> @@ -53,6 +54,13 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CompositeCollectionProcessor"> + <arguments> + <argument name="collectionProcessors" xsi:type="array"> + <item name="catalog" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor\CatalogProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\SearchCriteriaProcessor"> <arguments> <argument name="searchCriteriaApplier" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor</argument> @@ -84,4 +92,9 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor\CatalogProcessor"> + <arguments> + <argument name="collectionProcessor" xsi:type="object">Magento\Eav\Model\Api\SearchCriteria\CollectionProcessor</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 065840426fdd3..f59bc338ced69 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -23,6 +23,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; @@ -44,9 +45,10 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @since 100.0.2 */ -class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity +class Product extends AbstractEntity { - const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; + public const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; + private const HASH_ALGORITHM = 'sha256'; /** * Size of bunch - part of products to save in one step. @@ -766,6 +768,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $linkProcessor; + /** + * @var File + */ + private $fileDriver; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -814,6 +821,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param StatusProcessor|null $statusProcessor * @param StockProcessor|null $stockProcessor * @param LinkProcessor|null $linkProcessor + * @param File|null $fileDriver * @throws LocalizedException * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -866,7 +874,8 @@ public function __construct( ProductRepositoryInterface $productRepository = null, StatusProcessor $statusProcessor = null, StockProcessor $stockProcessor = null, - LinkProcessor $linkProcessor = null + LinkProcessor $linkProcessor = null, + ?File $fileDriver = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -930,6 +939,7 @@ public function __construct( $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); $this->productRepository = $productRepository ?? ObjectManager::getInstance() ->get(ProductRepositoryInterface::class); + $this->fileDriver = $fileDriver ?: ObjectManager::getInstance()->get(File::class); } /** @@ -1570,7 +1580,10 @@ protected function _saveProducts() $uploadedImages = []; $previousType = null; $prevAttributeSet = null; + + $importDir = $this->_mediaDirectory->getAbsolutePath($this->getUploader()->getTmpDir()); $existingImages = $this->getExistingImages($bunch); + $this->addImageHashes($existingImages); foreach ($bunch as $rowNum => $rowData) { // reset category processor's failed categories array @@ -1738,7 +1751,8 @@ protected function _saveProducts() $position = 0; foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $columnImageKey => $columnImage) { - if (!isset($uploadedImages[$columnImage])) { + $uploadedFile = $this->getAlreadyExistedImage($rowExistingImages, $columnImage, $importDir); + if (!$uploadedFile && !isset($uploadedImages[$columnImage])) { $uploadedFile = $this->uploadMediaFiles($columnImage); $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); if ($uploadedFile) { @@ -1753,7 +1767,7 @@ protected function _saveProducts() ProcessingError::ERROR_LEVEL_NOT_CRITICAL ); } - } else { + } elseif (isset($uploadedImages[$columnImage])) { $uploadedFile = $uploadedImages[$columnImage]; } @@ -1782,8 +1796,7 @@ protected function _saveProducts() } if (isset($rowLabels[$column][$columnImageKey]) - && $rowLabels[$column][$columnImageKey] != - $currentFileData['label'] + && $rowLabels[$column][$columnImageKey] !== $currentFileData['label'] ) { $labelsForUpdate[] = [ 'label' => $rowLabels[$column][$columnImageKey], @@ -1792,7 +1805,7 @@ protected function _saveProducts() ]; } } else { - if ($column == self::COL_MEDIA_IMAGE) { + if ($column === self::COL_MEDIA_IMAGE) { $rowData[$column][] = $uploadedFile; } $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ @@ -1908,24 +1921,14 @@ protected function _saveProducts() } } - $this->saveProductEntity( - $entityRowsIn, - $entityRowsUp - )->_saveProductWebsites( - $this->websitesCache - )->_saveProductCategories( - $this->categoriesCache - )->_saveProductTierPrices( - $tierPrices - )->_saveMediaGallery( - $mediaGallery - )->_saveProductAttributes( - $attributes - )->updateMediaGalleryVisibility( - $imagesForChangeVisibility - )->updateMediaGalleryLabels( - $labelsForUpdate - ); + $this->saveProductEntity($entityRowsIn, $entityRowsUp) + ->_saveProductWebsites($this->websitesCache) + ->_saveProductCategories($this->categoriesCache) + ->_saveProductTierPrices($tierPrices) + ->_saveMediaGallery($mediaGallery) + ->_saveProductAttributes($attributes) + ->updateMediaGalleryVisibility($imagesForChangeVisibility) + ->updateMediaGalleryLabels($labelsForUpdate); $this->_eventManager->dispatch( 'catalog_product_import_bunch_save_after', @@ -1939,6 +1942,87 @@ protected function _saveProducts() // phpcs:enable + /** + * Returns image hash by path + * + * @param string $path + * @return string + */ + private function getFileHash(string $path): string + { + return hash_file(self::HASH_ALGORITHM, $path); + } + + /** + * Returns existed image + * + * @param array $imageRow + * @param string $columnImage + * @param string $importDir + * @return string + */ + private function getAlreadyExistedImage(array $imageRow, string $columnImage, string $importDir): string + { + if (filter_var($columnImage, FILTER_VALIDATE_URL)) { + $hash = $this->getFileHash($columnImage); + } else { + $path = $importDir . DS . $columnImage; + $hash = $this->isFileExists($path) ? $this->getFileHash($path) : ''; + } + + return array_reduce( + $imageRow, + function ($exists, $file) use ($hash) { + if (!$exists && isset($file['hash']) && $file['hash'] === $hash) { + return $file['value']; + } + + return $exists; + }, + '' + ); + } + + /** + * Generate hashes for existing images for comparison with newly uploaded images. + * + * @param array $images + * @return void + */ + private function addImageHashes(array &$images): void + { + $productMediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getAbsolutePath(DS . 'catalog' . DS . 'product'); + + foreach ($images as $storeId => $skus) { + foreach ($skus as $sku => $files) { + foreach ($files as $path => $file) { + if ($this->fileDriver->isExists($productMediaPath . $file['value'])) { + $fileName = $productMediaPath . $file['value']; + $images[$storeId][$sku][$path]['hash'] = $this->getFileHash($fileName); + } + } + } + } + } + + /** + * Is file exists + * + * @param string $path + * @return bool + */ + private function isFileExists(string $path): bool + { + try { + $fileExists = $this->fileDriver->isExists($path); + } catch (\Exception $exception) { + $fileExists = false; + } + + return $fileExists; + } + /** * Clears entries from Image Set and Row Data marked as no_selection * @@ -1950,9 +2034,8 @@ private function clearNoSelectionImages($rowImages, $rowData) { foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $key => $image) { - if ($image == 'no_selection') { - unset($rowImages[$column][$key]); - unset($rowData[$column]); + if ($image === 'no_selection') { + unset($rowImages[$column][$key], $rowData[$column]); } } } @@ -2095,6 +2178,21 @@ protected function _saveProductTierPrices(array $tierPriceData) return $this; } + /** + * Returns the import directory if specified or a default import directory (media/import). + * + * @return string + */ + private function getImportDir(): string + { + $dirConfig = DirectoryList::getDefaultConfig(); + $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + + return empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]) + ? $dirAddon . DS . $this->_mediaDirectory->getRelativePath('import') + : $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; + } + /** * Returns an object for upload a media files * @@ -2111,11 +2209,7 @@ protected function _getUploader() $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; - if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) { - $tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; - } else { - $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import'); - } + $tmpPath = $this->getImportDir(); if (!$fileUploader->setTmpDir($tmpPath)) { throw new LocalizedException( diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index e12fc726f1056..7757a2ba5eb7d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -421,7 +421,7 @@ public function __construct( $this->_initMessageTemplates(); - $this->_initProductsSku()->_initOldCustomOptions(); + $this->_initProductsSku(); } /** @@ -606,6 +606,9 @@ protected function _initOldCustomOptions() 'option_title.store_id = ?', $storeId ); + if (!empty($this->_newOptionsOldData)) { + $this->_optionCollection->addProductToFilter(array_keys($this->_newOptionsOldData)); + } $this->_byPagesIterator->iterate($this->_optionCollection, $this->_pageSize, [$addCustomOptions]); } @@ -614,6 +617,20 @@ protected function _initOldCustomOptions() return $this; } + /** + * Get existing custom options data + * + * @return array + */ + private function getOldCustomOptions(): array + { + if ($this->_oldCustomOptions === null) { + $this->_initOldCustomOptions(); + } + + return $this->_oldCustomOptions; + } + /** * Imported entity type code getter * @@ -717,9 +734,9 @@ protected function _findOldOptionsWithTheSameTitles() $errorRows = []; foreach ($this->_newOptionsOldData as $productId => $options) { foreach ($options as $outerData) { - if (isset($this->_oldCustomOptions[$productId])) { + if (isset($this->getOldCustomOptions()[$productId])) { $optionsCount = 0; - foreach ($this->_oldCustomOptions[$productId] as $innerData) { + foreach ($this->getOldCustomOptions()[$productId] as $innerData) { if (count($outerData['titles']) == count($innerData['titles'])) { $outerTitles = $outerData['titles']; $innerTitles = $innerData['titles']; @@ -753,8 +770,8 @@ protected function _findNewOldOptionsTypeMismatch() $errorRows = []; foreach ($this->_newOptionsOldData as $productId => $options) { foreach ($options as $outerData) { - if (isset($this->_oldCustomOptions[$productId])) { - foreach ($this->_oldCustomOptions[$productId] as $innerData) { + if (isset($this->getOldCustomOptions()[$productId])) { + foreach ($this->getOldCustomOptions()[$productId] as $innerData) { if (count($outerData['titles']) == count($innerData['titles'])) { $outerTitles = $outerData['titles']; $innerTitles = $innerData['titles']; @@ -784,9 +801,9 @@ protected function _findNewOldOptionsTypeMismatch() protected function _findExistingOptionId(array $newOptionData, array $newOptionTitles) { $productId = $newOptionData['product_id']; - if (isset($this->_oldCustomOptions[$productId])) { + if (isset($this->getOldCustomOptions()[$productId])) { ksort($newOptionTitles); - $existingOptions = $this->_oldCustomOptions[$productId]; + $existingOptions = $this->getOldCustomOptions()[$productId]; foreach ($existingOptions as $optionId => $optionData) { if ($optionData['type'] == $newOptionData['type'] && $optionData['titles'][Store::DEFAULT_STORE_ID] == $newOptionTitles[Store::DEFAULT_STORE_ID] diff --git a/app/code/Magento/CatalogInventory/Model/StockIndex.php b/app/code/Magento/CatalogInventory/Model/StockIndex.php index ad0cff43c6ac9..6b659073485ad 100644 --- a/app/code/Magento/CatalogInventory/Model/StockIndex.php +++ b/app/code/Magento/CatalogInventory/Model/StockIndex.php @@ -169,11 +169,11 @@ protected function processChildren( $requiredChildrenIds = $typeInstance->getChildrenIds($productId, true); if ($requiredChildrenIds) { - $childrenIds = [[]]; + $childrenIds = []; foreach ($requiredChildrenIds as $groupedChildrenIds) { $childrenIds[] = $groupedChildrenIds; } - $childrenIds = array_merge(...$childrenIds); + $childrenIds = array_merge([], ...$childrenIds); $childrenWebsites = $this->productWebsite->getWebsites($childrenIds); foreach ($websitesWithStores as $websiteId => $storeId) { @@ -232,13 +232,13 @@ protected function getWebsitesWithDefaultStores($websiteId = null) */ protected function processParents($productId, $websiteId) { - $parentIds = [[]]; + $parentIds = []; foreach ($this->getProductTypeInstances() as $typeInstance) { /* @var ProductType\AbstractType $typeInstance */ $parentIds[] = $typeInstance->getParentIdsByChild($productId); } - $parentIds = array_merge(...$parentIds); + $parentIds = array_merge([], ...$parentIds); if (empty($parentIds)) { return; diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index f2e8e54d34665..2d92192368960 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -895,4 +895,14 @@ public function getIdentities() { return ['price']; } + + /** + * Clear price rules cache. + * + * @return void; + */ + public function clearPriceRulesData(): void + { + self::$_priceRulesData = []; + } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml index 8103e6b115950..ece8dc4bacf28 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductAndFixedMethodTest"> + <test name="ApplyCatalogRuleForSimpleProductAndFixedMethodTest" deprecated="Use StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> - <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> + <title value="DEPRECATED. Admin should be able to apply the catalog price rule for simple product with custom options"/> <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> <severity value="CRITICAL"/> <testCaseId value="MC-14771"/> <group value="CatalogRule"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead</issueId> + </skip> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index d9b62ef8fc913..45e97f179a11f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest"> + <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest" deprecated="Use StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> - <title value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> + <title value="Deprecated. Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> <description value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> <severity value="CRITICAL"/> <testCaseId value="MC-14769"/> <group value="CatalogRule"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead</issueId> + </skip> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml new file mode 100644 index 0000000000000..c127f19db3749 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -0,0 +1,113 @@ +<?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="StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Apply catalog price rule"/> + <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> + <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28347"/> + <group value="catalogRule"/> + <group value="mtf_migrated"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create Simple Product --> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + + <!-- Update all products to have custom options --> + <updateData createDataKey="createProduct" entity="productWithFixedOptions" stepKey="updateProductWithOptions"/> + + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Clear all catalog price rules and reindex before test --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> + </before> + <after> + <!-- Delete products and category --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete the catalog price rule --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfter"/> + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- 1. Begin creating a new catalog price rule --> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRulePage"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForCatalogPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> + </actionGroup> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="fillConditionsForCatalogPriceRule"> + <argument name="conditionValue" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="by_fixed"/> + <argument name="discountAmount" value="12.3"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + + <!-- Navigate to category on store front --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + + <!-- Check product name on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="assertStorefrontProductName"> + <argument name="productInfo" value="$createProduct.name$"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Check product price on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="assertStorefrontProductPrice"> + <argument name="productInfo" value="$44.48"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Check product regular price on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="assertStorefrontProductRegularPrice"> + <argument name="productInfo" value="$56.78"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Navigate to product on store front --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage"> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> + <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices"> + <argument name="customOption" value="ProductOptionRadioButton2"/> + <argument name="customOptionValue" value="ProductOptionValueRadioButtons1"/> + <argument name="productPrice" value="$156.77"/> + <argument name="productFinalPrice" value="$144.47"/> + </actionGroup> + + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + + <!-- Assert sub total on mini shopping cart --> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$144.47"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml new file mode 100644 index 0000000000000..a616a7ab172f1 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -0,0 +1,183 @@ +<?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="StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Apply catalog price rule"/> + <title value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> + <description value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28345"/> + <group value="catalogRule"/> + <group value="mtf_migrated"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct3"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + + <!-- Update all products to have custom options --> + <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProduc1tWithOptions"/> + <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProduct2WithOptions"/> + <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProduct3WithOptions"/> + + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Clear all catalog price rules before test --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> + </before> + <after> + <!-- Delete products and category --> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete the catalog price rule --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfterTest"/> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- 1. Begin creating a new catalog price rule --> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRulePage"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForCatalogPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> + </actionGroup> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="fillConditionsForCatalogPriceRule"> + <argument name="conditionValue" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="by_percent"/> + <argument name="discountAmount" value="10"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + + <!-- Navigate to category on store front --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + + <!-- Check product 1 price on store front category page --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct1Price"> + <argument name="productName" value="$createProduct1.name$"/> + <argument name="productPrice" value="$51.10"/> + </actionGroup> + + <!-- Check product 1 regular price on store front category page --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct1RegularPrice"> + <argument name="productName" value="$createProduct1.name$"/> + <argument name="productPrice" value="$56.78"/> + </actionGroup> + + <!-- Check product 2 price on store front category page --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct2Price"> + <argument name="productName" value="$createProduct2.name$"/> + <argument name="productPrice" value="$51.10"/> + </actionGroup> + + <!-- Check product 2 regular price on store front category page --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct2RegularPrice"> + <argument name="productName" value="$createProduct2.name$"/> + <argument name="productPrice" value="$56.78"/> + </actionGroup> + + <!-- Check product 3 price on store front category page --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct3Price"> + <argument name="productName" value="$createProduct3.name$"/> + <argument name="productPrice" value="$51.10"/> + </actionGroup> + + <!-- Check product 3 regular price on store front category page --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct3RegularPrice"> + <argument name="productName" value="$createProduct3.name$"/> + <argument name="productPrice" value="$56.78"/> + </actionGroup> + + <!-- Navigate to product 1 on store front --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProduct1Page"> + <argument name="productUrlKey" value="$createProduct1.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Assert regular and special price for product 1 after selecting ProductOptionValueDropdown1 --> + <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertProduct1Prices"> + <argument name="customOption" value="{{ProductOptionValueDropdown1.title}} +$0.01"/> + <argument name="productPrice" value="$56.79"/> + <argument name="productFinalPrice" value="$51.11"/> + </actionGroup> + + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProduct1Page"> + <argument name="productName" value="$createProduct1.name$"/> + </actionGroup> + + <!-- Navigate to product 2 on store front --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProduct2Page"> + <argument name="productUrlKey" value="$createProduct2.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Assert regular and special price for product 2 after selecting ProductOptionValueDropdown3 --> + <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertProduct2Prices"> + <argument name="customOption" value="{{ProductOptionValueDropdown3.title}} +$5.11"/> + <argument name="productPrice" value="$62.46"/> + <argument name="productFinalPrice" value="$56.21"/> + </actionGroup> + + <!-- Add product 2 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProduct2Page"> + <argument name="productName" value="$createProduct2.name$"/> + </actionGroup> + + <!-- Navigate to product 3 on store front --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProduct3Page"> + <argument name="productUrlKey" value="$createProduct3.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Add product 3 to cart with no custom option --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProduct3Page"> + <argument name="productName" value="$createProduct3.name$"/> + </actionGroup> + + <!-- Assert subtotal on mini shopping cart --> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$158.42"/> + </actionGroup> + + <!-- Navigate to checkout shipping page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="onCheckout"/> + + <!-- Fill Shipping information --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillOrderShippingInfo"> + <argument name="customerVar" value="Simple_US_Customer"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + </actionGroup> + + <!-- Verify order summary on payment page --> + <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummaryActionGroup"> + <argument name="orderSummarySubTotal" value="$158.42"/> + <argument name="orderSummaryShippingTotal" value="$15.00"/> + <argument name="orderSummaryTotal" value="$173.42"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 264c55ba43390..bebf6ce5302d6 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -35,7 +35,7 @@ <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyFirstPriceRule"/> <!-- Perform reindex --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value="catalogrule_rule"/> + <argument name="indices" value=""/> </actionGroup> </before> diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index e226bdc6900e6..f72516d28c46f 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -12,6 +12,7 @@ use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\Store\Model\StoreDimensionProvider; use Magento\Indexer\Model\ProcessManager; @@ -33,6 +34,11 @@ class Fulltext implements */ const INDEXER_ID = 'catalogsearch_fulltext'; + /** + * Default batch size + */ + private const BATCH_SIZE = 100; + /** * @var array index structure */ @@ -77,6 +83,11 @@ class Fulltext implements */ private $processManager; + /** + * @var int + */ + private $batchSize; + /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -86,6 +97,7 @@ class Fulltext implements * @param DimensionProviderInterface $dimensionProvider * @param array $data * @param ProcessManager $processManager + * @param int|null $batchSize * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -96,7 +108,8 @@ public function __construct( StateFactory $indexScopeStateFactory, DimensionProviderInterface $dimensionProvider, array $data, - ProcessManager $processManager = null + ProcessManager $processManager = null, + ?int $batchSize = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); $this->indexerHandlerFactory = $indexerHandlerFactory; @@ -106,6 +119,7 @@ public function __construct( $this->indexScopeState = ObjectManager::getInstance()->get(State::class); $this->dimensionProvider = $dimensionProvider; $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class); + $this->batchSize = $batchSize ?? self::BATCH_SIZE; } /** @@ -148,13 +162,42 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = } else { // internal implementation works only with array $entityIds = iterator_to_array($entityIds); - $productIds = array_unique( - array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) - ); - if ($saveHandler->isAvailable($dimensions)) { - $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); - $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + $currentBatch = []; + $i = 0; + + foreach ($entityIds as $entityId) { + $currentBatch[] = $entityId; + if (++$i === $this->batchSize) { + $this->processBatch($saveHandler, $dimensions, $currentBatch); + $i = 0; + $currentBatch = []; + } } + if (!empty($currentBatch)) { + $this->processBatch($saveHandler, $dimensions, $currentBatch); + } + } + } + + /** + * Process batch + * + * @param IndexerInterface $saveHandler + * @param array $dimensions + * @param array $entityIds + */ + private function processBatch( + IndexerInterface $saveHandler, + array $dimensions, + array $entityIds + ) : void { + $storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue(); + $productIds = array_unique( + array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) + ); + if ($saveHandler->isAvailable($dimensions)) { + $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); } } diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index b1aecc6885bf0..080af5daa0322 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -70,7 +70,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) $label = $this->getOptionText($value); $labels[] = is_array($label) ? $label : [$label]; } - $label = implode(',', array_unique(array_merge(...$labels))); + $label = implode(',', array_unique(array_merge([], ...$labels))); $this->getLayer() ->getState() ->addFilter($this->_createItem($label, $attributeValue)); diff --git a/app/code/Magento/CatalogSearch/Model/Search/Category.php b/app/code/Magento/CatalogSearch/Model/Search/Category.php new file mode 100644 index 0000000000000..200bc81526e66 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/Category.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Model\Search; + +use Magento\Backend\Helper\Data; +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; +use Magento\Framework\DataObject; +use Magento\Framework\Stdlib\StringUtils; + +/** + * Search model for backend search + */ +class Category extends DataObject +{ + /** + * @var Data + */ + private $adminhtmlData = null; + + /** + * @var CategoryListInterface + */ + private $categoryRepository; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var SearchCriteriaBuilderFactory + */ + private $searchCriteriaBuilderFactory; + + /** + * @var StringUtils + */ + private $string; + + /** + * @var SearchCriteriaBuilder|void + */ + private $searchCriteriaBuilder; + + /** + * @param Data $adminhtmlData + * @param CategoryListInterface $categoryRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory + * @param FilterBuilder $filterBuilder + * @param StringUtils $string + */ + public function __construct( + Data $adminhtmlData, + CategoryListInterface $categoryRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, + FilterBuilder $filterBuilder, + StringUtils $string + ) { + $this->adminhtmlData = $adminhtmlData; + $this->categoryRepository = $categoryRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->filterBuilder = $filterBuilder; + $this->string = $string; + } + + /** + * Load search results + * + * @return $this + */ + public function load() + { + $result = []; + if (!$this->hasStart() || !$this->hasLimit() || !$this->hasQuery()) { + $this->setResults($result); + return $this; + } + $this->searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); + $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); + $this->searchCriteriaBuilder->setPageSize($this->getLimit()); + $searchFields = ['name']; + + $filters = []; + foreach ($searchFields as $field) { + $filters[] = $this->filterBuilder + ->setField($field) + ->setConditionType('like') + ->setValue(sprintf("%%%s%%", $this->getQuery())) + ->create(); + } + $this->searchCriteriaBuilder->addFilters($filters); + + $searchCriteria = $this->searchCriteriaBuilder->create(); + $searchResults = $this->categoryRepository->getList($searchCriteria); + + foreach ($searchResults->getItems() as $category) { + $description = $category->getDescription() ? strip_tags($category->getDescription()) : ''; + $result[] = [ + 'id' => sprintf('category/1/%d', $category->getId()), + 'type' => __('Category'), + 'name' => $category->getName(), + 'description' => $this->string->substr($description, 0, 30), + 'url' => $this->adminhtmlData->getUrl('catalog/category/edit', ['id' => $category->getId()]), + ]; + } + $this->setResults($result); + return $this; + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml new file mode 100644 index 0000000000000..b4ee0144657af --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml @@ -0,0 +1,52 @@ +<?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="AdminCategorySearchTest"> + <annotations> + <features value="Search Category"/> + <stories value="Search categories in admin panel"/> + <title value="Search for categories"/> + <description value="Global search in backend can search into Categories."/> + <severity value="MINOR"/> + <group value="Search"/> + <testCaseId value="MC-37809"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Create Simple Category --> + <createData entity="SimpleSubCategory" stepKey="createSimpleCategory"/> + </before> + <after> + <!-- Delete created category --> + <deleteData createDataKey="createSimpleCategory" stepKey="deleteCreatedCategory"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Add created category name in the search field--> + <actionGroup ref="AdminSetGlobalSearchValueActionGroup" stepKey="setSearch"> + <argument name="textSearch" value="$$createSimpleCategory.name$$"/> + </actionGroup> + + <!-- Wait for suggested results--> + <waitForElementVisible selector="{{AdminGlobalSearchSection.globalSearchSuggestedCategoryText}}" stepKey="waitForSuggestions"/> + + <!-- Click on suggested result in category URL--> + <click selector="{{AdminGlobalSearchSection.globalSearchSuggestedCategoryLink}}" stepKey="openCategory"/> + + <!-- Wait for suggested results--> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Loaded page should be edit page of created category --> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="$$createSimpleCategory.name$$" stepKey="checkCategoryName"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php index d07b15dbfd5d9..241f00de825d9 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php @@ -144,7 +144,7 @@ private function setupDataProvider($stores) $dimension = $this->getMockBuilder(Dimension::class) ->disableOriginalConstructor() ->getMock(); - $dimension->expects($this->once()) + $dimension->expects($this->any()) ->method('getValue') ->willReturn($storeId); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/CategoryTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/CategoryTest.php index 6351fb5f5a05b..8ce6f172e0d64 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/CategoryTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/CategoryTest.php @@ -94,7 +94,7 @@ protected function setUp(): void ->setMethods(['getState', 'getProductCollection']) ->getMock(); - $this->fulltextCollection = $this->fulltextCollection = $this->getMockBuilder( + $this->fulltextCollection = $this->getMockBuilder( Collection::class ) ->disableOriginalConstructor() diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/DecimalTest.php index 40703650c7bea..dc85b68abde71 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/DecimalTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/DecimalTest.php @@ -84,7 +84,7 @@ protected function setUp(): void ->method('create') ->willReturn($this->filterItem); - $this->fulltextCollection = $this->fulltextCollection = $this->getMockBuilder( + $this->fulltextCollection = $this->getMockBuilder( Collection::class ) ->disableOriginalConstructor() diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php index bb14ad2da9a66..8de684fcc17bf 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php @@ -99,7 +99,7 @@ protected function setUp(): void ->method('getState') ->willReturn($this->state); - $this->fulltextCollection = $this->fulltextCollection = $this->getMockBuilder( + $this->fulltextCollection = $this->getMockBuilder( Collection::class ) ->disableOriginalConstructor() diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 6ff9119e78c2a..f8e2a262d73ca 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -66,6 +66,10 @@ <item name="class" xsi:type="string">Magento\CatalogSearch\Model\Search\Catalog</item> <item name="acl" xsi:type="string">Magento_Catalog::catalog</item> </item> + <item name="categories" xsi:type="array"> + <item name="class" xsi:type="string">Magento\CatalogSearch\Model\Search\Category</item> + <item name="acl" xsi:type="string">Magento_Catalog::categories</item> + </item> </argument> </arguments> </type> diff --git a/app/code/Magento/CatalogSearch/etc/mview.xml b/app/code/Magento/CatalogSearch/etc/mview.xml index e5580d86d1ef8..494b97a816886 100644 --- a/app/code/Magento/CatalogSearch/etc/mview.xml +++ b/app/code/Magento/CatalogSearch/etc/mview.xml @@ -19,6 +19,7 @@ <table name="catalog_product_bundle_selection" entity_column="parent_product_id" /> <table name="catalog_product_super_link" entity_column="product_id" /> <table name="catalog_product_link" entity_column="product_id" /> + <table name="catalog_category_product" entity_column="product_id" /> </subscriptions> </view> </config> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php index 308b82e38c43a..50875b1a418d0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php @@ -121,7 +121,7 @@ public function afterSave( */ protected function generateProductUrls($websiteId, $originWebsiteId) { - $urls = [[]]; + $urls = []; $websiteIds = $websiteId != $originWebsiteId ? [$websiteId, $originWebsiteId] : [$websiteId]; @@ -136,7 +136,7 @@ protected function generateProductUrls($websiteId, $originWebsiteId) $urls[] = $this->productUrlRewriteGenerator->generate($product); } - return array_merge(...$urls); + return array_merge([], ...$urls); } /** @@ -148,7 +148,7 @@ protected function generateProductUrls($websiteId, $originWebsiteId) */ protected function generateCategoryUrls($rootCategoryId, $storeIds) { - $urls = [[]]; + $urls = []; $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true); foreach ($categories as $category) { /** @var \Magento\Catalog\Model\Category $category */ @@ -157,6 +157,6 @@ protected function generateCategoryUrls($rootCategoryId, $storeIds) $urls[] = $this->categoryUrlRewriteGenerator->generate($category); } - return array_merge(...$urls); + return array_merge([], ...$urls); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 9d26184e2c2d4..7bf1da2b814e3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -6,6 +6,7 @@ namespace Magento\CatalogUrlRewrite\Model; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator; @@ -15,12 +16,14 @@ use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\UrlRewrite\Model\MergeDataProviderFactory; /** - * Class ProductScopeRewriteGenerator + * Generates Product/Category URLs for different scopes + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductScopeRewriteGenerator @@ -174,7 +177,6 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ continue; } - // category should be loaded per appropriate store if category's URL key has been changed $categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category); } @@ -240,9 +242,15 @@ public function isCategoryProperForGenerating(Category $category, $storeId) * Checks if URL key has been changed for provided category and returns reloaded category, * in other case - returns provided category. * + * Category should be loaded per appropriate store at all times. This is because whilst the URL key on the + * category in focus might be unchanged, parent category URL keys might be. If the category store ID + * and passed store ID are the same then return current category as it is correct but may have changed in memory + * * @param int $storeId * @param Category $category - * @return Category + * + * @return CategoryInterface + * @throws NoSuchEntityException */ private function getCategoryWithOverriddenUrlKey($storeId, Category $category) { @@ -252,9 +260,10 @@ private function getCategoryWithOverriddenUrlKey($storeId, Category $category) Category::ENTITY ); - if (!$isUrlKeyOverridden) { + if (!$isUrlKeyOverridden && $storeId === $category->getStoreId()) { return $category; } + return $this->categoryRepository->get($category->getEntityId(), $storeId); } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index b1dfa79373a05..b467771408ec0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -37,8 +37,6 @@ use RuntimeException; /** - * Class AfterImportDataObserver - * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -459,8 +457,7 @@ private function categoriesUrlRewriteGenerate(): array } } } - $result = !empty($urls) ? array_merge(...$urls) : []; - return $result; + return array_merge([], ...$urls); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php new file mode 100644 index 0000000000000..b7e4ef12f76d2 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Model; + +use Magento\Catalog\Model\Category; +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; + +class CategorySetSaveRewriteHistory +{ + private const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; + + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Add 'save_rewrites_history' param to the category for list + * + * @param CategoryUrlRewriteGenerator $subject + * @param Category $category + * @param bool $overrideStoreUrls + * @param int|null $rootCategoryId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeGenerate( + CategoryUrlRewriteGenerator $subject, + Category $category, + bool $overrideStoreUrls = false, + ?int $rootCategoryId = null + ) { + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isCustomAttributesExists($requestBodyParams, CategoryUrlRewriteGenerator::ENTITY_TYPE)) { + foreach ($requestBodyParams[CategoryUrlRewriteGenerator::ENTITY_TYPE]['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { + $category->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); + } + } + } + + return [$category, $overrideStoreUrls, $rootCategoryId]; + } + + /** + * Check is any custom options exists in data + * + * @param array $requestBodyParams + * @param string $entityCode + * @return bool + */ + private function isCustomAttributesExists(array $requestBodyParams, string $entityCode): bool + { + return !empty($requestBodyParams[$entityCode]['custom_attributes']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml new file mode 100644 index 0000000000000..776b5b9b70f33 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml @@ -0,0 +1,80 @@ +<?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="StorefrontCategoryUrlRewriteDifferentStoreTest"> + <annotations> + <stories value="Url rewrites"/> + <title value="Verify url category for different store view."/> + <description value="Verify url category for different store view, after change ukr_key category for one of them store view."/> + <features value="CatalogUrlRewrite"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-38053"/> + </annotations> + <before> + <magentoCLI command="config:set catalog/seo/product_use_categories 1" stepKey="setEnableUseCategoriesPath"/> + <createData entity="SubCategory" stepKey="rootCategory"/> + <createData entity="SimpleSubCategoryDifferentUrlStore" stepKey="subCategory"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="subCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBefore"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set catalog/seo/product_use_categories 0" stepKey="setEnableUseCategoriesPath"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteSubCategory" createDataKey="subCategory"/> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="navigateToCreatedSubCategory"> + <argument name="Category" value="$$subCategory$$"/> + </actionGroup> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchCustomStoreViewForSubCategory"> + <argument name="storeView" value="customStoreFR.name"/> + </actionGroup> + <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyForSubCategoryCustomStore"> + <argument name="value" value="{{SimpleSubCategoryDifferentUrlStore.url_key_custom_store}}"/> + </actionGroup> + + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="goToCategoryC"> + <argument name="categoryName" value="$$rootCategory.name$$"/> + <argument name="subCategoryName" value="$$subCategory.name$$"/> + </actionGroup> + + <click selector="{{StorefrontCategoryProductSection.ProductInfoByName($$createProduct.name$$)}}" stepKey="navigateToCreateProduct"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> + <argument name="storeView" value="customStoreFR" /> + </actionGroup> + + <grabFromCurrentUrl stepKey="grabUrl"/> + <assertStringContainsString stepKey="assertUrl"> + <expectedResult type="string">{{SimpleSubCategoryDifferentUrlStore.url_key_custom_store}}</expectedResult> + <actualResult type="string">{$grabUrl}</actualResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php index 7b18461a580fe..d9c6adce9661f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Model; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Model\ObjectRegistry; @@ -69,6 +70,9 @@ class ProductScopeRewriteGeneratorTest extends TestCase /** @var ScopeConfigInterface|MockObject */ private $configMock; + /** @var CategoryRepositoryInterface|MockObject */ + private $categoryRepositoryMock; + protected function setUp(): void { $this->serializer = $this->createMock(Json::class); @@ -126,6 +130,8 @@ function ($value) { $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) ->getMock(); + $this->categoryRepositoryMock = $this->getMockForAbstractClass(CategoryRepositoryInterface::class); + $this->productScopeGenerator = (new ObjectManager($this))->getObject( ProductScopeRewriteGenerator::class, [ @@ -137,7 +143,8 @@ function ($value) { 'storeViewService' => $this->storeViewService, 'storeManager' => $this->storeManager, 'mergeDataProviderFactory' => $mergeDataProviderFactory, - 'config' => $this->configMock + 'config' => $this->configMock, + 'categoryRepository' => $this->categoryRepositoryMock ] ); $this->categoryMock = $this->getMockBuilder(Category::class) @@ -215,6 +222,8 @@ public function testGenerationForSpecificStore() $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') ->willReturn([]); + $this->categoryRepositoryMock->expects($this->once())->method('get')->willReturn($this->categoryMock); + $this->assertEquals( ['category-1_1' => $canonical], $this->productScopeGenerator->generateForSpecificStoreView(1, [$this->categoryMock], $product, 1) diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml index 9c5186a5ec0ac..9348b03d17270 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml @@ -9,4 +9,7 @@ <type name="Magento\Webapi\Controller\Rest\InputParamsResolver"> <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver" sortOrder="1" disabled="false" /> </type> + <type name="Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator"> + <plugin name="category_set_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Model\CategorySetSaveRewriteHistory" disabled="false" /> + </type> </config> diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 9934cc9ad106a..7e6693ce68ef9 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -337,9 +337,13 @@ public function createCollection() $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); + /** + * Change sorting attribute to entity_id because created_at can be the same for products fastly created + * one by one and sorting by created_at is indeterministic in this case. + */ $collection = $this->_addProductAttributesAndPrices($collection) ->addStoreFilter() - ->addAttributeToSort('created_at', 'desc') + ->addAttributeToSort('entity_id', 'desc') ->setPageSize($this->getPageSize()) ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1)); @@ -506,7 +510,7 @@ public function getPagerHtml() */ public function getIdentities() { - $identities = [[]]; + $identities = []; if ($this->getProductCollection()) { foreach ($this->getProductCollection() as $product) { if ($product instanceof IdentityInterface) { @@ -514,7 +518,7 @@ public function getIdentities() } } } - $identities = array_merge(...$identities); + $identities = array_merge([], ...$identities); return $identities ?: [Product::CACHE_TAG]; } diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml new file mode 100644 index 0000000000000..1d5e369d50e1d --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml @@ -0,0 +1,85 @@ +<?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="CatalogProductListCheckWidgetOrderTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="Product list widget"/> + <title value="Checking order of products in the 'catalog Products List' widget"/> + <description value="Check that products are ordered with recently added products first"/> + <severity value="MAJOR"/> + <testCaseId value="MC-27616"/> + <useCaseId value="MC-5905"/> + <group value="catalogWidget"/> + <group value="catalog"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">20</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">30</field> + </createData> + <createData entity="_defaultCmsPage" stepKey="createPreReqPage"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + </before> + <after> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Open created cms page--> + <actionGroup ref="AdminOpenCmsPageActionGroup" stepKey="openEditPage"> + <argument name="page_id" value="$createPreReqPage.id$"/> + </actionGroup> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <!--Add widget to cms page--> + <waitForElementVisible selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="waitInsertWidgetIconVisible"/> + <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> + <waitForElementVisible selector="{{WidgetSection.WidgetType}}" stepKey="waitForWidgetTypeSelectorVisible"/> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> + <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParamBtnVisible"/> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> + <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParamVisible"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> + <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="waitForCategoryVisible" /> + <click selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="selectCategory" /> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> + <!--Save cms page and go to Storefront--> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="saveCmsPage"/> + <actionGroup ref="NavigateToStorefrontForCreatedPageActionGroup" stepKey="navigateToTheStoreFront1"> + <argument name="page" value="$createPreReqPage.identifier$"/> + </actionGroup> + <!--Check order of products: recently added first--> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="waitForThirdProductVisible"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="seeElementByName1"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="seeElementByName2"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="seeElementByName3"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml index fd87d58e47125..5bd9981a50236 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml @@ -8,18 +8,18 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CatalogProductListWidgetOrderTest"> + <test name="CatalogProductListWidgetOrderTest" deprecated="Use CatalogProductListCheckWidgetOrderTest instead"> <annotations> <features value="CatalogWidget"/> <stories value="MC-5905: Wrong sorting on Products component"/> - <title value="Checking order of products in the 'catalog Products List' widget"/> + <title value="Deprecated. Checking order of products in the 'catalog Products List' widget"/> <description value="Check that products are ordered with recently added products first"/> <severity value="MAJOR"/> <testCaseId value="MC-13794"/> <group value="CatalogWidget"/> <group value="WYSIWYGDisabled"/> <skip> - <issueId value="MC-13923"/> + <issueId value="DEPRECATED">Use CatalogProductListCheckWidgetOrderTest instead</issueId> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php index 3feb44ee23acf..87a76ab801a1f 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php @@ -314,7 +314,7 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP $collection->expects($this->once())->method('addAttributeToSelect')->willReturnSelf(); $collection->expects($this->once())->method('addUrlRewrite')->willReturnSelf(); $collection->expects($this->once())->method('addStoreFilter')->willReturnSelf(); - $collection->expects($this->once())->method('addAttributeToSort')->with('created_at', 'desc')->willReturnSelf(); + $collection->expects($this->once())->method('addAttributeToSort')->with('entity_id', 'desc')->willReturnSelf(); $collection->expects($this->once())->method('setPageSize')->with($expectedPageSize)->willReturnSelf(); $collection->expects($this->once())->method('setCurPage')->willReturnSelf(); $collection->expects($this->once())->method('distinct')->willReturnSelf(); diff --git a/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php new file mode 100644 index 0000000000000..04f3d9aa37722 --- /dev/null +++ b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Psr\Log\LoggerInterface; + +/** + * Process checkout data redirected from origin store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPostprocessor implements RedirectDataPostprocessorInterface +{ + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param CartRepositoryInterface $quoteRepository + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + * @param LoggerInterface $logger + */ + public function __construct( + CartRepositoryInterface $quoteRepository, + CustomerSession $customerSession, + CheckoutSession $checkoutSession, + LoggerInterface $logger + ) { + $this->quoteRepository = $quoteRepository; + $this->customerSession = $customerSession; + $this->checkoutSession = $checkoutSession; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): void + { + if (!empty($data['quote_id']) + && $this->checkoutSession->getQuoteId() === null + && !$this->customerSession->isLoggedIn() + ) { + try { + $quote = $this->quoteRepository->get((int) $data['quote_id']); + if ($quote + && $quote->getIsActive() + && in_array($context->getTargetStore()->getId(), $quote->getSharedStoreIds()) + ) { + $this->checkoutSession->setQuoteId($quote->getId()); + } + } catch (\Throwable $e) { + $this->logger->error($e); + } + } + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + // Update quote items so that product names are updated for current store view + $quote->setStoreId($context->getTargetStore()->getId()); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + } +} diff --git a/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php new file mode 100644 index 0000000000000..6047bb8bcad46 --- /dev/null +++ b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; + +/** + * Collect checkout data to be redirected to target store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface +{ + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + */ + public function __construct( + CustomerSession $customerSession, + CheckoutSession $checkoutSession + ) { + $this->customerSession = $customerSession; + $this->checkoutSession = $checkoutSession; + } + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): array + { + if ($this->checkoutSession->getQuoteId() && !$this->customerSession->isLoggedIn()) { + $quote = $this->checkoutSession->getQuote(); + if ($quote + && $quote->getIsActive() + && in_array($context->getTargetStore()->getId(), $quote->getSharedStoreIds()) + ) { + $data['quote_id'] = (int) $quote->getId(); + } + } + return $data; + } +} diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml index bbad2579a47d2..4cc0ac3bc3a06 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml @@ -27,9 +27,7 @@ <scrollTo selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="scrollToSummary"/> <see userInput="{{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> <see userInput="({{shippingMethod}})" selector="{{CheckoutCartSummarySection.shippingMethod}}" stepKey="assertShippingMethod"/> - <reloadPage stepKey="reloadPage" after="assertShippingMethod" /> - <waitForPageLoad stepKey="WaitForPageLoaded" after="reloadPage" /> - <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping" after="WaitForPageLoaded"/> - <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal" after="assertShipping"/> + <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping"/> + <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index de71fc3f8ad0e..d555079f48475 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -34,7 +34,7 @@ <element name="shippingMethodLabel" type="text" selector="#co-shipping-method-form dl dt span"/> <element name="methodName" type="text" selector="#co-shipping-method-form label"/> <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> - <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> + <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true" timeout="30"/> <element name="estimateShippingAndTaxForm" type="block" selector="#shipping-zip-form"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml new file mode 100644 index 0000000000000..e6a5f37c764fe --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml @@ -0,0 +1,75 @@ +<?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="CheckoutDifferentDefaultCountryPerStoreTest"> + <annotations> + <features value="One Page Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Checkout different default country per store"/> + <description value="Checkout display default country per store view"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37707"/> + <useCaseId value="MC-36884"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Create store view --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <!-- Set Germany as default country for created store view --> + <magentoCLI command="config:set --scope=stores --scope-code={{customStore.code}} general/country/default {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="changeDefaultCountry"/> + </before> + <after> + <!--Delete product and store view--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + </after> + <!-- Open product and add product to cart--> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$createProduct$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Go to cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <!-- Switch store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewActionGroup"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage"/> + <!-- Grab country code from checkout page and assert value with default country for created store view --> + <grabValueFrom selector="{{CheckoutShippingSection.country}}" stepKey="grabCountry"/> + <assertEquals stepKey="assertCountryValue"> + <actualResult type="const">$grabCountry</actualResult> + <expectedResult type="string">{{DE_Address_Berlin_Not_Default_Address.country_id}}</expectedResult> + </assertEquals> + <!-- Go to cart --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="returnToCartPage"/> + <!-- Switch to default store view --> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefaultStoreView"/> + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="proceedToCheckoutWithDefaultStore"/> + <!-- Grab country code from checkout page and assert value with default country for default store view --> + <grabValueFrom selector="{{CheckoutShippingSection.country}}" stepKey="grabDefaultStoreCountry"/> + <assertEquals stepKey="assertDefaultCountryValue"> + <actualResult type="const">$grabDefaultStoreCountry</actualResult> + <expectedResult type="string">{{US_Address_TX.country_id}}</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml index 4ae6925cc8d55..feab271110356 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml @@ -60,6 +60,8 @@ <argument name="customerEmail" value="$$createCustomer.email$$"/> <argument name="customerPwd" value="$$createCustomer.password$$"/> </actionGroup> + <waitForElementVisible selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitProceedToCheckout"/> + <scrollTo selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="scrollToGoToCheckout"/> <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout1"/> <waitForPageLoad stepKey="waitForShippingMethodSectionToLoad"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index aa05a4828e555..67cf37f75c979 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -7,16 +7,16 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest"> + <test name="StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest" deprecated="Use StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest"> <annotations> <stories value="Checkout"/> - <title value="Verify guest checkout using free shipping and tax variations"/> + <title value="DEPRECATED. Verify guest checkout using free shipping and tax variations"/> <description value="Verify guest checkout using free shipping and tax variations"/> <severity value="CRITICAL"/> <testCaseId value="MC-14709"/> <group value="mtf_migrated"/> <skip> - <issueId value="MC-18802"/> + <issueId value="DEPRECATED">Use StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest</issueId> </skip> </annotations> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml new file mode 100644 index 0000000000000..49af0a285b5f4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -0,0 +1,163 @@ +<?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="StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Verify guest checkout using free shipping and tax variations"/> + <description value="Verify guest checkout using free shipping and tax variations"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28285"/> + <group value="mtf_migrated"/> + <group value="checkout"/> + <group value="tax"/> + </annotations> + <before> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <createData entity="MinimumOrderAmount100" stepKey="minimumOrderAmount"/> + <createData entity="taxRate_US_NY_8_1" stepKey="createTaxRateUSNY"/> + <createData entity="DefaultTaxRuleWithCustomTaxRate" stepKey="createTaxRuleUSNY"> + <requiredEntity createDataKey="createTaxRateUSNY" /> + </createData> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"> + <field key="price">10.00</field> + </createData> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="configurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createProductAttributeOption"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addToDefaultSet"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getProductAttributeOption"> + <requiredEntity createDataKey="createProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="configurableChildProduct"> + <requiredEntity createDataKey="createProductAttribute"/> + <requiredEntity createDataKey="getProductAttributeOption"/> + <field key="price">10.00</field> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="configurableProduct"/> + <requiredEntity createDataKey="createProductAttribute"/> + <requiredEntity createDataKey="getProductAttributeOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="configurableProductAddChild"> + <requiredEntity createDataKey="configurableProduct"/> + <requiredEntity createDataKey="configurableChildProduct"/> + </createData> + <createData entity="SimpleProduct2" stepKey="firstBundleChildProduct"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="secondBundleChildProduct"> + <field key="price">200.00</field> + </createData> + <createData entity="BundleProductPriceViewRange" stepKey="bundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="MultipleSelectOption" stepKey="bundleOption"> + <requiredEntity createDataKey="bundleProduct"/> + <field key="required">True</field> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToProduct"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="firstBundleChildProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToProduct"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="secondBundleChildProduct"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="configurableChildProduct" stepKey="deleteConfigurableChildProduct"/> + <deleteData createDataKey="configurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="firstBundleChildProduct" stepKey="deleteFirstBundleChild"/> + <deleteData createDataKey="secondBundleChildProduct" stepKey="deleteSecondBundleChild"/> + <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createTaxRuleUSNY" stepKey="deleteTaxRuleUSNY"/> + <deleteData createDataKey="createTaxRateUSNY" stepKey="deleteTaxRateUSNY"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addSimpleProductToTheCart"> + <argument name="productQty" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> + <argument name="urlKey" value="$configurableProduct.custom_attributes[url_key]$" /> + <argument name="productAttribute" value="$createProductAttribute.default_value$"/> + <argument name="productOption" value="$getProductAttributeOption.label$"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyBundleProduct"> + <argument name="product" value="$bundleProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAddBundleProductFromProductToCartWithMultiOptionActionGroup" stepKey="addBundleProductToCart"> + <argument name="productName" value="$bundleProduct.name$"/> + <argument name="optionName" value="$bundleOption.name$"/> + <argument name="value" value="$firstBundleChildProduct.name$ +$100.00"/> + </actionGroup> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickMiniCart"/> + <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"> + <argument name="address" value="US_Address_NY_Default_Shipping"/> + </actionGroup> + <click selector="{{CheckoutCartSummarySection.shippingMethodElementId('freeshipping', 'freeshipping')}}" stepKey="selectShippingMethod"/> + <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.72" stepKey="seeTaxAmount"/> + <reloadPage stepKey="reloadThePage"/> + <waitForPageLoad stepKey="waitForPageToReload"/> + <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.72" stepKey="seeTaxAmountAfterLoadPage"/> + <scrollTo selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="scrollToProceedToCheckout" /> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <actionGroup ref="FillGuestCheckoutShippingAddressFormActionGroup" stepKey="fillTheSignInForm"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="customerAddress" value="US_Address_NY_Default_Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <seeElement selector="{{StorefrontMinicartSection.emptyMiniCart}}" stepKey="assertEmptyCart" /> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrderById"> + <argument name="orderId" value="$orderId"/> + </actionGroup> + <actionGroup ref="AdminAssertOrderAvailableButtonsActionGroup" stepKey="assertOrderButtons"/> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$129.72" stepKey="seeGrandTotal"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeOrderPendingStatus"/> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="shipTheOrder"/> + <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="assertCustomerInformation"> + <argument name="customer" value=""/> + <argument name="shippingAddress" value="US_Address_NY_Default_Shipping"/> + <argument name="billingAddress" value="US_Address_NY_Default_Shipping"/> + <argument name="customerGroup" value=""/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml index 1828251e68635..f38061dbf6a6c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -41,9 +41,7 @@ <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> </after> - <!--Open MARKETING > Cart Price Rules--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <!--Add New Rule--> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> diff --git a/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php new file mode 100644 index 0000000000000..f8e17ca8acdec --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php @@ -0,0 +1,167 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\StoreSwitcher\RedirectDataPostprocessor; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataPostprocessorTest extends TestCase +{ + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + /** + * @var ContextInterface|MockObject + */ + private $context; + /** + * @var RedirectDataPostprocessor + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->quoteRepository = $this->createMock(CartRepositoryInterface::class); + $this->customerSession = $this->createMock(CustomerSession::class); + $this->checkoutSession = $this->createMock(CheckoutSession::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataPostprocessor( + $this->quoteRepository, + $this->customerSession, + $this->checkoutSession, + $logger + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + } + + /** + * @dataProvider processDataProvider + * @param array $mock + * @param array $data + * @param bool $isQuoteSet + */ + public function testProcess(array $mock, array $data, bool $isQuoteSet): void + { + $this->customerSession->method('isLoggedIn') + ->willReturn($mock['isLoggedIn']); + $this->checkoutSession->method('getQuoteId') + ->willReturn($mock['getQuoteId']); + $this->checkoutSession->method('getQuote') + ->willReturnCallback( + function () use ($mock) { + return $this->createQuoteMock($mock); + } + ); + $this->quoteRepository->method('get') + ->willReturnCallback( + function ($id) use ($mock) { + return $this->createQuoteMock(array_merge($mock, ['getQuoteId' => $id])); + } + ); + $this->checkoutSession->expects($isQuoteSet ? $this->once() : $this->never()) + ->method('setQuoteId') + ->with($data['quote_id'] ?? null); + + $this->model->process($this->context, $data); + } + + /** + * @return array + */ + public function processDataProvider(): array + { + return [ + [ + ['isLoggedIn' => false, 'getQuoteId' => 4], + ['quote_id' => 2], + false + ], + [ + ['isLoggedIn' => true, 'getQuoteId' => null], + ['quote_id' => 2], + false + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null], + ['quote_id' => 1], + false + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null, 'getIsActive' => false], + ['quote_id' => 2], + false + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null], + ['quote_id' => 2], + true + ], + ]; + } + + /** + * @param array $mock + * @return Quote + */ + private function createQuoteMock(array $mock): Quote + { + return $this->createConfiguredMock( + Quote::class, + [ + 'getIsActive' => $mock['getIsActive'] ?? true, + 'getId' => $mock['getQuoteId'], + 'getSharedStoreIds' => !($mock['getQuoteId'] % 2) ? [1, 2] : [2], + ] + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php new file mode 100644 index 0000000000000..d5c4691d36a14 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\StoreSwitcher\RedirectDataPreprocessor; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Model\Quote; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class RedirectDataPreprocessorTest extends TestCase +{ + /** + * @var CustomerSession + */ + private $customerSession; + /** + * @var CheckoutSession + */ + private $checkoutSession; + /** + * @var RedirectDataPreprocessor + */ + private $model; + /** + * @var ContextInterface|MockObject + */ + private $context; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerSession = $this->createMock(CustomerSession::class); + $this->checkoutSession = $this->createMock(CheckoutSession::class); + $this->model = new RedirectDataPreprocessor( + $this->customerSession, + $this->checkoutSession + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + } + + /** + * @dataProvider processDataProvider + * @param array $mock + * @param array $data + */ + public function testProcess(array $mock, array $data): void + { + $this->customerSession->method('isLoggedIn') + ->willReturn($mock['isLoggedIn']); + $this->checkoutSession->method('getQuoteId') + ->willReturn($mock['getQuoteId']); + $this->checkoutSession->method('getQuote') + ->willReturnCallback( + function () use ($mock) { + return $this->createConfiguredMock( + Quote::class, + [ + 'getIsActive' => $mock['getIsActive'] ?? true, + 'getId' => $mock['getQuoteId'], + 'getSharedStoreIds' => !($mock['getQuoteId'] % 2) ? [1, 2] : [2], + ] + ); + } + ); + $this->assertEquals($data, $this->model->process($this->context, [])); + } + + /** + * @return array + */ + public function processDataProvider(): array + { + return [ + [ + ['isLoggedIn' => true, 'getQuoteId' => 1], + [] + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null], + [] + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => 1], + [] + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => 2], + ['quote_id' => 2] + ], + ]; + } +} diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 8f35fe9f37abf..f28e2a5d91ba3 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -99,4 +99,18 @@ <type name="Magento\Quote\Model\Quote"> <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="checkout_session" xsi:type="object">Magento\Checkout\Model\StoreSwitcher\RedirectDataPreprocessor</item> + </argument> + </arguments> + </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="checkout_session" xsi:type="object">Magento\Checkout\Model\StoreSwitcher\RedirectDataPostprocessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js index a857d89a72b14..d1adb27353e1c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js @@ -79,7 +79,13 @@ define( if (!quote.isVirtual()) { checkoutProvider.on('shippingAddress', function (shippingAddressData) { - checkoutData.setShippingAddressFromData(shippingAddressData); + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + if (quote.shippingAddress().countryId !== shippingAddressData.country_id || + (shippingAddressData.postcode || shippingAddressData.region_id) + ) { + checkoutData.setShippingAddressFromData(shippingAddressData); + } + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers }); } else { checkoutProvider.on('shippingAddress', function (shippingAddressData) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 646e6156ec646..2a52b64647749 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -121,7 +121,9 @@ define([ ); } checkoutProvider.on('shippingAddress', function (shippingAddrsData) { - checkoutData.setShippingAddressFromData(shippingAddrsData); + if (shippingAddrsData.street && !_.isEmpty(shippingAddrsData.street[0])) { + checkoutData.setShippingAddressFromData(shippingAddrsData); + } }); shippingRatesValidator.initFields(fieldsetName); }); diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php index 2643e69ba1efd..c78c807f9ea20 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php @@ -35,12 +35,12 @@ public function __construct($list = null) public function isValid($agreementIds = []) { $agreementIds = $agreementIds === null ? [] : $agreementIds; - $requiredAgreements = [[]]; + $requiredAgreements = []; foreach ($this->agreementsProviders as $agreementsProvider) { $requiredAgreements[] = $agreementsProvider->getRequiredAgreementIds(); } - $agreementsDiff = array_diff(array_merge(...$requiredAgreements), $agreementIds); + $agreementsDiff = array_diff(array_merge([], ...$requiredAgreements), $agreementIds); return empty($agreementsDiff); } diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml index e3e6ae9cffc02..b7a6618d76596 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml @@ -12,7 +12,7 @@ <element name="CheckIfTabExpand" type="button" selector="//div[@data-state-collapsible='closed']//span[text()='Content']"/> <element name="TinyMCE4" type="text" selector=".mce-branding"/> <element name="InsertWidgetBtn" type="button" selector=".action-add-widget"/> - <element name="InsertWidgetIcon" type="button" selector="div[aria-label='Insert Widget']"/> + <element name="InsertWidgetIcon" type="button" selector="div[aria-label='Insert Widget']" timeout="30"/> <element name="InsertVariableBtn" type="button" selector=".scalable.add-variable.plugin"/> <element name="InsertVariableIcon" type="button" selector="div[aria-label='Insert Variable']"/> <element name="InsertImageBtn" type="button" selector=".scalable.action-add-image.plugin"/> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index d1c204c01ad1c..154e76bd93e41 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -11,14 +11,14 @@ $filters = $block->getConfig()->getFilters() ?? []; $allowedExtensions = []; $blockHtmlId = $block->getHtmlId(); -$listExtensions = [[]]; +$listExtensions = []; foreach ($filters as $media_type) { $listExtensions[] = array_map(function ($fileExt) { return ltrim($fileExt, '.*'); }, $media_type['files']); } -$allowedExtensions = array_merge(...$listExtensions); +$allowedExtensions = array_merge([], ...$listExtensions); $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() ? "{action: 'resize', maxWidth: " diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index af54df24b64f5..332c316396122 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -64,7 +64,7 @@ <label translate="true">Store View</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml index 846356adf9429..12c3e8287ecd8 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml @@ -69,7 +69,7 @@ <label translate="true">Store View</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml index 762d17bdf87f1..127677ce05e0d 100644 --- a/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml +++ b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml @@ -8,6 +8,7 @@ <suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> <suite name="AppConfigDumpSuite"> <before> + <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> <magentoCLI command="app:config:dump" stepKey="configDump"/> </before> <after> diff --git a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php index 56993ecec1fbf..75592efc52dca 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Model\Quote\Item; +use Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface; +use Magento\Quote\Api\Data\ProductOptionExtensionInterface; use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\Framework\Serialize\Serializer\Json; @@ -64,7 +66,7 @@ public function __construct( public function convertToBuyRequest(CartItemInterface $cartItem) { if ($cartItem->getProductOption() && $cartItem->getProductOption()->getExtensionAttributes()) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $options */ + /** @var ConfigurableItemOptionValueInterface $options */ $options = $cartItem->getProductOption()->getExtensionAttributes()->getConfigurableItemOptions(); if (is_array($options)) { $requestData = []; @@ -82,13 +84,17 @@ public function convertToBuyRequest(CartItemInterface $cartItem) */ public function processOptions(CartItemInterface $cartItem) { - $attributesOption = $cartItem->getProduct()->getCustomOption('attributes'); + $attributesOption = $cartItem->getProduct() + ->getCustomOption('attributes'); + if (!$attributesOption) { + return $cartItem; + } $selectedConfigurableOptions = $this->serializer->unserialize($attributesOption->getValue()); if (is_array($selectedConfigurableOptions)) { $configurableOptions = []; foreach ($selectedConfigurableOptions as $optionId => $optionValue) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $option */ + /** @var ConfigurableItemOptionValueInterface $option */ $option = $this->itemOptionValueFactory->create(); $option->setOptionId($optionId); $option->setOptionValue($optionValue); @@ -99,8 +105,8 @@ public function processOptions(CartItemInterface $cartItem) ? $cartItem->getProductOption() : $this->productOptionFactory->create(); - /** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */ - $extensibleAttribute = $productOption->getExtensionAttributes() + /** @var ProductOptionExtensionInterface $extensibleAttribute */ + $extensibleAttribute = $productOption->getExtensionAttributes() ? $productOption->getExtensionAttributes() : $this->extensionFactory->create(); @@ -108,6 +114,7 @@ public function processOptions(CartItemInterface $cartItem) $productOption->setExtensionAttributes($extensibleAttribute); $cartItem->setProductOption($productOption); } + return $cartItem; } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php index 10f5b1cbb344a..cd68e1dcfce24 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php @@ -59,7 +59,7 @@ class CartItemProcessorTest extends TestCase */ private $productOptionExtensionAttributes; - /** @var \PHPUnit\Framework\MockObject\MockObject */ + /** @var MockObject */ private $serializer; protected function setUp(): void @@ -263,4 +263,25 @@ public function testProcessProductOptionsIfOptionsExist() $this->assertEquals($cartItemMock, $this->model->processOptions($cartItemMock)); } + + /** + * Checks processOptions method with the empty custom option + * + * @return void + */ + public function testProcessProductWithEmptyOption(): void + { + $customOption = $this->createMock(Option::class); + $productMock = $this->createMock(Product::class); + $productMock->method('getCustomOption') + ->with('attributes') + ->willReturn(null); + $customOption->expects($this->never()) + ->method('getValue'); + $cartItemMock = $this->createPartialMock(Item::class, ['getProduct']); + $cartItemMock->expects($this->once()) + ->method('getProduct') + ->willReturn($productMock); + $this->assertEquals($cartItemMock, $this->model->processOptions($cartItemMock)); + } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml index 2a40caaabae04..c23141d44a2ec 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml @@ -58,7 +58,7 @@ <label translate="true">Status</label> <dataScope>status</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Csp/Helper/InlineUtil.php b/app/code/Magento/Csp/Helper/InlineUtil.php index f9dd9aafa459e..648ba51e34f7d 100644 --- a/app/code/Magento/Csp/Helper/InlineUtil.php +++ b/app/code/Magento/Csp/Helper/InlineUtil.php @@ -110,14 +110,14 @@ private function extractHost(string $url): ?string */ private function extractRemoteFonts(string $styleContent): array { - $urlsFound = [[]]; + $urlsFound = []; preg_match_all('/\@font\-face\s*?\{([^\}]*)[^\}]*?\}/im', $styleContent, $fontFaces); foreach ($fontFaces[1] as $fontFaceContent) { preg_match_all('/url\([\'\"]?(http(s)?\:[^\)]+)[\'\"]?\)/i', $fontFaceContent, $urls); $urlsFound[] = $urls[1]; } - return array_map([$this, 'extractHost'], array_merge(...$urlsFound)); + return array_map([$this, 'extractHost'], array_merge([], ...$urlsFound)); } /** diff --git a/app/code/Magento/Csp/Model/BlockCache.php b/app/code/Magento/Csp/Model/BlockCache.php index f0469c3251379..fac0beec51c07 100644 --- a/app/code/Magento/Csp/Model/BlockCache.php +++ b/app/code/Magento/Csp/Model/BlockCache.php @@ -111,7 +111,7 @@ public function save($data, $identifier, $tags = [], $lifeTime = null) ]; } } - $data = $this->serializer->serialize(['policies' => $policiesData, 'html' => $data]); + $data = $this->serializer->serialize(['policies' => $policiesData, 'html' => (string)$data]); } return $this->cache->save($data, $identifier, $tags, $lifeTime); diff --git a/app/code/Magento/Csp/Model/Collector/CompositeMerger.php b/app/code/Magento/Csp/Model/Collector/CompositeMerger.php new file mode 100644 index 0000000000000..16430f1ff8aa9 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CompositeMerger.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Merges policies using different mergers. + */ +class CompositeMerger implements MergerInterface +{ + /** + * @var MergerInterface[] + */ + private $mergers; + + /** + * @param MergerInterface[] $mergers + */ + public function __construct(array $mergers) + { + $this->mergers = $mergers; + } + + /** + * @inheritDoc + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($policy1, $policy2)) { + return $merger->merge($policy1, $policy2); + } + } + + throw new \RuntimeException('Cannot merge 2 policies of ' .get_class($policy1)); + } + + /** + * @inheritDoc + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool + { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($policy1, $policy2)) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php new file mode 100644 index 0000000000000..acc0dd1600db1 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\CspWhitelistXml; + +use Magento\Framework\Config\FileResolverInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Design\Theme\CustomizationInterface; +use Magento\Framework\View\Design\Theme\CustomizationInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\ReadInterface as DirectoryRead; +use Magento\Framework\Config\CompositeFileIteratorFactory; + +/** + * Combines configuration files from both modules and current theme. + */ +class FileResolver implements FileResolverInterface +{ + /** + * @var FileResolverInterface + */ + private $moduleFileResolver; + + /** + * @var ThemeInterface + */ + private $theme; + + /** + * @var CustomizationInterfaceFactory + */ + private $themeInfoFactory; + + /** + * @var DirectoryRead + */ + private $rootDir; + + /** + * @var CompositeFileIteratorFactory + */ + private $iteratorFactory; + + /** + * @param FileResolverInterface $moduleFileResolver + * @param DesignInterface $design + * @param CustomizationInterfaceFactory $customizationFactory + * @param Filesystem $filesystem + * @param CompositeFileIteratorFactory $iteratorFactory + */ + public function __construct( + FileResolverInterface $moduleFileResolver, + DesignInterface $design, + CustomizationInterfaceFactory $customizationFactory, + Filesystem $filesystem, + CompositeFileIteratorFactory $iteratorFactory + ) { + $this->moduleFileResolver = $moduleFileResolver; + $this->theme = $design->getDesignTheme(); + $this->themeInfoFactory = $customizationFactory; + $this->rootDir = $filesystem->getDirectoryRead(DirectoryList::ROOT); + $this->iteratorFactory = $iteratorFactory; + } + + /** + * @inheritDoc + */ + public function get($filename, $scope) + { + $configs = $this->moduleFileResolver->get($filename, $scope); + if ($scope === 'global') { + $files = []; + $theme = $this->theme; + while ($theme) { + /** @var CustomizationInterface $info */ + $info = $this->themeInfoFactory->create(['theme' => $theme]); + $file = $info->getThemeFilesPath() .'/etc/' .$filename; + if ($this->rootDir->isExist($file)) { + $files[] = $file; + } + $theme = $theme->getParentTheme(); + } + $configs = $this->iteratorFactory->create( + ['paths' => array_reverse($files), 'existingIterator' => $configs] + ); + } + + return $configs; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/DynamicCollector.php b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php index 6478e9622f910..743f77c93f3d8 100644 --- a/app/code/Magento/Csp/Model/Collector/DynamicCollector.php +++ b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php @@ -20,6 +20,19 @@ class DynamicCollector implements PolicyCollectorInterface */ private $added = []; + /** + * @var MergerInterface + */ + private $merger; + + /** + * @param MergerInterface $merger + */ + public function __construct(MergerInterface $merger) + { + $this->merger = $merger; + } + /** * Add a policy for current page. * @@ -28,7 +41,15 @@ class DynamicCollector implements PolicyCollectorInterface */ public function add(PolicyInterface $policy): void { - $this->added[] = $policy; + if (array_key_exists($policy->getId(), $this->added)) { + if ($this->merger->canMerge($this->added[$policy->getId()], $policy)) { + $this->added[$policy->getId()] = $this->merger->merge($this->added[$policy->getId()], $policy); + } else { + throw new \RuntimeException('Cannot merge a policy of ' .get_class($policy)); + } + } else { + $this->added[$policy->getId()] = $policy; + } } /** @@ -36,6 +57,6 @@ public function add(PolicyInterface $policy): void */ public function collect(array $defaultPolicies = []): array { - return array_merge($defaultPolicies, $this->added); + return array_merge($defaultPolicies, array_values($this->added)); } } diff --git a/app/code/Magento/Csp/etc/di.xml b/app/code/Magento/Csp/etc/di.xml index 7b1129a0e1a41..5e90c4b0c866c 100644 --- a/app/code/Magento/Csp/etc/di.xml +++ b/app/code/Magento/Csp/etc/di.xml @@ -15,6 +15,17 @@ </arguments> </type> <preference for="Magento\Csp\Api\PolicyCollectorInterface" type="Magento\Csp\Model\CompositePolicyCollector" /> + <preference for="Magento\Csp\Model\Collector\MergerInterface" type="Magento\Csp\Model\Collector\CompositeMerger" /> + <type name="Magento\Csp\Model\Collector\CompositeMerger"> + <arguments> + <argument name="mergers" xsi:type="array"> + <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\FetchPolicyMerger</item> + <item name="flag" xsi:type="object">Magento\Csp\Model\Collector\FlagPolicyMerger</item> + <item name="plugins" xsi:type="object">Magento\Csp\Model\Collector\PluginTypesPolicyMerger</item> + <item name="sandbox" xsi:type="object">Magento\Csp\Model\Collector\SandboxPolicyMerger</item> + </argument> + </arguments> + </type> <type name="Magento\Csp\Model\CompositePolicyCollector"> <arguments> <argument name="collectors" xsi:type="array"> @@ -24,10 +35,7 @@ <item name="dynamic" xsi:type="object" sortOrder="3">Magento\Csp\Model\Collector\DynamicCollector\Proxy</item> </argument> <argument name="mergers" xsi:type="array"> - <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\FetchPolicyMerger</item> - <item name="flag" xsi:type="object">Magento\Csp\Model\Collector\FlagPolicyMerger</item> - <item name="plugins" xsi:type="object">Magento\Csp\Model\Collector\PluginTypesPolicyMerger</item> - <item name="sandbox" xsi:type="object">Magento\Csp\Model\Collector\SandboxPolicyMerger</item> + <item name="composite" xsi:type="object">Magento\Csp\Model\Collector\MergerInterface</item> </argument> </arguments> </type> @@ -46,6 +54,7 @@ <arguments> <argument name="converter" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\SchemaLocator</argument> + <argument name="fileResolver" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\FileResolver</argument> <argument name="fileName" xsi:type="string">csp_whitelist.xml</argument> </arguments> </type> @@ -93,6 +102,7 @@ <type name="Magento\Csp\Model\BlockCache"> <arguments> <argument name="cache" xsi:type="object">configured_block_cache</argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> </arguments> </type> <type name="Magento\Framework\View\Element\Context"> diff --git a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php index d48df02d9de27..400aa56bc68e9 100644 --- a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php +++ b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php @@ -292,7 +292,7 @@ protected function _unserializeStoreConfig($configPath, $storeId = null) */ protected function getAllowedCurrencies() { - $allowedCurrencies = [[]]; + $allowedCurrencies = []; $allowedCurrencies[] = explode( self::ALLOWED_CURRENCIES_CONFIG_SEPARATOR, $this->_scopeConfig->getValue( @@ -330,6 +330,6 @@ protected function getAllowedCurrencies() } } } - return array_unique(array_merge(...$allowedCurrencies)); + return array_unique(array_merge([], ...$allowedCurrencies)); } } diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 90ce9ba210ed2..ef2d2cca169f5 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -7,11 +7,16 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Framework\Api\ArrayObjectSearch; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\EncoderInterface; +use Magento\Framework\Locale\Bundle\DataBundle; +use Magento\Framework\Locale\ResolverInterface; /** * Customer date of birth attribute block * * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Dob extends AbstractWidget { @@ -39,6 +44,18 @@ class Dob extends AbstractWidget */ protected $filterFactory; + /** + * JSON Encoder + * + * @var EncoderInterface + */ + private $encoder; + + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Customer\Helper\Address $addressHelper @@ -46,6 +63,8 @@ class Dob extends AbstractWidget * @param \Magento\Framework\View\Element\Html\Date $dateElement * @param \Magento\Framework\Data\Form\FilterFactory $filterFactory * @param array $data + * @param EncoderInterface|null $encoder + * @param ResolverInterface|null $localeResolver */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -53,10 +72,14 @@ public function __construct( CustomerMetadataInterface $customerMetadata, \Magento\Framework\View\Element\Html\Date $dateElement, \Magento\Framework\Data\Form\FilterFactory $filterFactory, - array $data = [] + array $data = [], + ?EncoderInterface $encoder = null, + ?ResolverInterface $localeResolver = null ) { $this->dateElement = $dateElement; $this->filterFactory = $filterFactory; + $this->encoder = $encoder ?? ObjectManager::getInstance()->get(EncoderInterface::class); + $this->localeResolver = $localeResolver ?? ObjectManager::getInstance()->get(ResolverInterface::class); parent::__construct($context, $addressHelper, $customerMetadata, $data); } @@ -281,7 +304,7 @@ public function getHtmlExtraParams() */ public function getDateFormat() { - $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); + $dateFormat = $this->setTwoDayPlaces($this->_localeDate->getDateFormatWithLongYear()); /** Escape RTL characters which are present in some locales and corrupt formatting */ $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); @@ -377,4 +400,45 @@ public function getFirstDay() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } + + /** + * Get translated calendar config json formatted + * + * @return string + */ + public function getTranslatedCalendarConfigJson(): string + { + $localeData = (new DataBundle())->get($this->localeResolver->getLocale()); + $monthsData = $localeData['calendar']['gregorian']['monthNames']; + $daysData = $localeData['calendar']['gregorian']['dayNames']; + + return $this->encoder->encode( + [ + 'closeText' => __('Done'), + 'prevText' => __('Prev'), + 'nextText' => __('Next'), + 'currentText' => __('Today'), + 'monthNames' => array_values(iterator_to_array($monthsData['format']['wide'])), + 'monthNamesShort' => array_values(iterator_to_array($monthsData['format']['abbreviated'])), + 'dayNames' => array_values(iterator_to_array($daysData['format']['wide'])), + 'dayNamesShort' => array_values(iterator_to_array($daysData['format']['abbreviated'])), + 'dayNamesMin' => array_values(iterator_to_array($daysData['format']['short'])), + ] + ); + } + + /** + * Set 2 places for day value in format string + * + * @param string $format + * @return string + */ + private function setTwoDayPlaces(string $format): string + { + return preg_replace( + '/(?<!d)d(?!d)/', + 'dd', + $format + ); + } } diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index d22a10145c7be..67017562e105c 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -977,7 +977,6 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); - $customer->setConfirmation(null); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); @@ -1615,37 +1614,6 @@ private function getEmailNotification() } } - /** - * Destroy all active customer sessions by customer id (current session will not be destroyed). - * - * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering - * configured session lifetime. - * - * @param string|int $customerId - * @return void - */ - private function destroyCustomerSessions($customerId) - { - $this->sessionManager->regenerateId(); - $sessionLifetime = $this->scopeConfig->getValue( - \Magento\Framework\Session\Config::XML_PATH_COOKIE_LIFETIME, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $dateTime = $this->dateTimeFactory->create(); - $activeSessionsTime = $dateTime->setTimestamp($dateTime->getTimestamp() - $sessionLifetime) - ->format(DateTime::DATETIME_PHP_FORMAT); - /** @var \Magento\Customer\Model\ResourceModel\Visitor\Collection $visitorCollection */ - $visitorCollection = $this->visitorCollectionFactory->create(); - $visitorCollection->addFieldToFilter('customer_id', $customerId); - $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]); - $visitorCollection->addFieldToFilter('session_id', ['neq' => $this->sessionManager->getSessionId()]); - /** @var \Magento\Customer\Model\Visitor $visitor */ - foreach ($visitorCollection->getItems() as $visitor) { - $sessionId = $visitor->getSessionId(); - $this->saveHandler->destroy($sessionId); - } - } - /** * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation * diff --git a/app/code/Magento/Customer/Model/AccountManagementApi.php b/app/code/Magento/Customer/Model/AccountManagementApi.php new file mode 100644 index 0000000000000..02a05705b57ef --- /dev/null +++ b/app/code/Magento/Customer/Model/AccountManagementApi.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\CustomerInterface; + +/** + * Account Management service implementation for external API access. + * Handle various customer account actions. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class AccountManagementApi extends AccountManagement +{ + /** + * @inheritDoc + * + * Override createAccount method to unset confirmation attribute for security purposes. + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { + $customer = parent::createAccount($customer, $password, $redirectUrl); + $customer->setConfirmation(null); + + return $customer; + } +} diff --git a/app/code/Magento/Customer/Model/Address/CompositeValidator.php b/app/code/Magento/Customer/Model/Address/CompositeValidator.php index 4c77f10c11de4..62308ba329d03 100644 --- a/app/code/Magento/Customer/Model/Address/CompositeValidator.php +++ b/app/code/Magento/Customer/Model/Address/CompositeValidator.php @@ -30,11 +30,11 @@ public function __construct( */ public function validate(AbstractAddress $address) { - $errors = [[]]; + $errors = []; foreach ($this->validators as $validator) { $errors[] = $validator->validate($address); } - return array_merge(...$errors); + return array_merge([], ...$errors); } } diff --git a/app/code/Magento/Customer/Model/Metadata/Form.php b/app/code/Magento/Customer/Model/Metadata/Form.php index 85637ebf508b8..81ded6dec071a 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form.php +++ b/app/code/Magento/Customer/Model/Metadata/Form.php @@ -363,11 +363,11 @@ public function validateData(array $data) { $validator = $this->_getValidator($data); if (!$validator->isValid(false)) { - $messages = [[]]; + $messages = []; foreach ($validator->getMessages() as $errorMessages) { $messages[] = (array)$errorMessages; } - return array_merge(...$messages); + return array_merge([], ...$messages); } return true; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php index 020067570efb4..1ca1c5622803f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php @@ -84,7 +84,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) $websiteIds = []; if (!$this->shareConfig->isGlobalScope()) { - $allowedCountries = [[]]; + $allowedCountries = []; foreach ($this->storeManager->getWebsites() as $website) { $countries = $this->allowedCountriesReader @@ -96,7 +96,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) } } - $allowedCountries = array_unique(array_merge(...$allowedCountries)); + $allowedCountries = array_unique(array_merge([], ...$allowedCountries)); } else { $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(); } diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php new file mode 100644 index 0000000000000..9fb3d6f432c2f --- /dev/null +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\StoreSwitcher; + +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Psr\Log\LoggerInterface; + +/** + * Process customer data redirected from origin store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPostprocessor implements RedirectDataPostprocessorInterface +{ + /** + * @var Session + */ + private $session; + /** + * @var LoggerInterface + */ + private $logger; + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @param CustomerRegistry $customerRegistry + * @param Session $session + * @param LoggerInterface $logger + */ + public function __construct( + CustomerRegistry $customerRegistry, + Session $session, + LoggerInterface $logger + ) { + $this->customerRegistry = $customerRegistry; + $this->session = $session; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): void + { + if (!empty($data['customer_id'])) { + try { + $customer = $this->customerRegistry->retrieve($data['customer_id']); + if (!$this->session->isLoggedIn() + && in_array($context->getTargetStore()->getId(), $customer->getSharedStoreIds()) + ) { + $this->session->setCustomerDataAsLoggedIn($customer->getDataModel()); + } + } catch (NoSuchEntityException $e) { + $this->logger->error($e); + throw new LocalizedException(__('Something went wrong.'), $e); + } catch (LocalizedException $e) { + $this->logger->error($e); + throw new LocalizedException(__('Something went wrong.'), $e); + } + } + } +} diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php new file mode 100644 index 0000000000000..94f7619678df0 --- /dev/null +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\StoreSwitcher; + +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Collect customer data to be redirected to target store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface +{ + /** + * @var Session + */ + private $session; + /** + * @var LoggerInterface + */ + private $logger; + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @param CustomerRegistry $customerRegistry + * @param Session $session + * @param LoggerInterface $logger + */ + public function __construct( + CustomerRegistry $customerRegistry, + Session $session, + LoggerInterface $logger + ) { + $this->customerRegistry = $customerRegistry; + $this->session = $session; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): array + { + if ($this->session->isLoggedIn()) { + try { + $customer = $this->customerRegistry->retrieve($this->session->getCustomerId()); + if (in_array($context->getTargetStore()->getId(), $customer->getSharedStoreIds())) { + $data['customer_id'] = (int) $customer->getId(); + } + } catch (Throwable $e) { + $this->logger->error($e); + } + } + + return $data; + } +} diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml deleted file mode 100644 index f226d49e3bf54..0000000000000 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?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="AdminCustomerGridMainActionsSection"> - <element name="addNewAddress" type="button" selector=".add-new-address-button" timeout="30"/> - <element name="actions" type="text" selector=".admin__data-grid-header-row .action-select"/> - </section> -</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml index 8277cdd64928a..44ab653259b55 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml @@ -13,8 +13,9 @@ <element name="multicheck" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>label"/> <element name="multicheckTick" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>input"/> <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> - <element name="actions" type="text" selector=".action-select"/> <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{arg}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']//input" parameterized="true"/> <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="addNewAddress" type="button" selector=".add-new-address-button" timeout="30"/> + <element name="actions" type="text" selector=".admin__data-grid-header-row .action-select"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 39071f25ea18c..6da31b552c622 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -17,8 +17,10 @@ use Magento\Framework\Data\Form\FilterFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; use Magento\Framework\Stdlib\DateTime\Timezone; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\Html\Date; @@ -50,7 +52,7 @@ class DobTest extends TestCase const YEAR = '2014'; // Value of date('Y', strtotime(self::DATE)) - const DATE_FORMAT = 'M/d/Y'; + const DATE_FORMAT = 'M/dd/y'; /** Constants used by Dob::setDateInput($code, $html) */ const DAY_HTML = @@ -90,6 +92,16 @@ class DobTest extends TestCase */ private $_locale; + /** + * @var EncoderInterface + */ + private $encoder; + + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * @inheritDoc */ @@ -109,17 +121,18 @@ protected function setUp(): void $cache->expects($this->any())->method('getFrontend')->willReturn($frontendCache); $objectManager = new ObjectManager($this); - $localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); - $localeResolver->expects($this->any()) + $this->localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->localeResolver->expects($this->any()) ->method('getLocale') ->willReturnCallback( function () { return $this->_locale; } ); + $localeResolver = $this->localeResolver; $timezone = $objectManager->getObject( Timezone::class, - ['localeResolver' => $localeResolver] + ['localeResolver' => $localeResolver, 'dateFormatterFactory' => new DateFormatterFactory()] ); $this->_locale = Resolver::DEFAULT_LOCALE; @@ -156,12 +169,17 @@ function () use ($timezone, $localeResolver) { } ); + $this->encoder = $this->getMockForAbstractClass(EncoderInterface::class); + $this->_block = new Dob( $this->context, $this->createMock(Address::class), $this->customerMetadata, $this->createMock(Date::class), - $this->filterFactory + $this->filterFactory, + [], + $this->encoder, + $this->localeResolver ); } @@ -355,10 +373,15 @@ public function getDateFormatDataProvider(): array [ 'ar_SA', preg_replace( - '/[^MmDdYy\/\.\-]/', - '', - (new \IntlDateFormatter('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE)) - ->getPattern() + '/(?<!d)d(?!d)/', + 'dd', + preg_replace( + '/[^MmDdYy\/\.\-]/', + '', + (new DateFormatterFactory()) + ->create('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE) + ->getPattern() + ) ) ], [Resolver::DEFAULT_LOCALE, self::DATE_FORMAT], @@ -596,4 +619,80 @@ public function testGetHtmlExtraParamsWithRequiredOption() $this->_block->getHtmlExtraParams() ); } + + /** + * Tests getTranslatedCalendarConfigJson() + * + * @param string $locale + * @param array $expectedArray + * @param string $expectedJson + * @dataProvider getTranslatedCalendarConfigJsonDataProvider + * @return void + */ + public function testGetTranslatedCalendarConfigJson( + string $locale, + array $expectedArray, + string $expectedJson + ): void { + $this->_locale = $locale; + + $this->encoder->expects($this->once()) + ->method('encode') + ->with($expectedArray) + ->willReturn($expectedJson); + + $this->assertEquals( + $expectedJson, + $this->_block->getTranslatedCalendarConfigJson() + ); + } + + /** + * Provider for testGetTranslatedCalendarConfigJson + * + * @return array + */ + public function getTranslatedCalendarConfigJsonDataProvider() + { + return [ + [ + 'locale' => 'en_US', + 'expectedArray' => [ + 'closeText' => 'Done', + 'prevText' => 'Prev', + 'nextText' => 'Next', + 'currentText' => 'Today', + 'monthNames' => ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December'], + 'monthNamesShort' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + 'dayNames' => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + 'dayNamesShort' => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + 'dayNamesMin' => ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], + ], + // phpcs:disable Generic.Files.LineLength.TooLong + 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthNamesShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"dayNames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"dayNamesShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"dayNamesMin":["Su","Mo","Tu","We","Th","Fr","Sa"]}' + // phpcs:enable Generic.Files.LineLength.TooLong + ], + [ + 'locale' => 'de_DE', + 'expectedArray' => [ + 'closeText' => 'Done', + 'prevText' => 'Prev', + 'nextText' => 'Next', + 'currentText' => 'Today', + 'monthNames' => ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + 'monthNamesShort' => ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', + 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.'], + 'dayNames' => ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + 'dayNamesShort' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], + 'dayNamesMin' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], + ], + // phpcs:disable Generic.Files.LineLength.TooLong + 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],"monthNamesShort":["Jan.","Feb.","M\u00e4rz","Apr.","Mai","Juni","Juli","Aug.","Sept.","Okt.","Nov.","Dez."],"dayNames":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],"dayNamesShort":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],"dayNamesMin":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."]}' + // phpcs:enable Generic.Files.LineLength.TooLong + ], + ]; + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php new file mode 100644 index 0000000000000..0be0212652058 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\StoreSwitcher; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\StoreSwitcher\RedirectDataPostprocessor; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataPostprocessorTest extends TestCase +{ + /** + * @var Session + */ + private $session; + /** + * @var RedirectDataPostprocessor + */ + private $model; + /** + * @var ContextInterface|MockObject + */ + private $context; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $customerRegistry = $this->createMock(CustomerRegistry::class); + $this->session = $this->createMock(Session::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataPostprocessor( + $customerRegistry, + $this->session, + $logger + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + + $customerRegistry->method('retrieve') + ->willReturnCallback( + function ($id) { + switch ($id) { + case 1: + throw new NoSuchEntityException(__('Customer does not exist')); + case 2: + throw new LocalizedException(__('Something went wrong')); + default: + $customer = $this->createMock(Customer::class); + $customer->method('getSharedStoreIds') + ->willReturn(!($id % 2) ? [1, 2] : [2]); + $customer->method('getDataModel') + ->willReturn( + $this->createConfiguredMock( + CustomerInterface::class, + [ + 'getId' => $id + ] + ) + ); + return $customer; + } + } + ); + } + + public function testProcessShouldLoginCustomerIfCustomerIsRegisteredInTargetStore(): void + { + $data = ['customer_id' => 4]; + $this->session->expects($this->once()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } + + public function testProcessShouldNotLoginCustomerIfNotRegisteredInTargetStore(): void + { + $data = ['customer_id' => 3]; + $this->session->expects($this->never()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } + + public function testProcessShouldThrowExceptionIfCustomerDoesNotExist(): void + { + $this->expectErrorMessage('Something went wrong.'); + $data = ['customer_id' => 1]; + $this->session->expects($this->never()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } + + public function testProcessShouldThrowExceptionIfAnErrorOccur(): void + { + $this->expectErrorMessage('Something went wrong.'); + $data = ['customer_id' => 2]; + $this->session->expects($this->never()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php new file mode 100644 index 0000000000000..3d0c9c2e0a630 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\StoreSwitcher; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\StoreSwitcher\RedirectDataPreprocessor; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataPreprocessorTest extends TestCase +{ + /** + * @var RedirectDataPreprocessor + */ + private $model; + /** + * @var Session|MockObject + */ + private $context; + /** + * @var UserContextInterface|MockObject + */ + private $session; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $customerRegistry = $this->createMock(CustomerRegistry::class); + $logger = $this->createMock(LoggerInterface::class); + $this->session = $this->createMock(Session::class); + $this->model = new RedirectDataPreprocessor( + $customerRegistry, + $this->session, + $logger + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + + $customerRegistry->method('retrieve') + ->willReturnCallback( + function ($id) { + switch ($id) { + case 1: + throw new NoSuchEntityException(__('Customer does not exist')); + case 2: + throw new LocalizedException(__('Something went wrong')); + default: + $customer = $this->createMock(Customer::class); + $customer->method('getSharedStoreIds') + ->willReturn(!($id % 2) ? [1, 2] : [2]); + $customer->method('getId') + ->willReturn($id); + return $customer; + } + } + ); + } + + /** + * @dataProvider processDataProvider + * @param int|null $customerId + * @param array $data + */ + public function testProcess(?int $customerId, array $data): void + { + $this->session->method('isLoggedIn') + ->willReturn(true); + $this->session->method('getCustomerId') + ->willReturn($customerId); + $this->assertEquals($data, $this->model->process($this->context, [])); + } + + /** + * @return array + */ + public function processDataProvider(): array + { + return [ + [1, []], + [2, []], + [3, []], + [4, ['customer_id' => 4]] + ]; + } +} diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 2a6e36a1ea3d7..31f3e11522e12 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -113,4 +113,18 @@ </argument> </arguments> </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="customer_session" xsi:type="object">Magento\Customer\Model\StoreSwitcher\RedirectDataPreprocessor</item> + </argument> + </arguments> + </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="customer_session" xsi:type="object">Magento\Customer\Model\StoreSwitcher\RedirectDataPostprocessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index d07d1a61c3d62..18627b68320ed 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -31,4 +31,6 @@ </argument> </arguments> </type> + <preference for="Magento\Customer\Api\AccountManagementInterface" + type="Magento\Customer\Model\AccountManagementApi" /> </config> diff --git a/app/code/Magento/Customer/etc/webapi_soap/di.xml b/app/code/Magento/Customer/etc/webapi_soap/di.xml index c23de8ef3f7e1..cb0b1ce58a594 100644 --- a/app/code/Magento/Customer/etc/webapi_soap/di.xml +++ b/app/code/Magento/Customer/etc/webapi_soap/di.xml @@ -18,4 +18,6 @@ </argument> </arguments> </type> + <preference for="Magento\Customer\Api\AccountManagementInterface" + type="Magento\Customer\Model\AccountManagementApi" /> </config> diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index 3c2f970faadee..da1c85cce9856 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** @var \Magento\Customer\Block\Widget\Dob $block */ /* @@ -23,14 +24,17 @@ NOTE: Regarding styles - if we leave it this way, we'll move it to boxes.css. Al automatically using block input parameters. */ +$translatedCalendarConfigJson = $block->getTranslatedCalendarConfigJson(); $fieldCssClass = 'field date field-' . $block->getHtmlId(); $fieldCssClass .= $block->isRequired() ? ' required' : ''; ?> <div class="<?= $block->escapeHtmlAttr($fieldCssClass) ?>"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('dob')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('dob')) ?></span> + </label> <div class="control customer-dob"> <?= $block->getFieldHtml() ?> - <?php if ($_message = $block->getAdditionalDescription()) : ?> + <?php if ($_message = $block->getAdditionalDescription()): ?> <div class="note"><?= $block->escapeHtml($_message) ?></div> <?php endif; ?> </div> @@ -42,4 +46,22 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; "Magento_Customer/js/validation": {} } } - </script> +</script> + +<?php $scriptString = <<<code + +require([ + 'jquery', + 'jquery-ui-modules/datepicker' +], function($){ + +//<![CDATA[ + $.extend(true, $, { + calendarConfig: {$translatedCalendarConfigJson} + }); +//]]> + +}); +code; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php index fa2ae669cc89d..a098325c820d6 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php @@ -118,7 +118,7 @@ public function resolve( $args['newPassword'] ); } catch (LocalizedException $e) { - throw new GraphQlInputException(__('Cannot set the customer\'s password'), $e); + throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index f15f920fe95f4..0c3be73ec5047 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -525,12 +525,12 @@ public function validateData() protected function _importData() { //Preparing data for mass validation/import. - $rows = [[]]; + $rows = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $rows[] = $bunch; } - $this->prepareCustomerData(array_merge(...$rows)); + $this->prepareCustomerData(array_merge([], ...$rows)); unset($bunch, $rows); $this->_dataSourceModel->getIterator()->rewind(); diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 5ebf242bd6ac4..2a02205bdc7e5 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -514,8 +514,8 @@ protected function _importData() { while ($bunch = $this->_dataSourceModel->getNextBunch()) { $this->prepareCustomerData($bunch); - $entitiesToCreate = [[]]; - $entitiesToUpdate = [[]]; + $entitiesToCreate = []; + $entitiesToUpdate = []; $entitiesToDelete = []; $attributesToSave = []; @@ -549,8 +549,8 @@ protected function _importData() } } - $entitiesToCreate = array_merge(...$entitiesToCreate); - $entitiesToUpdate = array_merge(...$entitiesToUpdate); + $entitiesToCreate = array_merge([], ...$entitiesToCreate); + $entitiesToUpdate = array_merge([], ...$entitiesToUpdate); $this->updateItemsCounterStats($entitiesToCreate, $entitiesToUpdate, $entitiesToDelete); /** diff --git a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php index 8a75ad0def222..eb87a9c12125b 100644 --- a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php +++ b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php @@ -3,14 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Deploy\Console\Command\App; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; +use Magento\Deploy\Console\Command\App\ConfigImport\Processor; +use Magento\Framework\App\Area; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Magento\Framework\Console\Cli; -use Magento\Deploy\Console\Command\App\ConfigImport\Processor; /** * Runs the process of importing configuration data from shared source to appropriate application sources @@ -21,9 +28,6 @@ */ class ConfigImportCommand extends Command { - /** - * Command name. - */ const COMMAND_NAME = 'app:config:import'; /** @@ -33,12 +37,40 @@ class ConfigImportCommand extends Command */ private $processor; + /** + * @var EmulatedAdminhtmlAreaProcessor + */ + private $adminhtmlAreaProcessor; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var AreaList + */ + private $areaList; + /** * @param Processor $processor the configuration importer + * @param DeploymentConfig|null $deploymentConfig + * @param EmulatedAdminhtmlAreaProcessor|null $adminhtmlAreaProcessor + * @param AreaList|null $areaList */ - public function __construct(Processor $processor) - { + public function __construct( + Processor $processor, + DeploymentConfig $deploymentConfig = null, + EmulatedAdminhtmlAreaProcessor $adminhtmlAreaProcessor = null, + AreaList $areaList = null + ) { $this->processor = $processor; + $this->deploymentConfig = $deploymentConfig + ?? ObjectManager::getInstance()->get(DeploymentConfig::class); + $this->adminhtmlAreaProcessor = $adminhtmlAreaProcessor + ?? ObjectManager::getInstance()->get(EmulatedAdminhtmlAreaProcessor::class); + $this->areaList = $areaList + ?? ObjectManager::getInstance()->get(AreaList::class); parent::__construct(); } @@ -55,12 +87,26 @@ protected function configure() } /** - * Imports data from deployment configuration files to the DB. {@inheritdoc} + * Imports data from deployment configuration files to the DB. + * {@inheritdoc} + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output) { try { - $this->processor->execute($input, $output); + if ($this->canEmulateAdminhtmlArea()) { + // Emulate adminhtml area in order to execute all needed plugins declared only for this area + // For instance URL rewrite generation during creating store view + $this->adminhtmlAreaProcessor->process(function () use ($input, $output) { + $this->processor->execute($input, $output); + }); + } else { + $this->processor->execute($input, $output); + } } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); @@ -69,4 +115,19 @@ protected function execute(InputInterface $input, OutputInterface $output) return Cli::RETURN_SUCCESS; } + + /** + * Detects if we can emulate adminhtml area + * + * This area could be not available for instance during setup:install + * + * @return bool + * @throws RuntimeException + * @throws FileSystemException + */ + private function canEmulateAdminhtmlArea(): bool + { + return $this->deploymentConfig->isAvailable() + && in_array(Area::AREA_ADMINHTML, $this->areaList->getCodes()); + } } diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index 2a83d0d4c56ec..5780b46365680 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -443,11 +443,11 @@ public function getResultMap() */ public function getParentMap() { - $map = [[]]; + $map = []; foreach ($this->getParentPackages() as $parentPackage) { $map[] = $parentPackage->getMap(); } - return array_merge(...$map); + return array_merge([], ...$map); } /** @@ -458,7 +458,7 @@ public function getParentMap() */ public function getParentFiles($type = null) { - $files = [[]]; + $files = []; foreach ($this->getParentPackages() as $parentPackage) { if ($type === null) { $files[] = $parentPackage->getFiles(); @@ -466,7 +466,7 @@ public function getParentFiles($type = null) $files[] = $parentPackage->getFilesByType($type); } } - return array_merge(...$files); + return array_merge([], ...$files); } /** @@ -535,7 +535,7 @@ private function collectParentPaths( $area, $theme, $locale, - array & $result = [], + array &$result = [], ThemeInterface $themeModel = null ) { if (($package->getArea() != $area) || ($package->getTheme() != $theme) || ($package->getLocale() != $locale)) { diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php index da790a19f480a..32bdd63ef4638 100644 --- a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php @@ -7,8 +7,11 @@ namespace Magento\Deploy\Test\Unit\Console\Command\App; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; use Magento\Deploy\Console\Command\App\ConfigImport\Processor; use Magento\Deploy\Console\Command\App\ConfigImportCommand; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\RuntimeException; use PHPUnit\Framework\MockObject\MockObject; @@ -27,16 +30,37 @@ class ConfigImportCommandTest extends TestCase */ private $commandTester; + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfigMock; + + /** + * @var EmulatedAdminhtmlAreaProcessor|MockObject + */ + private $adminhtmlAreaProcessorMock; + + /** + * @var AreaList|MockObject + */ + private $areaListMock; + /** * @return void */ protected function setUp(): void { - $this->processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); + $this->processorMock = $this->createMock(Processor::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->adminhtmlAreaProcessorMock = $this->createMock(EmulatedAdminhtmlAreaProcessor::class); + $this->areaListMock = $this->createMock(AreaList::class); - $configImportCommand = new ConfigImportCommand($this->processorMock); + $configImportCommand = new ConfigImportCommand( + $this->processorMock, + $this->deploymentConfigMock, + $this->adminhtmlAreaProcessorMock, + $this->areaListMock + ); $this->commandTester = new CommandTester($configImportCommand); } @@ -46,6 +70,13 @@ protected function setUp(): void */ public function testExecute() { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->once()) + ->method('process')->willReturnCallback(function (callable $callback, array $params = []) { + return $callback(...$params); + }); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn(['adminhtml']); + $this->processorMock->expects($this->once()) ->method('execute'); @@ -57,6 +88,13 @@ public function testExecute() */ public function testExecuteWithException() { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->once()) + ->method('process')->willReturnCallback(function (callable $callback, array $params = []) { + return $callback(...$params); + }); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn(['adminhtml']); + $this->processorMock->expects($this->once()) ->method('execute') ->willThrowException(new RuntimeException(__('Some error'))); @@ -64,4 +102,34 @@ public function testExecuteWithException() $this->assertSame(Cli::RETURN_FAILURE, $this->commandTester->execute([])); $this->assertStringContainsString('Some error', $this->commandTester->getDisplay()); } + + /** + * @return void + */ + public function testExecuteWithDeploymentConfigNotAvailable() + { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(false); + $this->adminhtmlAreaProcessorMock->expects($this->never())->method('process'); + $this->areaListMock->expects($this->never())->method('getCodes'); + + $this->processorMock->expects($this->once()) + ->method('execute'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } + + /** + * @return void + */ + public function testExecuteWithMissingAdminhtmlLocale() + { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->never())->method('process'); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn([]); + + $this->processorMock->expects($this->once()) + ->method('execute'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } } diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index 0c32baebf12df..d40ed3144e7e6 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -35,6 +35,12 @@ </argument> </arguments> </type> + <type name="Magento\Deploy\Console\Command\App\ConfigImportCommand"> + <arguments> + <argument name="adminhtmlAreaProcessor" xsi:type="object">Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor\Proxy</argument> + <argument name="areaList" xsi:type="object">Magento\Framework\App\AreaList\Proxy</argument> + </arguments> + </type> <type name="Magento\Deploy\Model\Filesystem"> <arguments> <argument name="shell" xsi:type="object">Magento\Framework\App\Shell</argument> diff --git a/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php b/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php index 9e473ccaa2d92..8bd827958df15 100644 --- a/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php +++ b/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php @@ -116,7 +116,7 @@ private function getUrnDictionary(OutputInterface $output) $files = $this->filesUtility->getXmlCatalogFiles('*.xml'); $files = array_merge($files, $this->filesUtility->getXmlCatalogFiles('*.xsd')); - $urns = [[]]; + $urns = []; foreach ($files as $file) { // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileDir = dirname($file[0]); @@ -130,7 +130,7 @@ private function getUrnDictionary(OutputInterface $output) $urns[] = $matches[1]; } } - $urns = array_unique(array_merge(...$urns)); + $urns = array_unique(array_merge([], ...$urns)); $paths = []; foreach ($urns as $urn) { try { diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 204094571ba3b..c5eb27b21e58b 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -826,10 +826,9 @@ protected function _getAllItems() $fullItems[] = array_fill(0, $qty, $this->_getWeight($itemWeight)); } } - if ($fullItems) { - $fullItems = array_merge(...$fullItems); - sort($fullItems); - } + + $fullItems = array_merge([], ...$fullItems); + sort($fullItems); return $fullItems; } diff --git a/app/code/Magento/Directory/Model/AllowedCountries.php b/app/code/Magento/Directory/Model/AllowedCountries.php index 2ceeb70ba5b01..69326439edc03 100644 --- a/app/code/Magento/Directory/Model/AllowedCountries.php +++ b/app/code/Magento/Directory/Model/AllowedCountries.php @@ -62,11 +62,11 @@ public function getAllowedCountries( switch ($scope) { case ScopeInterface::SCOPE_WEBSITES: case ScopeInterface::SCOPE_STORES: - $allowedCountries = [[]]; + $allowedCountries = []; foreach ($scopeCode as $singleFilter) { $allowedCountries[] = $this->getCountriesFromConfig($this->getSingleScope($scope), $singleFilter); } - $allowedCountries = array_merge(...$allowedCountries); + $allowedCountries = array_merge([], ...$allowedCountries); break; default: $allowedCountries = $this->getCountriesFromConfig($scope, $scopeCode); diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index f7230df6e86ea..b574170ac5d3c 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -73,7 +73,7 @@ public function getConfigCurrencies(string $path) */ private function getConfigForAllStores(string $path) { - $storesResult = [[]]; + $storesResult = []; foreach ($this->storeManager->getStores() as $store) { $storesResult[] = explode( ',', @@ -81,7 +81,7 @@ private function getConfigForAllStores(string $path) ); } - return array_merge(...$storesResult); + return array_merge([], ...$storesResult); } /** diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForUruguay.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForUruguay.php new file mode 100644 index 0000000000000..d9aa041c1f7d1 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForUruguay.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Directory\Setup\DataInstallerFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add Uruguay States/Regions + */ +class AddDataForUruguay implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForUruguay() + ); + + return $this; + } + + /** + * Uruguay states data. + * + * @return array + */ + private function getDataForUruguay(): array + { + return [ + ['UY', 'UY-AR', 'Artigas'], + ['UY', 'UY-CA', 'Canelones'], + ['UY', 'UY-CL', 'Cerro Largo'], + ['UY', 'UY-CO', 'Colonia'], + ['UY', 'UY-DU', 'Durazno'], + ['UY', 'UY-FS', 'Flores'], + ['UY', 'UY-FD', 'Florida'], + ['UY', 'UY-LA', 'Lavalleja'], + ['UY', 'UY-MA', 'Maldonado'], + ['UY', 'UY-MO', 'Montevideo'], + ['UY', 'UY-PA', 'Paysandu'], + ['UY', 'UY-RN', 'Río Negro'], + ['UY', 'UY-RV', 'Rivera'], + ['UY', 'UY-RO', 'Rocha'], + ['UY', 'UY-SA', 'Salto'], + ['UY', 'UY-SJ', 'San José'], + ['UY', 'UY-SO', 'Soriano'], + ['UY', 'UY-TA', 'Tacuarembó'], + ['UY', 'UY-TT', 'Treinta y Tres'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php index 69f417e1ea732..f53f1e97a872d 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php @@ -152,7 +152,7 @@ protected function _prepareOptionValues( $inputType = ''; } - $values = [[]]; + $values = []; $isSystemAttribute = is_array($optionCollection); if ($isSystemAttribute) { $values[] = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); @@ -168,7 +168,7 @@ protected function _prepareOptionValues( } } - return array_merge(...$values); + return array_merge([], ...$values); } /** diff --git a/app/code/Magento/Eav/Model/Form.php b/app/code/Magento/Eav/Model/Form.php index 074c6cf46a2f4..b06c084cf6675 100644 --- a/app/code/Magento/Eav/Model/Form.php +++ b/app/code/Magento/Eav/Model/Form.php @@ -487,11 +487,11 @@ public function validateData(array $data) { $validator = $this->_getValidator($data); if (!$validator->isValid($this->getEntity())) { - $messages = [[]]; + $messages = []; foreach ($validator->getMessages() as $errorMessages) { $messages[] = (array)$errorMessages; } - return array_merge(...$messages); + return array_merge([], ...$messages); } return true; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Helper.php b/app/code/Magento/Eav/Model/ResourceModel/Helper.php index fc8a47994a6aa..c81db40c608a8 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Helper.php @@ -19,6 +19,7 @@ class Helper extends \Magento\Framework\DB\Helper * @param \Magento\Framework\App\ResourceConnection $resource * @param string $modulePrefix * @codeCoverageIgnore + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod */ public function __construct(\Magento\Framework\App\ResourceConnection $resource, $modulePrefix = 'Magento_Eav') { @@ -117,7 +118,7 @@ public function getLoadAttributesSelectGroups($selects) if (array_key_exists('all', $mainGroup)) { // it is better to call array_merge once after loop instead of calling it on each loop - $mainGroup['all'] = array_merge(...$mainGroup['all']); + $mainGroup['all'] = array_merge([], ...$mainGroup['all']); } return array_values($mainGroup); diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php index b276b67ff7fba..980842d6233b1 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php @@ -40,12 +40,12 @@ public function __construct(array $providers) */ public function getFields(array $context = []): array { - $allAttributes = [[]]; + $allAttributes = []; foreach ($this->providers as $provider) { $allAttributes[] = $provider->getFields($context); } - return array_merge(...$allAttributes); + return array_merge([], ...$allAttributes); } } diff --git a/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php b/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php index 197be38fb7f5f..8dc153f28c162 100644 --- a/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php @@ -49,6 +49,6 @@ public function getIdentities() if ($this->getItem()) { $identities[] = $this->getGroupedProduct()->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php b/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php index 97dc90ec93493..78ae4047c0aad 100644 --- a/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php @@ -32,10 +32,10 @@ protected function _getChildProducts() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getChildProducts() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php new file mode 100644 index 0000000000000..0bb102f34dd2d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Inventory; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Catalog\Api\Data\ProductInterface as Product; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\GroupedProduct\Model\ResourceModel\Product\Link; +use Magento\Framework\App\ResourceConnection; + +/** + * Process parent stock item for grouped product + */ +class ParentItemProcessor implements ParentItemProcessorInterface +{ + /** + * @var Grouped + */ + private $groupedType; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * Product metadata pool + * + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @param Grouped $groupedType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + * @param ResourceConnection $resource + * @param MetadataPool $metadataPool + */ + public function __construct( + Grouped $groupedType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration, + ResourceConnection $resource, + MetadataPool $metadataPool + ) { + $this->groupedType = $groupedType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockItemRepository = $stockItemRepository; + $this->resource = $resource; + $this->metadataPool = $metadataPool; + } + + /** + * Process parent products + * + * @param Product $product + * @return void + */ + public function process(Product $product) + { + $parentIds = $this->getParentEntityIdsByChild($product->getId()); + foreach ($parentIds as $productId) { + $this->processStockForParent((int)$productId); + } + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + private function processStockForParent(int $productId) + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); + $criteria->setProductsFilter($groupedChildrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $groupedChildrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $groupedChildrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { + $parentStockItem->setIsInStock($groupedChildrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool + { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } + + /** + * Retrieve parent ids array by child id + * + * @param int $childId + * @return string[] + */ + private function getParentEntityIdsByChild($childId) + { + $select = $this->resource->getConnection() + ->select() + ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) + ->join( + ['e' => $this->resource->getTableName('catalog_product_entity')], + 'e.' . + $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', + ['e.entity_id'] + ) + ->where('l.linked_product_id = ?', $childId) + ->where( + 'link_type_id = ?', + Link::LINK_TYPE_GROUPED + ); + + return $this->resource->getConnection()->fetchCol($select); + } +} diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index 43678d0ad7a82..d9534c6d3fe7d 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -105,4 +105,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="grouped" xsi:type="object"> Magento\GroupedProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml b/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml index 831dc5a765dfb..becd7ca8079da 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml +++ b/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml @@ -58,7 +58,7 @@ <label translate="true">Status</label> <dataScope>status</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index 55992c92226af..87cd4cf346288 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -226,6 +226,7 @@ protected function _prepareForm() 'title' => __('Select File to Import'), 'required' => true, 'class' => 'input-file', + 'onchange' => 'varienImport.refreshLoadedFileLastModified(this);', 'note' => __( 'File must be saved in UTF-8 encoding for proper import' ), @@ -282,7 +283,7 @@ protected function getDownloadSampleFileHtml() private function getImportBehaviorTooltip() { $html = '<div class="admin__field-tooltip tooltip"> - <a class="admin__field-tooltip-action action-help" target="_blank" title="What is this?" + <a class="admin__field-tooltip-action action-help" target="_blank" title="What is this?" href="https://docs.magento.com/m2/ce/user_guide/system/data-import.html"><span>' . __('What is this?') . '</span></a></div>'; diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 5ea6227231543..2f8bfdcf70a5e 100644 --- a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -242,7 +242,7 @@ public function getAllErrors() } $errors = array_values($this->items['rows']); - return array_merge(...$errors); + return array_merge([], ...$errors); } /** @@ -253,14 +253,14 @@ public function getAllErrors() */ public function getErrorsByCode(array $codes) { - $result = [[]]; + $result = []; foreach ($codes as $code) { if (isset($this->items['codes'][$code])) { $result[] = $this->items['codes'][$code]; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index a4943fe72826f..a91a76612fd9f 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -127,3 +127,4 @@ Summary,Summary "File %1 deleted","File %1 deleted" "Please provide valid export file name","Please provide valid export file name" "%1 is not a valid file","%1 is not a valid file" +"Content of uploaded file was changed, please re-upload the file","Content of uploaded file was changed, please re-upload the file" diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml index 69779baba381d..d512ce8182ede 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml @@ -7,6 +7,10 @@ <?php /** @var $block \Magento\ImportExport\Block\Adminhtml\Import\Edit\Before */ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +$fieldNameSourceFile = \Magento\ImportExport\Model\Import::FIELD_NAME_SOURCE_FILE; +$uploaderErrorMessage = $block->escapeHtml( + __('Content of uploaded file was changed, please re-upload the file') +); ?> <?php $scriptString = <<<script @@ -49,6 +53,12 @@ require([ */ sampleFilesBaseUrl: '{$block->escapeJs($block->getUrl('*/*/download/', ['filename' => 'entity-name']))}', + /** + * Loaded file last modified + * @type {int|null} + */ + loadedFileLastModified: null, + /** * Reset selected index * @param {string} elementId @@ -162,11 +172,50 @@ require([ } }, + /** + * Refresh loaded file last modified + */ + refreshLoadedFileLastModified: function(e) { + if (jQuery(e)[0].files.length > 0) { + this.loadedFileLastModified = jQuery(e)[0].files[0].lastModified; + } else { + this.loadedFileLastModified = null; + } + }, + /** * Post form data to dynamic iframe. * @param {string} newActionUrl OPTIONAL Change form action to this if specified */ postToFrame: function(newActionUrl) { + var fileUploader = document.getElementById('{$fieldNameSourceFile}'); + + if (fileUploader.files.length > 0) { + var file = fileUploader.files[0], + ifrElName = this.ifrElemName, + reader = new FileReader(); + + reader.readAsText(file, "UTF-8"); + + reader.onerror = function () { + jQuery('body').loader('hide'); + alert({ + content: '{$uploaderErrorMessage}' + }); + fileUploader.value = null; + jQuery('iframe#' + ifrElName).remove(); + return; + } + + if (file.lastModified !== this.loadedFileLastModified) { + alert({ + content: '{$uploaderErrorMessage}' + }); + fileUploader.value = null; + return; + } + } + if (!jQuery('[name="' + this.ifrElemName + '"]').length) { jQuery('body').append('<iframe name="' + this.ifrElemName + '" id="' + this.ifrElemName + '"/>'); jQuery('iframe#' + this.ifrElemName).attr('display', 'none'); diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index e7517ba0c8818..735bc85244bdb 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -125,16 +125,16 @@ protected function getIndexers(InputInterface $input) return $indexers; } - $relatedIndexers = [[]]; - $dependentIndexers = [[]]; + $relatedIndexers = []; + $dependentIndexers = []; foreach ($indexers as $indexer) { $relatedIndexers[] = $this->getRelatedIndexerIds($indexer->getId()); $dependentIndexers[] = $this->getDependentIndexerIds($indexer->getId()); } - $relatedIndexers = $relatedIndexers ? array_unique(array_merge(...$relatedIndexers)) : []; - $dependentIndexers = $dependentIndexers ? array_merge(...$dependentIndexers) : []; + $relatedIndexers = array_unique(array_merge([], ...$relatedIndexers)); + $dependentIndexers = array_merge([], ...$dependentIndexers); $invalidRelatedIndexers = []; foreach ($relatedIndexers as $relatedIndexer) { @@ -165,12 +165,12 @@ protected function getIndexers(InputInterface $input) */ private function getRelatedIndexerIds(string $indexerId): array { - $relatedIndexerIds = [[]]; + $relatedIndexerIds = []; foreach ($this->getDependencyInfoProvider()->getIndexerIdsToRunBefore($indexerId) as $relatedIndexerId) { $relatedIndexerIds[] = [$relatedIndexerId]; $relatedIndexerIds[] = $this->getRelatedIndexerIds($relatedIndexerId); } - $relatedIndexerIds = $relatedIndexerIds ? array_unique(array_merge(...$relatedIndexerIds)) : []; + $relatedIndexerIds = array_unique(array_merge([], ...$relatedIndexerIds)); return $relatedIndexerIds; } @@ -183,7 +183,7 @@ private function getRelatedIndexerIds(string $indexerId): array */ private function getDependentIndexerIds(string $indexerId): array { - $dependentIndexerIds = [[]]; + $dependentIndexerIds = []; foreach (array_keys($this->getConfig()->getIndexers()) as $id) { $dependencies = $this->getDependencyInfoProvider()->getIndexerIdsToRunBefore($id); if (array_search($indexerId, $dependencies) !== false) { @@ -191,7 +191,7 @@ private function getDependentIndexerIds(string $indexerId): array $dependentIndexerIds[] = $this->getDependentIndexerIds($id); } } - $dependentIndexerIds = $dependentIndexerIds ? array_unique(array_merge(...$dependentIndexerIds)) : []; + $dependentIndexerIds = array_unique(array_merge([], ...$dependentIndexerIds)); return $dependentIndexerIds; } diff --git a/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php b/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php index 2d323fea34e7d..b6ea810666b9b 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php @@ -222,13 +222,13 @@ public function isTreeEmpty() */ protected function _getAllResourceIds(array $resources) { - $resourceIds = [[]]; + $resourceIds = []; foreach ($resources as $resource) { $resourceIds[] = [$resource['id']]; if (isset($resource['children'])) { $resourceIds[] = $this->_getAllResourceIds($resource['children']); } } - return array_merge(...$resourceIds); + return array_merge([], ...$resourceIds); } } diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml new file mode 100644 index 0000000000000..92fea20a83157 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.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="StorefrontAssertAppliedFilterActionGroup"> + <annotations> + <description>Asserts applied filter label and value on storefront category page.</description> + </annotations> + <arguments> + <argument name="attributeLabel" type="string"/> + <argument name="attributeOptionLabel" type="string"/> + </arguments> + <see selector="{{StorefrontLayeredNavigationSection.appliedFilterLabel('1')}}" userInput="{{attributeLabel}}" stepKey="seeAppliedFilterLabel"/> + <see selector="{{StorefrontLayeredNavigationSection.appliedFilterValue('1')}}" userInput="{{attributeOptionLabel}}" stepKey="seeAppliedFilterValue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml new file mode 100644 index 0000000000000..f6fe2b20185e6 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.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="StorefrontFilterCategoryPageByAttributeOptionActionGroup"> + <annotations> + <description>Filters storefront category page by given filterable attribute and attribute option.</description> + </annotations> + <arguments> + <argument name="attributeLabel" type="string"/> + <argument name="attributeOptionLabel" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" stepKey="waitForFilterVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandFilter"/> + <click selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel(attributeOptionLabel)}}" stepKey="clickOnOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml index d3a3005c296b2..d8a103116ef06 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml @@ -9,5 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontLayeredNavigationSection"> <element name="shoppingOptionsByName" type="button" selector="//*[text()='Shopping Options']/..//*[contains(text(),'{{arg}}')]" parameterized="true"/> + <element name="appliedFilterLabel" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-label" parameterized="true"/> + <element name="appliedFilterValue" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-value" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml new file mode 100644 index 0000000000000..0cd115d3febeb --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml @@ -0,0 +1,103 @@ +<?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="StorefrontDropdownAttributeInLayeredNavigationTest"> + <annotations> + <features value="LayeredNavigation"/> + <stories value="Product attributes in Layered Navigation"/> + <title value="[ES] Search with Layered Navigation and different types of attribute products."/> + <description value="Filtering by dropdown attribute in Layered navigation"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-36326"/> + <group value="layeredNavigation"/> + <group value="catalog"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="dropdownProductAttribute" stepKey="createDropdownProductAttribute"/> + <createData entity="productAttributeOption" stepKey="firstDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="secondDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </getData> + <createData entity="AddToDefaultSet" stepKey="AddDropdownProductAttributeToAttributeSet"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + <requiredEntity createDataKey="getFirstDropdownProductAttributeOption"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + <requiredEntity createDataKey="getSecondDropdownProductAttributeOption"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createDropdownProductAttribute" stepKey="deleteDropdownProductAttribute"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategory"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertFirstAttributeOptionPresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getFirstDropdownProductAttributeOption.label$"/> + <argument name="attributeOptionPosition" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertSecondAttributeOptionPresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> + <argument name="attributeOptionPosition" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontFilterCategoryPageByAttributeOptionActionGroup" stepKey="filterCategoryByFirstOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getFirstDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertAppliedFilterActionGroup" stepKey="assertFilterByFirstOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getFirstDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="assertFirstProductOnCatalogPage"> + <argument name="productName" value="$createFirstProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup" stepKey="assertSecondProductIsMissingOnCatalogPage"> + <argument name="productName" value="$createSecondProduct.name$"/> + </actionGroup> + <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeSideBarFilter"/> + <actionGroup ref="StorefrontFilterCategoryPageByAttributeOptionActionGroup" stepKey="filterCategoryBySecondOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertAppliedFilterActionGroup" stepKey="assertFilterBySecondOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="assertSecondProductOnCatalogPage"> + <argument name="productName" value="$createSecondProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup" stepKey="assertFirstProductIsMissingOnCatalogPage"> + <argument name="productName" value="$createFirstProduct.name$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml b/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml index 6b65d184b462a..83f40ab4911e7 100644 --- a/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml +++ b/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php -/** @var $block \Magento\LayeredNavigation\Block\Navigation\FilterRenderer */ +/** @var \Magento\LayeredNavigation\Block\Navigation\FilterRenderer $block */ +/** @var \Magento\Framework\Escaper $escaper */ /** @var \Magento\LayeredNavigation\ViewModel\Layer\Filter $viewModel */ $viewModel = $block->getData('product_layer_view_model'); ?> @@ -16,28 +16,29 @@ $viewModel = $block->getData('product_layer_view_model'); <?php foreach ($filterItems as $filterItem): ?> <li class="item"> <?php if ($filterItem->getCount() > 0): ?> - <a href="<?= $block->escapeUrl($filterItem->getUrl()) ?>" rel="nofollow"> - <?= /* @noEscape */ $filterItem->getLabel() ?> - <?php if ($viewModel->shouldDisplayProductCountOnLayer()): ?> - <span class="count"><?= /* @noEscape */ (int)$filterItem->getCount() ?> - <span class="filter-count-label"> - <?php if ($filterItem->getCount() == 1): - ?> <?= $block->escapeHtml(__('item')) ?><?php + <a + href="<?= $escaper->escapeUrl($filterItem->getUrl()) ?>" + rel="nofollow" + ><?= /* @noEscape */ $filterItem->getLabel() ?><?php + if ($viewModel->shouldDisplayProductCountOnLayer()): ?><span + class="count"><?= /* @noEscape */ (int) $filterItem->getCount() ?><span + class="filter-count-label"><?php + if ($filterItem->getCount() == 1): ?> + <?= $escaper->escapeHtml(__('item')) ?><?php else: - ?> <?= $block->escapeHtml(__('item')) ?><?php + ?><?= $escaper->escapeHtml(__('item')) ?><?php endif;?></span></span> - <?php endif; ?> - </a> + <?php endif; ?></a> <?php else: ?> - <?= /* @noEscape */ $filterItem->getLabel() ?> - <?php if ($viewModel->shouldDisplayProductCountOnLayer()): ?> - <span class="count"><?= /* @noEscape */ (int)$filterItem->getCount() ?> - <span class="filter-count-label"> - <?php if ($filterItem->getCount() == 1): - ?><?= $block->escapeHtml(__('items')) ?><?php - else: - ?><?= $block->escapeHtml(__('items')) ?><?php - endif;?></span></span> + <?= /* @noEscape */ $filterItem->getLabel() ?><?php + if ($viewModel->shouldDisplayProductCountOnLayer()): ?><span + class="count"><?= /* @noEscape */ (int) $filterItem->getCount() ?><span + class="filter-count-label"><?php + if ($filterItem->getCount() == 1): ?> + <?= $escaper->escapeHtml(__('items')) ?><?php + else: + ?><?= $escaper->escapeHtml(__('items')) ?><?php + endif;?></span></span> <?php endif; ?> <?php endif; ?> </li> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml index ea06263901b9e..26ffc877bbe42 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -23,13 +23,18 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCachesAfterSet"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> @@ -39,7 +44,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAfter"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCachesDefault"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Verify Login as Customer Login action works correctly from Customer page --> diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php index fd61f96b300d6..472d1cc631290 100644 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php +++ b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php @@ -131,7 +131,7 @@ public function run() ]; if ($maxMessages) { - $arguments[] = '--max-messages=' . $maxMessages; + $arguments[] = '--max-messages=' . min($consumer->getMaxMessages() ?? $maxMessages, $maxMessages); } $command = $php . ' ' . BP . '/bin/magento queue:consumers:start %s %s' diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 1ea2dc2618778..942741e9f7975 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -10,6 +10,8 @@ use Magento\Quote\Model\Quote\Address; use Magento\Checkout\Helper\Data as CheckoutHelper; use Magento\Framework\App\ObjectManager; +use Magento\Quote\Model\Quote\Address\Total\Collector; +use Magento\Store\Model\ScopeInterface; /** * Multishipping checkout overview information @@ -429,9 +431,12 @@ public function getBillingAddressTotals() */ public function renderTotals($totals, $colspan = null) { - //check if the shipment is multi shipment + // check if the shipment is multi shipment $totals = $this->getMultishippingTotals($totals); + // sort totals by configuration settings + $totals = $this->sortTotals($totals); + if ($colspan === null) { $colspan = 3; } @@ -481,4 +486,38 @@ protected function _getRowItemRenderer($type) } return $renderer; } + + /** + * Sort total information based on configuration settings. + * + * @param array $totals + * @return array + */ + private function sortTotals($totals): array + { + $sortedTotals = []; + $sorts = $this->_scopeConfig->getValue( + Collector::XML_PATH_SALES_TOTALS_SORT, + ScopeInterface::SCOPE_STORES + ); + + $sorted = []; + foreach ($sorts as $code => $sortOrder) { + $sorted[$sortOrder] = $code; + } + ksort($sorted); + + foreach ($sorted as $code) { + if (isset($totals[$code])) { + $sortedTotals[$code] = $totals[$code]; + } + } + + $notSorted = array_diff(array_keys($totals), array_keys($sortedTotals)); + foreach ($notSorted as $code) { + $sortedTotals[$code] = $totals[$code]; + } + + return $sortedTotals; + } } diff --git a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php index 7da77030f308a..2d044afd32c70 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php @@ -8,6 +8,7 @@ namespace Magento\Multishipping\Test\Unit\Block\Checkout; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; @@ -67,6 +68,11 @@ class OverviewTest extends TestCase */ private $urlBuilderMock; + /** + * @var MockObject + */ + private $scopeConfigMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -85,6 +91,7 @@ protected function setUp(): void $this->createMock(Multishipping::class); $this->quoteMock = $this->createMock(Quote::class); $this->urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); + $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->model = $objectManager->getObject( Overview::class, [ @@ -92,7 +99,8 @@ protected function setUp(): void 'totalsCollector' => $this->totalsCollectorMock, 'totalsReader' => $this->totalsReaderMock, 'multishipping' => $this->checkoutMock, - 'urlBuilder' => $this->urlBuilderMock + 'urlBuilder' => $this->urlBuilderMock, + '_scopeConfig' => $this->scopeConfigMock ] ); } @@ -187,4 +195,44 @@ public function testGetVirtualProductEditUrl() $this->urlBuilderMock->expects($this->once())->method('getUrl')->with('checkout/cart', [])->willReturn($url); $this->assertEquals($url, $this->model->getVirtualProductEditUrl()); } + + /** + * Test sort total information + * + * @return void + */ + public function testSortCollectors(): void + { + $sorts = [ + 'discount' => 40, + 'subtotal' => 10, + 'tax' => 20, + 'shipping' => 30, + ]; + + $this->scopeConfigMock->method('getValue') + ->with('sales/totals_sort', 'stores') + ->willReturn($sorts); + + $totalsNotSorted = [ + 'subtotal' => [], + 'shipping' => [], + 'tax' => [], + ]; + + $totalsExpected = [ + 'subtotal' => [], + 'tax' => [], + 'shipping' => [], + ]; + + $method = new \ReflectionMethod($this->model, 'sortTotals'); + $method->setAccessible(true); + $result = $method->invoke($this->model, $totalsNotSorted); + + $this->assertEquals( + $totalsExpected, + $result + ); + } } diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index bbc199c91263a..112accbae8070 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -140,10 +140,9 @@ public function collectRates(RateRequest $request) $freePackageValue += $item->getBaseRowTotal(); } } - $oldValue = $request->getPackageValue(); - $newPackageValue = $oldValue - $freePackageValue; - $request->setPackageValue($newPackageValue); - $request->setPackageValueWithDiscount($newPackageValue); + + $request->setPackageValue($request->getPackageValue() - $freePackageValue); + $request->setPackageValueWithDiscount($request->getPackageValueWithDiscount() - $freePackageValue); } if (!$request->getConditionName()) { diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml new file mode 100644 index 0000000000000..d225e5fa28f97 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -0,0 +1,101 @@ +<?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="SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest"> + <annotations> + <features value="Shipping"/> + <stories value="Offline Shipping Methods"/> + <title value="SalesRule Discount Is Applied On PackageValue For TableRate"/> + <description value="SalesRule Discount Is Applied On PackageValue For TableRate"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-38271"/> + <group value="shipping"/> + </annotations> + <before> + <!-- Add simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">13.00</field> + </createData> + + <!-- Create cart price rule --> + <createData entity="ActiveSalesRuleForNotLoggedIn" stepKey="createCartPriceRule"/> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Enable Table Rate method and save config --> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + + <!-- Uncheck Use Default checkbox for Default Condition --> + <uncheckOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" stepKey="disableUseDefaultCondition"/> + + <!-- Make sure you have Condition Price vs. Destination --> + <selectOption selector="{{AdminShippingMethodTableRatesSection.condition}}" userInput="{{TableRateShippingMethodConfig.package_value_with_discount}}" stepKey="setCondition"/> + + <!-- Import file and save config --> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="usa_tablerates.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + </before> + <after> + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Check Use Default checkbox for Default Condition and Active --> + <checkOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" stepKey="enableUseDefaultCondition"/> + <checkOption selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="enableUseDefaultActive"/> + + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Remove simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete sales rule --> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Assert that table rate value is correct for US --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCheckout"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <waitForPageLoad stepKey="waitForSelectCountry"/> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$5.99" stepKey="seeShippingForUS"/> + + <!-- Apply Coupon --> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyDiscount"> + <argument name="coupon" value="$$createCouponForCartPriceRule$$"/> + </actionGroup> + + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$7.99" stepKey="seeShippingForUSWithDiscount"/> + </test> +</tests> diff --git a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php index 1b64f3b635c03..6aff8aef2c2d9 100644 --- a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php +++ b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php @@ -84,7 +84,7 @@ public function afterGenerateElements(Layout $subject) public function afterGetOutput(Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { - $tags = [[]]; + $tags = []; $isVarnish = $this->config->getType() === Config::VARNISH; foreach ($subject->getAllBlocks() as $block) { @@ -96,7 +96,7 @@ public function afterGetOutput(Layout $subject, $result) $tags[] = $block->getIdentities(); } } - $tags = array_unique(array_merge(...$tags)); + $tags = array_unique(array_merge([], ...$tags)); $tags = $this->pageCacheTagsPreprocessor->process($tags); $this->response->setHeader('X-Magento-Tags', implode(',', $tags)); } diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index 8c8d13300849e..af42554484117 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -59,8 +59,8 @@ public function __construct( public function validate(array $validationSubject) { $isValid = true; - $failsDescriptionAggregate = [[]]; - $errorCodesAggregate = [[]]; + $failsDescriptionAggregate = []; + $errorCodesAggregate = []; foreach ($this->validators as $key => $validator) { $result = $validator->validate($validationSubject); if (!$result->isValid()) { @@ -76,8 +76,8 @@ public function validate(array $validationSubject) return $this->createResult( $isValid, - array_merge(...$failsDescriptionAggregate), - array_merge(...$errorCodesAggregate) + array_merge([], ...$failsDescriptionAggregate), + array_merge([], ...$errorCodesAggregate) ); } } diff --git a/app/code/Magento/Payment/Model/PaymentMethodList.php b/app/code/Magento/Payment/Model/PaymentMethodList.php index 4e400dbf0c906..b27d02bbdff4b 100644 --- a/app/code/Magento/Payment/Model/PaymentMethodList.php +++ b/app/code/Magento/Payment/Model/PaymentMethodList.php @@ -6,53 +6,57 @@ namespace Magento\Payment\Model; use Magento\Payment\Api\Data\PaymentMethodInterface; +use Magento\Payment\Api\Data\PaymentMethodInterfaceFactory; +use Magento\Payment\Api\PaymentMethodListInterface; +use Magento\Payment\Helper\Data; +use UnexpectedValueException; -/** - * Payment method list class. - */ -class PaymentMethodList implements \Magento\Payment\Api\PaymentMethodListInterface +class PaymentMethodList implements PaymentMethodListInterface { /** - * @var \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory + * @var PaymentMethodInterfaceFactory */ private $methodFactory; /** - * @var \Magento\Payment\Helper\Data + * @var Data */ private $helper; /** - * @param \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory $methodFactory - * @param \Magento\Payment\Helper\Data $helper + * @param PaymentMethodInterfaceFactory $methodFactory + * @param Data $helper */ public function __construct( - \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory $methodFactory, - \Magento\Payment\Helper\Data $helper + PaymentMethodInterfaceFactory $methodFactory, + Data $helper ) { $this->methodFactory = $methodFactory; $this->helper = $helper; } /** - * {@inheritdoc} + * @inheritDoc */ public function getList($storeId) { $methodsCodes = array_keys($this->helper->getPaymentMethods()); - $methodsInstances = array_map( function ($code) { - return $this->helper->getMethodInstance($code); + try { + return $this->helper->getMethodInstance($code); + } catch (UnexpectedValueException $e) { + return null; + } }, $methodsCodes ); - $methodsInstances = array_filter($methodsInstances, function (MethodInterface $method) { - return !($method instanceof \Magento\Payment\Model\Method\Substitution); + $methodsInstances = array_filter($methodsInstances, function ($method) { + return $method && !($method instanceof \Magento\Payment\Model\Method\Substitution); }); - @uasort( + uasort( $methodsInstances, function (MethodInterface $a, MethodInterface $b) use ($storeId) { return (int)$a->getConfigData('sort_order', $storeId) - (int)$b->getConfigData('sort_order', $storeId); @@ -76,7 +80,7 @@ function (MethodInterface $methodInstance) use ($storeId) { } /** - * {@inheritdoc} + * @inheritDoc */ public function getActiveList($storeId) { diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php index 29d4a5bd1f25c..95dc8ee487edf 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php @@ -174,6 +174,7 @@ protected function _processPaypalApiError($exception) $this->_redirectSameToken(); break; case ApiProcessableException::API_ADDRESS_MATCH_FAIL: + case ApiProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED: $this->redirectToOrderReviewPageAndShowError($exception->getUserMessage()); break; case ApiProcessableException::API_UNABLE_TRANSACTION_COMPLETE: diff --git a/app/code/Magento/Paypal/Model/Api/Nvp.php b/app/code/Magento/Paypal/Model/Api/Nvp.php index b35f783482e06..30bfb660aa6f1 100644 --- a/app/code/Magento/Paypal/Model/Api/Nvp.php +++ b/app/code/Magento/Paypal/Model/Api/Nvp.php @@ -1286,15 +1286,6 @@ protected function _handleCallErrors($response) ); $this->_logger->critical($exceptionLogMessage); - /** - * The response code 10415 'Transaction has already been completed for this token' - * must not fails place order. The old Paypal interface does not lock 'Send' button - * it may result to re-send data. - */ - if (in_array((string)ProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED, $this->_callErrors)) { - return; - } - $exceptionPhrase = __('PayPal gateway has rejected request. %1', $errorMessages); /** @var \Magento\Framework\Exception\LocalizedException $exception */ diff --git a/app/code/Magento/Paypal/Model/Api/ProcessableException.php b/app/code/Magento/Paypal/Model/Api/ProcessableException.php index 40ee6d98c4381..12a11ff442418 100644 --- a/app/code/Magento/Paypal/Model/Api/ProcessableException.php +++ b/app/code/Magento/Paypal/Model/Api/ProcessableException.php @@ -67,6 +67,12 @@ public function getUserMessage() . ' Please contact us so we can assist you.' ); break; + case self::API_TRANSACTION_HAS_BEEN_COMPLETED: + $message = __( + 'A successful payment transaction has already been completed.' + . ' Please, check if the order has been placed.' + ); + break; case self::API_ADDRESS_MATCH_FAIL: $message = __( 'A match of the Shipping Address City, State, and Postal Code failed.' diff --git a/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php b/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php index 61410499e956e..a3cef539dc17b 100644 --- a/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php +++ b/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php @@ -84,7 +84,7 @@ public function modify(array $initialStructure) */ private function getMoveInstructions($section, $data) { - $moved = [[]]; + $moved = []; if (array_key_exists('children', $data)) { foreach ($data['children'] as $childSection => $childData) { @@ -106,6 +106,6 @@ private function getMoveInstructions($section, $data) ]; } - return array_merge(...$moved); + return array_merge([], ...$moved); } } diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php index 946c0fd4c66ca..39b1c6f7e3c28 100644 --- a/app/code/Magento/Paypal/Model/Express.php +++ b/app/code/Magento/Paypal/Model/Express.php @@ -276,6 +276,7 @@ protected function _setApiProcessableErrors() ApiProcessableException::API_MAXIMUM_AMOUNT_FILTER_DECLINE, ApiProcessableException::API_OTHER_FILTER_DECLINE, ApiProcessableException::API_ADDRESS_MATCH_FAIL, + ApiProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED, self::$authorizationExpiredCode ] ); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php index 42b99ae8e7459..69aa9b99bc9e7 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php @@ -305,8 +305,7 @@ public function testGetDebugReplacePrivateDataKeys() /** * Tests case if obtained response with code 10415 'Transaction has already - * been completed for this token'. It must does not throws the exception and - * must returns response array. + * been completed for this token'. It must throw the ProcessableException. */ public function testCallTransactionHasBeenCompleted() { @@ -317,15 +316,10 @@ public function testCallTransactionHasBeenCompleted() ->method('read') ->willReturn($response); $this->model->setProcessableErrors($processableErrors); - $this->customLoggerMock->expects($this->once()) - ->method('debug'); - $expectedResponse = [ - 'ACK' => 'Failure', - 'L_ERRORCODE0' => '10415', - 'L_SHORTMESSAGE0' => 'Message.', - 'L_LONGMESSAGE0' => 'Long Message.' - ]; - $this->assertEquals($expectedResponse, $this->model->call('some method', ['data' => 'some data'])); + $this->expectExceptionMessageMatches('/PayPal gateway has rejected request/'); + $this->expectException(ProcessableException::class); + + $this->model->call('DoExpressCheckout', ['data' => 'some data']); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php index dc54b71324a9b..a6a18418e92ac 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php @@ -162,14 +162,14 @@ public function testMovedToTargetSpecialGroup() */ private function fetchAllAvailableGroups($structure) { - $availableGroups = [[]]; + $availableGroups = []; foreach ($structure as $group => $data) { $availableGroups[] = [$group]; if (isset($data['children'])) { $availableGroups[] = $this->fetchAllAvailableGroups($data['children']); } } - $availableGroups = array_merge(...$availableGroups); + $availableGroups = array_merge([], ...$availableGroups); $availableGroups = array_values(array_unique($availableGroups)); sort($availableGroups); return $availableGroups; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php index 8cf2fb91a8452..14dcc4fc4229d 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php @@ -53,6 +53,7 @@ class ExpressTest extends TestCase ApiProcessableException::API_MAXIMUM_AMOUNT_FILTER_DECLINE, ApiProcessableException::API_OTHER_FILTER_DECLINE, ApiProcessableException::API_ADDRESS_MATCH_FAIL, + ApiProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED ]; /** diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index 8db6285dc157e..a8f26b422dc7c 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -737,3 +737,4 @@ User,User "Please enter at least 0 and at most 65535","Please enter at least 0 and at most 65535" "Order is suspended as an account verification transaction is suspected to be fraudulent.","Order is suspended as an account verification transaction is suspected to be fraudulent." "Payment can't be accepted since transaction was rejected by merchant.","Payment can't be accepted since transaction was rejected by merchant." +"A successful payment transaction has already been completed. Please, check if the order has been placed.","A successful payment transaction has already been completed. Please, check if the order has been placed." diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index b6504d528fbe4..35b07ebdb7c44 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -5,6 +5,7 @@ */ namespace Magento\Persistent\Model; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Framework\App\ObjectManager; use Magento\Persistent\Helper\Data; @@ -64,6 +65,11 @@ class QuoteManager */ private $cartExtensionFactory; + /** + * @var CustomerInterfaceFactory + */ + private $customerDataFactory; + /** * @param \Magento\Persistent\Helper\Session $persistentSession * @param Data $persistentData @@ -71,6 +77,7 @@ class QuoteManager * @param CartRepositoryInterface $quoteRepository * @param CartExtensionFactory|null $cartExtensionFactory * @param ShippingAssignmentProcessor|null $shippingAssignmentProcessor + * @param CustomerInterfaceFactory|null $customerDataFactory */ public function __construct( \Magento\Persistent\Helper\Session $persistentSession, @@ -78,7 +85,8 @@ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, CartRepositoryInterface $quoteRepository, ?CartExtensionFactory $cartExtensionFactory = null, - ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null + ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null, + ?CustomerInterfaceFactory $customerDataFactory = null ) { $this->persistentSession = $persistentSession; $this->persistentData = $persistentData; @@ -88,6 +96,8 @@ public function __construct( ?? ObjectManager::getInstance()->get(CartExtensionFactory::class); $this->shippingAssignmentProcessor = $shippingAssignmentProcessor ?? ObjectManager::getInstance()->get(ShippingAssignmentProcessor::class); + $this->customerDataFactory = $customerDataFactory + ?? ObjectManager::getInstance()->get(CustomerInterfaceFactory::class); } /** @@ -109,14 +119,11 @@ public function setGuest($checkQuote = false) $quote->getPaymentsCollection()->walk('delete'); $quote->getAddressesCollection()->walk('delete'); $this->_setQuotePersistent = false; + $this->cleanCustomerData($quote); $quote->setIsActive(true) - ->setCustomerId(null) - ->setCustomerEmail(null) - ->setCustomerFirstname(null) - ->setCustomerLastname(null) - ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) ->removeAllAddresses(); + //Create guest addresses $quote->getShippingAddress(); $quote->getBillingAddress(); @@ -129,6 +136,27 @@ public function setGuest($checkQuote = false) $this->persistentSession->setSession(null); } + /** + * Clear customer data in quote + * + * @param Quote $quote + */ + private function cleanCustomerData($quote) + { + /** + * Set empty customer object in quote to avoid restore customer id + * @see Quote::beforeSave() + */ + if ($quote->getCustomerId()) { + $quote->setCustomer($this->customerDataFactory->create()); + } + $quote->setCustomerId(null) + ->setCustomerEmail(null) + ->setCustomerFirstname(null) + ->setCustomerLastname(null) + ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID); + } + /** * Emulate guest cart with persistent cart * diff --git a/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php b/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php index f2f9b96fa82e4..98c9c3df27852 100644 --- a/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php +++ b/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php @@ -1,16 +1,14 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Persistent\Observer; use Magento\Framework\Event\ObserverInterface; /** - * Make persistent quote to be guest + * Make persistent quote to be guest * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -38,26 +36,26 @@ class MakePersistentQuoteGuestObserver implements ObserverInterface protected $_persistentData = null; /** - * @var \Magento\Persistent\Model\QuoteManager + * @var \Magento\Checkout\Model\Session */ - protected $quoteManager; + private $checkoutSession; /** * @param \Magento\Persistent\Helper\Session $persistentSession * @param \Magento\Persistent\Helper\Data $persistentData * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Persistent\Model\QuoteManager $quoteManager + * @param \Magento\Checkout\Model\Session $checkoutSession */ public function __construct( \Magento\Persistent\Helper\Session $persistentSession, \Magento\Persistent\Helper\Data $persistentData, \Magento\Customer\Model\Session $customerSession, - \Magento\Persistent\Model\QuoteManager $quoteManager + \Magento\Checkout\Model\Session $checkoutSession ) { $this->_persistentSession = $persistentSession; $this->_persistentData = $persistentData; $this->_customerSession = $customerSession; - $this->quoteManager = $quoteManager; + $this->checkoutSession = $checkoutSession; } /** @@ -74,7 +72,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) || $this->_persistentData->isShoppingCartPersist() ) { - $this->quoteManager->setGuest(true); + $this->checkoutSession->clearQuote()->clearStorage(); } } } diff --git a/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php b/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php index fe754711c910b..efc9ecd4c1a59 100644 --- a/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php +++ b/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php @@ -10,6 +10,8 @@ /** * Observer to remove persistent session if guest empties persistent cart previously created and added to by customer. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class RemoveGuestPersistenceOnEmptyCartObserver implements ObserverInterface { @@ -96,6 +98,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if (!$cart || $cart->getItemsCount() == 0) { + $this->customerSession->setCustomerId(null) + ->setCustomerGroupId(null); $this->quoteManager->setGuest(); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 0c183084edca2..03d6ab02beb3c 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -9,6 +9,8 @@ namespace Magento\Persistent\Test\Unit\Model; use Magento\Checkout\Model\Session; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Model\GroupManagement; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Persistent\Helper\Data; @@ -78,6 +80,11 @@ class QuoteManagerTest extends TestCase */ private $shippingAssignmentProcessor; + /** + * @var CustomerInterfaceFactory|MockObject + */ + private $customerDataFactory; + protected function setUp(): void { $this->persistentSessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class); @@ -124,13 +131,15 @@ protected function setUp(): void 'getItemsQty', 'getExtensionAttributes', 'setExtensionAttributes', - '__wakeup' + '__wakeup', + 'setCustomer' ]) ->disableOriginalConstructor() ->getMock(); $this->cartExtensionFactory = $this->createPartialMock(CartExtensionFactory::class, ['create']); $this->shippingAssignmentProcessor = $this->createPartialMock(ShippingAssignmentProcessor::class, ['create']); + $this->customerDataFactory = $this->createMock(CustomerInterfaceFactory::class); $this->model = new QuoteManager( $this->persistentSessionMock, @@ -138,7 +147,8 @@ protected function setUp(): void $this->checkoutSessionMock, $this->quoteRepositoryMock, $this->cartExtensionFactory, - $this->shippingAssignmentProcessor + $this->shippingAssignmentProcessor, + $this->customerDataFactory ); } @@ -189,6 +199,7 @@ public function testSetGuestWhenShoppingCartAndQuoteAreNotPersistent() public function testSetGuest() { + $customerId = 22; $this->checkoutSessionMock->expects($this->once()) ->method('getQuote')->willReturn($this->quoteMock); $this->quoteMock->expects($this->once())->method('getId')->willReturn(11); @@ -220,6 +231,7 @@ public function testSetGuest() ->method('getShippingAddress')->willReturn($quoteAddressMock); $this->quoteMock->expects($this->once()) ->method('getBillingAddress')->willReturn($quoteAddressMock); + $this->quoteMock->method('getCustomerId')->willReturn($customerId); $this->quoteMock->expects($this->once())->method('collectTotals')->willReturn($this->quoteMock); $this->quoteRepositoryMock->expects($this->once())->method('save')->with($this->quoteMock); $this->persistentSessionMock->expects($this->once()) @@ -229,7 +241,6 @@ public function testSetGuest() $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); $this->quoteMock->expects($this->once())->method('getItemsQty')->willReturn(1); $extensionAttributes = $this->getMockBuilder(CartExtensionInterface::class) - ->addMethods(['getShippingAssignments', 'setShippingAssignments']) ->getMockForAbstractClass(); $shippingAssignment = $this->createMock(ShippingAssignmentInterface::class); $extensionAttributes->expects($this->once()) @@ -248,6 +259,11 @@ public function testSetGuest() $this->quoteMock->expects($this->once()) ->method('setExtensionAttributes') ->with($extensionAttributes); + $customerMock = $this->createMock(CustomerInterface::class); + $this->customerDataFactory->method('create')->willReturn($customerMock); + $this->quoteMock->expects($this->once()) + ->method('setCustomer') + ->with($customerMock); $this->model->setGuest(false); } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php index 3622fe66099a4..bb78447cf852f 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,12 +7,12 @@ namespace Magento\Persistent\Test\Unit\Observer; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\Event; use Magento\Framework\Event\Observer; use Magento\Persistent\Controller\Index; use Magento\Persistent\Helper\Data; use Magento\Persistent\Helper\Session; -use Magento\Persistent\Model\QuoteManager; use Magento\Persistent\Observer\MakePersistentQuoteGuestObserver; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -48,10 +47,10 @@ class MakePersistentQuoteGuestObserverTest extends TestCase /** * @var MockObject */ - protected $quoteManagerMock; + protected $checkoutSession; /** - * @var MockObject + * @var CheckoutSession|MockObject */ protected $eventManagerMock; @@ -60,6 +59,9 @@ class MakePersistentQuoteGuestObserverTest extends TestCase */ protected $actionMock; + /** + * @inheritdoc + */ protected function setUp(): void { $this->actionMock = $this->createMock(Index::class); @@ -67,7 +69,7 @@ protected function setUp(): void $this->sessionHelperMock = $this->createMock(Session::class); $this->helperMock = $this->createMock(Data::class); $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->quoteManagerMock = $this->createMock(QuoteManager::class); + $this->checkoutSession = $this->createMock(CheckoutSession::class); $this->eventManagerMock = $this->getMockBuilder(Event::class) ->addMethods(['getControllerAction']) @@ -81,7 +83,7 @@ protected function setUp(): void $this->sessionHelperMock, $this->helperMock, $this->customerSessionMock, - $this->quoteManagerMock + $this->checkoutSession ); } @@ -94,7 +96,8 @@ public function testExecute() $this->sessionHelperMock->expects($this->once())->method('isPersistent')->willReturn(true); $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(false); $this->helperMock->expects($this->never())->method('isShoppingCartPersist'); - $this->quoteManagerMock->expects($this->once())->method('setGuest')->with(true); + $this->checkoutSession->expects($this->once())->method('clearQuote')->willReturnSelf(); + $this->checkoutSession->expects($this->once())->method('clearStorage')->willReturnSelf(); $this->model->execute($this->observerMock); } @@ -107,7 +110,8 @@ public function testExecuteWhenShoppingCartIsPersist() $this->sessionHelperMock->expects($this->once())->method('isPersistent')->willReturn(true); $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(true); $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->willReturn(true); - $this->quoteManagerMock->expects($this->once())->method('setGuest')->with(true); + $this->checkoutSession->expects($this->once())->method('clearQuote')->willReturnSelf(); + $this->checkoutSession->expects($this->once())->method('clearStorage')->willReturnSelf(); $this->model->execute($this->observerMock); } @@ -120,7 +124,8 @@ public function testExecuteWhenShoppingCartIsNotPersist() $this->sessionHelperMock->expects($this->once())->method('isPersistent')->willReturn(true); $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(true); $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->willReturn(false); - $this->quoteManagerMock->expects($this->never())->method('setGuest'); + $this->checkoutSession->expects($this->never())->method('clearQuote')->willReturnSelf(); + $this->checkoutSession->expects($this->never())->method('clearStorage')->willReturnSelf(); $this->model->execute($this->observerMock); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php index 4adc806fed415..7bef8feaaacc5 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php @@ -137,6 +137,13 @@ public function testExecuteWithEmptyCart() ->with($customerId) ->willReturn($quoteMock); $quoteMock->expects($this->once())->method('getItemsCount')->willReturn($emptyCount); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerId') + ->with(null) + ->willReturnSelf(); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerGroupId') + ->with(null); $this->quoteManagerMock->expects($this->once())->method('setGuest'); $this->model->execute($this->observerMock); @@ -160,6 +167,13 @@ public function testExecuteWithNonexistentCart() ->method('getActiveForCustomer') ->with($customerId) ->willThrowException($exception); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerId') + ->with(null) + ->willReturnSelf(); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerGroupId') + ->with(null); $this->quoteManagerMock->expects($this->once())->method('setGuest'); $this->model->execute($this->observerMock); diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php index 13b19e4f79c9a..7e8b4d916334f 100644 --- a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -56,6 +56,6 @@ public function build(CartItem $cartItem): DataObject $requestData[] = $provider->execute($cartItem); } - return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } } diff --git a/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php b/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php index 678c92250f531..ccc4735ad1763 100644 --- a/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php +++ b/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php @@ -68,7 +68,7 @@ public function __construct( } /** - * Converts a specified rate model to a shipping method data object. + * Converts a specified quote item model to a totals item data object. * * @param \Magento\Quote\Model\Quote\Item $item * @return \Magento\Quote\Api\Data\TotalsItemInterface diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index b0aef022dcd25..1d4b8feba07f5 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -8,6 +8,7 @@ namespace Magento\Quote\Model; use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\Data\GroupInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Framework\Exception\CouldNotSaveException; @@ -396,7 +397,8 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) } } $quote->setCustomerIsGuest(true); - $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); + $groupId = $quote->getCustomer()->getGroupId() ?: GroupInterface::NOT_LOGGED_IN_ID; + $quote->setCustomerGroupId($groupId); } $remoteAddress = $this->remoteAddress->getRemoteAddress(); diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php deleted file mode 100644 index 19a7e03264d8a..0000000000000 --- a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Quote\Plugin; - -use Magento\Checkout\Model\Session; -use Magento\Quote\Model\QuoteRepository; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\StoreSwitcherInterface; - -/** - * Updates quote items store id. - * - * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ -class UpdateQuoteItemStore -{ - /** - * @var QuoteRepository - */ - private $quoteRepository; - - /** - * @var Session - */ - private $checkoutSession; - - /** - * @param QuoteRepository $quoteRepository - * @param Session $checkoutSession - */ - public function __construct( - QuoteRepository $quoteRepository, - Session $checkoutSession - ) { - $this->quoteRepository = $quoteRepository; - $this->checkoutSession = $checkoutSession; - } - - /** - * Update store id in active quote after store view switching. - * - * @param StoreSwitcherInterface $subject - * @param string $result - * @param StoreInterface $fromStore store where we came from - * @param StoreInterface $targetStore store where to go to - * @param string $redirectUrl original url requested for redirect after switching - * @return string url to be redirected after switching - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterSwitch( - StoreSwitcherInterface $subject, - $result, - StoreInterface $fromStore, - StoreInterface $targetStore, - string $redirectUrl - ): string { - $quote = $this->checkoutSession->getQuote(); - if ($quote->getIsActive()) { - $quote->setStoreId( - $targetStore->getId() - ); - $quote->getItemsCollection(false); - $this->quoteRepository->save($quote); - } - return $result; - } -} diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index ea758f7ce34f3..4197af2f2848a 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -247,6 +247,7 @@ protected function setUp(): void 'getPayment', 'setCheckoutMethod', 'setCustomerIsGuest', + 'getCustomer', 'getId' ] ) @@ -799,6 +800,12 @@ public function testPlaceOrderIfCustomerIsGuest() $this->quoteMock->expects($this->once()) ->method('getCheckoutMethod') ->willReturn(Onepage::METHOD_GUEST); + $customerMock = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteMock->expects($this->once()) + ->method('getCustomer') + ->willReturn($customerMock); $this->quoteMock->expects($this->once())->method('setCustomerId')->with(null)->willReturnSelf(); $this->quoteMock->expects($this->once())->method('setCustomerEmail')->with($email)->willReturnSelf(); @@ -866,6 +873,9 @@ public function testPlaceOrderIfCustomerIsGuest() $this->assertEquals($orderId, $service->placeOrder($cartId)); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testPlaceOrder() { $cartId = 323; diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml index ecad94fbbc249..125afb96f20fd 100644 --- a/app/code/Magento/Quote/etc/frontend/di.xml +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -12,9 +12,6 @@ <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> </arguments> </type> - <type name="Magento\Store\Model\StoreSwitcherInterface"> - <plugin name="update_quote_item_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteItemStore"/> - </type> <type name="Magento\Store\Api\StoreCookieManagerInterface"> <plugin name="update_quote_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteStore"/> </type> diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php index c14cc1324732c..c4909eef31287 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -45,11 +45,11 @@ public function __construct( */ public function build(array $cartItemData): DataObject { - $requestData = [[]]; + $requestData = []; foreach ($this->providers as $provider) { $requestData[] = $provider->execute($cartItemData); } - return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php new file mode 100644 index 0000000000000..73a22471584ec --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * {@inheritdoc} + */ +class ItemDataCompositeProcessor implements ItemDataProcessorInterface +{ + /** + * @var ItemDataProcessorInterface[] + */ + private $itemDataProcessors; + + /** + * @param ItemDataProcessorInterface[] $itemDataProcessors + */ + public function __construct(array $itemDataProcessors = []) + { + $this->itemDataProcessors = $itemDataProcessors; + } + + /** + * Process cart item data + * + * @param array $cartItemData + * @param ContextInterface $context + * @return array + */ + public function process(array $cartItemData, ContextInterface $context): array + { + foreach ($this->itemDataProcessors as $itemDataProcessor) { + $cartItemData = $itemDataProcessor->process($cartItemData, $context); + } + + return $cartItemData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php new file mode 100644 index 0000000000000..33f40bd28c1d3 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Process Cart Item Data + */ +interface ItemDataProcessorInterface +{ + /** + * Process cart item data + * + * @param array $cartItemData + * @param ContextInterface $context + * @return array + */ + public function process(array $cartItemData, ContextInterface $context): array; +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php index d5e554f096ec1..c7ab7596741e0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php @@ -11,11 +11,13 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Model\Cart\AddProductsToCart as AddProductsToCartService; use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput; use Magento\Quote\Model\Cart\Data\CartItemFactory; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\Quote\Model\Cart\Data\Error; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataProcessorInterface; /** * Resolver for addProductsToCart mutation @@ -34,16 +36,24 @@ class AddProductsToCart implements ResolverInterface */ private $addProductsToCartService; + /** + * @var ItemDataProcessorInterface + */ + private $itemDataProcessor; + /** * @param GetCartForUser $getCartForUser * @param AddProductsToCartService $addProductsToCart + * @param ItemDataProcessorInterface $itemDataProcessor */ public function __construct( GetCartForUser $getCartForUser, - AddProductsToCartService $addProductsToCart + AddProductsToCartService $addProductsToCart, + ItemDataProcessorInterface $itemDataProcessor ) { $this->getCartForUser = $getCartForUser; $this->addProductsToCartService = $addProductsToCart; + $this->itemDataProcessor = $itemDataProcessor; } /** @@ -68,6 +78,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cartItems = []; foreach ($cartItemsData as $cartItemData) { + if (!$this->itemIsAllowedToCart($cartItemData, $context)) { + continue; + } $cartItems[] = (new CartItemFactory())->create($cartItemData); } @@ -90,4 +103,21 @@ function (Error $error) { ) ]; } + + /** + * Check if the item can be added to cart + * + * @param array $cartItemData + * @param ContextInterface $context + * @return bool + */ + private function itemIsAllowedToCart(array $cartItemData, ContextInterface $context): bool + { + $cartItemData = $this->itemDataProcessor->process($cartItemData, $context); + if (isset($cartItemData['grant_checkout']) && $cartItemData['grant_checkout'] === false) { + return false; + } + + return true; + } } diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml index d230df253221b..35b52dd495c5a 100644 --- a/app/code/Magento/QuoteGraphQl/etc/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Composite" /> + <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataProcessorInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataCompositeProcessor" /> <type name="Magento\QuoteGraphQl\Model\Resolver\CartItemTypeResolver"> <arguments> <argument name="supportedTypes" xsi:type="array"> diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index e14d8bde6be74..fac7b23d408e3 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -89,8 +89,7 @@ private function findRelations(array $products, array $loadAttributes, int $link if (!$relations) { return []; } - $relatedIds = array_values($relations); - $relatedIds = array_unique(array_merge(...$relatedIds)); + $relatedIds = array_unique(array_merge([], ...array_values($relations))); //Loading products data. $this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in'); $relatedSearchResult = $this->productDataProvider->getList( @@ -142,7 +141,7 @@ public function resolve(ContextInterface $context, Field $field, array $requests $products[] = $request->getValue()['model']; $fields[] = $this->productFieldsSelector->getProductFieldsFromInfo($request->getInfo(), $this->getNode()); } - $fields = array_unique(array_merge(...$fields)); + $fields = array_unique(array_merge([], ...$fields)); //Finding relations. $related = $this->findRelations($products, $fields, $this->getLinkType()); diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Grid.php index 8885c94c6989a..eade7250f6123 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid.php @@ -209,7 +209,7 @@ protected function _getAllowedStoreIds() } elseif ($this->getRequest()->getParam('website')) { $storeIds = $this->_storeManager->getWebsite($this->getRequest()->getParam('website'))->getStoreIds(); } elseif ($this->getRequest()->getParam('group')) { - $storeIds = $storeIds = $this->_storeManager->getGroup( + $storeIds = $this->_storeManager->getGroup( $this->getRequest()->getParam('group') )->getStoreIds(); } diff --git a/app/code/Magento/Reports/Block/Product/Viewed.php b/app/code/Magento/Reports/Block/Product/Viewed.php index ba4d03182213a..09d59e475905b 100644 --- a/app/code/Magento/Reports/Block/Product/Viewed.php +++ b/app/code/Magento/Reports/Block/Product/Viewed.php @@ -76,10 +76,10 @@ protected function _toHtml() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getItemsCollection() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php index efef617acf900..81f670de91805 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php @@ -68,7 +68,7 @@ public function getItem() */ public function getOrderOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -80,7 +80,7 @@ public function getOrderOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php index cbb79f188f231..57fc0441fe830 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php @@ -39,7 +39,7 @@ public function getOrder() */ public function getItemOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getOrderItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -52,7 +52,7 @@ public function getItemOptions() } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php index 0291a1275c350..cb9c7315244ac 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php @@ -34,7 +34,7 @@ public function getOrder() */ public function getItemOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -47,7 +47,7 @@ public function getItemOptions() } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index bca6d49760d9a..010878559c2f0 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -105,7 +105,7 @@ public function getOrderItem() */ public function getItemOptions() { - $result = [[]]; + $result = []; $options = $this->getOrderItem()->getProductOptions(); if ($options) { if (isset($options['options'])) { @@ -118,7 +118,7 @@ public function getItemOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Helper/Guest.php b/app/code/Magento/Sales/Helper/Guest.php index a3f2ac6ba3556..3b7e491086b17 100644 --- a/app/code/Magento/Sales/Helper/Guest.php +++ b/app/code/Magento/Sales/Helper/Guest.php @@ -15,6 +15,7 @@ /** * Sales module base helper * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Guest extends \Magento\Framework\App\Helper\AbstractHelper { @@ -71,7 +72,7 @@ class Guest extends \Magento\Framework\App\Helper\AbstractHelper const COOKIE_NAME = 'guest-view'; /** - * Cookie path + * Cookie path value */ const COOKIE_PATH = '/'; @@ -151,6 +152,7 @@ public function loadValidOrder(App\RequestInterface $request) return $this->resultRedirectFactory->create()->setPath('sales/order/history'); } $post = $request->getPostValue(); + $post = filter_var($post, FILTER_CALLBACK, ['options' => 'trim']); $fromCookie = $this->cookieManager->getCookie(self::COOKIE_NAME); if (empty($post) && !$fromCookie) { return $this->resultRedirectFactory->create()->setPath('sales/guest/form'); @@ -224,6 +226,7 @@ private function setGuestViewCookie($cookieValue) */ private function loadFromCookie($fromCookie) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $cookieData = explode(':', base64_decode($fromCookie)); $protectCode = isset($cookieData[0]) ? $cookieData[0] : null; $incrementId = isset($cookieData[1]) ? $cookieData[1] : null; diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 5b9f254201bda..393d61b69bf22 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -642,6 +642,7 @@ protected function _initShippingAddressFromOrder(\Magento\Sales\Model\Order $ord * @param \Magento\Sales\Model\Order\Item $orderItem * @param int $qty * @return \Magento\Quote\Model\Quote\Item|string|$this + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $qty = null) { @@ -666,10 +667,17 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q $productOptions = $orderItem->getProductOptions(); if ($productOptions !== null && !empty($productOptions['options'])) { $formattedOptions = []; + $useFrontendCalendar = $this->useFrontendCalendar(); foreach ($productOptions['options'] as $option) { + if (in_array($option['option_type'], ['date', 'date_time']) && $useFrontendCalendar) { + $product->setSkipCheckRequiredOption(false); + break; + } $formattedOptions[$option['option_id']] = $option['option_value']; } - $buyRequest->setData('options', $formattedOptions); + if (!empty($formattedOptions)) { + $buyRequest->setData('options', $formattedOptions); + } } $item = $this->getQuote()->addProduct($product, $buyRequest); if (is_string($item)) { @@ -2115,4 +2123,17 @@ private function isAddressesAreEqual(Order $order) return $shippingData == $billingData; } + + /** + * Use Calendar on frontend or not + * + * @return bool + */ + private function useFrontendCalendar(): bool + { + return (bool)$this->_scopeConfig->getValue( + 'catalog/custom_options/use_calendar', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index 93c8ed00f9daa..a92a1480bd023 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -111,6 +111,12 @@ public function send( 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index 004f36c277028..44b4df17619d8 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -111,6 +111,12 @@ public function send( 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index 6a2a77b52927a..d1a34b496b1ac 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -742,7 +742,7 @@ public function refund($creditmemo) $this->formatPrice($baseAmountToRefund) ); } - $message = $message = $this->prependMessage($message); + $message = $this->prependMessage($message); $message = $this->_appendTransactionToMessage($transaction, $message); $orderState = $this->getOrderStateResolver()->getStateForOrder($this->getOrder()); $statuses = $this->getOrder()->getConfig()->getStateStatuses($orderState, false); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php index 29e011217ef20..a7315aeb9e3be 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php @@ -326,7 +326,7 @@ public function getItemPricesForDisplay() */ public function getItemOptions() { - $result = [[]]; + $result = []; $options = $this->getItem()->getOrderItem()->getProductOptions(); if ($options) { if (isset($options['options'])) { @@ -339,7 +339,7 @@ public function getItemOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index fe68555d9f7c7..286d33815aea1 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -112,7 +112,13 @@ public function send( 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php index 72ce60d32877c..eccfc8e56e6e5 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php +++ b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php @@ -87,7 +87,7 @@ public function __construct( * Perform actions after object save * * @param \Magento\Framework\Model\AbstractModel $object - * @param string $attribute + * @param AbstractAttribute|string[]|string $attribute * @return $this * @throws \Exception */ diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php index 58284759b2fee..3ff2ed66a846b 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php @@ -257,7 +257,7 @@ protected function getStatusByState($state) { return (string)$this->getConnection()->fetchOne( $select = $this->getConnection()->select() - ->from(['sss' => $this->stateTable, []]) + ->from(['sss' => $this->stateTable], []) ->where('state = ?', $state) ->limit(1) ->columns(['status']) diff --git a/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php b/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php index 645e411b80b67..10b3ca1bde996 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php @@ -37,11 +37,11 @@ public function __construct(TMapFactory $tmapFactory, array $providers = []) */ public function getIds($mainTableName, $gridTableName) { - $result = [[]]; + $result = []; foreach ($this->providers as $provider) { $result[] = $provider->getIds($mainTableName, $gridTableName); } - return array_unique(array_merge(...$result)); + return array_unique(array_merge([], ...$result)); } } diff --git a/app/code/Magento/Sales/Model/ShipOrder.php b/app/code/Magento/Sales/Model/ShipOrder.php index 26fe5a8e4b457..f955f6574a7b2 100644 --- a/app/code/Magento/Sales/Model/ShipOrder.php +++ b/app/code/Magento/Sales/Model/ShipOrder.php @@ -177,11 +177,13 @@ public function execute( $connection->beginTransaction(); try { $this->orderRegistrar->register($order, $shipment); - $order->setState( - $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) - ); - $order->setStatus($this->config->getStateDefaultStatus($order->getState())); - $shippingData = $this->shipmentRepository->save($shipment); + $shipment = $this->shipmentRepository->save($shipment); + if ($order->getState() === Order::STATE_NEW) { + $order->setState( + $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + } $this->orderRepository->save($order); $connection->commit(); } catch (\Exception $e) { @@ -191,9 +193,7 @@ public function execute( __('Could not save a shipment, see error log for details') ); } - if ($shipment && empty($shipment->getEntityId())) { - $shipment->setEntityId($shippingData->getEntityId()); - } + if ($notify) { if (!$appendComment) { $comment = null; diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..d6d5c9e7315d9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.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="AdminAssertOrderShippingMethodActionGroup"> + <annotations> + <description>Assert that shipping method and shipping price is present for the order.</description> + </annotations> + <arguments> + <argument name="shippingMethod" type="string" defaultValue="{{flatRateTitleDefault.value}} - {{flatRateNameDefault.value}}"/> + <argument name="shippingPrice" type="string" defaultValue="$5.00"/> + </arguments> + <see selector="{{AdminOrderShippingInformationSection.shippingMethod}}" userInput="{{shippingMethod}}" stepKey="seeShippingMethod"/> + <see selector="{{AdminOrderShippingInformationSection.shippingPrice}}" userInput="{{shippingPrice}}" stepKey="seeShippingMethodPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.xml new file mode 100644 index 0000000000000..361787948a133 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.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="AdminSelectFieldToColumnActionGroup"> + <annotations> + <description>Select or clear the checkbox to display the column on the Orders grid page.</description> + </annotations> + <arguments> + <argument name="column" type="string" defaultValue="Purchase Point"/> + </arguments> + <click selector="{{AdminOrdersGridSection.columnsDropdown}}" stepKey="openColumnsDropdown" /> + <click selector="{{AdminOrdersGridSection.viewColumnCheckbox(column)}}" stepKey="disableColumn"/> + <click selector="{{AdminOrdersGridSection.columnsDropdown}}" stepKey="closeColumnsDropdown" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.xml new file mode 100644 index 0000000000000..ad7f5011af954 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.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="StorefrontFillOrdersAndReturnsFormTypeZipActionGroup"> + <arguments> + <argument name="orderNumber" type="string"/> + <argument name="customer" type="entity"/> + <argument name="address" type="entity"/> + </arguments> + <fillField selector="{{StorefrontGuestOrderSearchSection.orderId}}" userInput="{{orderNumber}}" stepKey="inputOrderId"/> + <fillField selector="{{StorefrontGuestOrderSearchSection.billingLastName}}" userInput="{{customer.lastname}}" stepKey="inputBillingLastName"/> + <selectOption selector="{{StorefrontGuestOrderSearchSection.findOrderBy}}" userInput="zip" stepKey="selectFindOrderByZip"/> + <fillField selector="{{StorefrontGuestOrderSearchSection.zip}}" userInput="{{address.postcode}}" stepKey="inputZip"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml index 920618a70dfb8..f96028405c4e5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml @@ -24,6 +24,7 @@ <data key="postcode">78758</data> <data key="email" unique="prefix">joe.buyer@email.com</data> <data key="telephone">512-345-6789</data> + <data key="country">United States</data> </entity> <entity name="BillingAddressTX" type="billing_address"> <data key="firstname">Joe</data> @@ -41,5 +42,6 @@ <data key="postcode">78758</data> <data key="email" unique="prefix">joe.buyer@email.com</data> <data key="telephone">512-345-6789</data> + <data key="country">United States</data> </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index a18ca0c415567..02878e79f3d70 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -18,6 +18,7 @@ <element name="idFilter" type="input" selector=".admin__data-grid-filters input[name='increment_id']"/> <element name="selectStatus" type="select" selector="select[name='status']" timeout="60"/> <element name="billToNameFilter" type="input" selector=".admin__data-grid-filters input[name='billing_name']"/> + <element name="purchasePoint" type="select" selector=".admin__data-grid-filters select[name='store_id']"/> <element name="enabledFilters" type="block" selector=".admin__data-grid-header .admin__data-grid-filters-current._show"/> <element name="clearFilters" type="button" selector=".admin__data-grid-header [data-action='grid-filter-reset']" timeout="30"/> <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml index 5e420ee03bf75..efee68f2bd25f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml @@ -13,6 +13,7 @@ <element name="billingLastName" type="input" selector="#oar-billing-lastname"/> <element name="findOrderBy" type="select" selector="#quick-search-type-id"/> <element name="email" type="input" selector="#oar_email"/> + <element name="zip" type="input" selector="#oar_zip"/> <element name="continue" type="button" selector="//*/span[contains(text(), 'Continue')]"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml new file mode 100644 index 0000000000000..874164fdcdcf0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.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="AdminReorderOrderWithOfflinePaymentMethodTest"> + <annotations> + <features value="Sales"/> + <stories value="Reorder"/> + <title value="Reorder Order from Admin for Offline Payment Methods"/> + <description value="Create reorder for order with two products and Check Money payment method"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37495"/> + <group value="sales"/> + </annotations> + <before> + <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="CustomerCart" stepKey="createCartForCustomer"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addFirstProductToCustomerCart"> + <requiredEntity createDataKey="createCartForCustomer"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addSecondProductToCustomerCart"> + <requiredEntity createDataKey="createCartForCustomer"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCartForCustomer"/> + </createData> + <updateData createDataKey="createCartForCustomer" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformation"> + <requiredEntity createDataKey="createCartForCustomer"/> + </updateData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> + </after> + <actionGroup ref="AdminOpenOrderByEntityIdActionGroup" stepKey="openOrderById"> + <argument name="entityId" value="$createCartForCustomer.return$"/> + </actionGroup> + <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorderButton"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitReorder"/> + <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> + <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="verifyOrderAddressInformation"> + <argument name="customer" value="$createCustomer$"/> + <argument name="shippingAddress" value="ShippingAddressTX"/> + <argument name="billingAddress" value="BillingAddressTX"/> + </actionGroup> + <see selector="{{AdminOrderDetailsInformationSection.paymentInformation}}" userInput="Check / Money order" stepKey="seePaymentMethod"/> + <actionGroup ref="AdminAssertOrderShippingMethodActionGroup" stepKey="assertShippingOrderInformation"> + <argument name="shippingPrice" value="$10.00"/> + </actionGroup> + <actionGroup ref="SeeProductInItemsOrderedActionGroup" stepKey="seeFirstProductInItemsOrdered"> + <argument name="product" value="$createFirstSimpleProduct$"/> + </actionGroup> + <actionGroup ref="SeeProductInItemsOrderedActionGroup" stepKey="seeSecondProductInItemsOrdered"> + <argument name="product" value="$createSecondSimpleProduct$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml new file mode 100644 index 0000000000000..b0c6b3a2fc6ca --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml @@ -0,0 +1,38 @@ +<?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="AdminVerifyFieldToFilterOnOrdersGridTest"> + <annotations> + <features value="Sales"/> + <stories value="Github issue: #28385 Resolve issue with filter visibility with column visibility in grid"/> + <title value="Verify field to filter"/> + <description value="Verify not appear fields to filter on Orders grid if it disables in columns dropdown."/> + <severity value="MAJOR"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin" /> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout" /> + </after> + + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="goToOrders"/> + <actionGroup ref="AdminSelectFieldToColumnActionGroup" stepKey="unSelectPurchasePoint" /> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openColumnsDropdown" /> + <dontSeeElement selector="{{AdminOrdersGridSection.purchasePoint}}" stepKey="dontSeeElement"/> + + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="closeColumnsDropdown" /> + <actionGroup ref="AdminSelectFieldToColumnActionGroup" stepKey="selectPurchasePoint" /> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openColumnsDropdown2" /> + <seeElement selector="{{AdminOrdersGridSection.purchasePoint}}" stepKey="seeElement"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index 20dcb262b5831..b2bdf8ce5d90b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -39,8 +39,7 @@ </after> <!-- Create a cart price rule for $10 Fixed amount discount --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ApiSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsite"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml new file mode 100644 index 0000000000000..c99a02750d6ce --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml @@ -0,0 +1,30 @@ +<?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="StorefrontPrintOrderFindByZipGuestTest" extends="StorefrontPrintOrderGuestTest"> + <annotations> + <stories value="Print Order"/> + <title value="Print Order from Guest on Frontend using Zip for search"/> + <description value="Print Order from Guest on Frontend"/> + <severity value="MINOR"/> + <testCaseId value="MC-37449"/> + <group value="sales"/> + </annotations> + + <remove keyForRemoval="fillOrder"/> + + <!-- Fill the form with correspondent Order data using search by Zip --> + <actionGroup ref="StorefrontFillOrdersAndReturnsFormTypeZipActionGroup" stepKey="fillOrderZip" before="clickContinue"> + <argument name="orderNumber" value="{$getOrderId}"/> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php b/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php index 0ee1e4249e27d..07f740f7c1fd8 100644 --- a/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php +++ b/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php @@ -112,7 +112,6 @@ protected function setUp(): void ->setMethods(['getTotalCount', 'getItems']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->searchCriteriaBuilder->method('addFilter')->willReturnSelf(); $resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) ->setMethods(['create']) @@ -148,29 +147,45 @@ protected function setUp(): void ); } - public function testLoadValidOrderNotEmptyPost() + /** + * Test load valid order with non empty post data. + * + * @param array $post + * @dataProvider loadValidOrderNotEmptyPostDataProvider + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\CookieSizeLimitReachedException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException + */ + public function testLoadValidOrderNotEmptyPost($post) { - $post = [ - 'oar_order_id' => 1, - 'oar_type' => 'email', - 'oar_billing_lastname' => 'oar_billing_lastname', - 'oar_email' => 'oar_email', - 'oar_zip' => 'oar_zip', - - ]; $incrementId = $post['oar_order_id']; $protectedCode = 'protectedCode'; $this->sessionMock->expects($this->once())->method('isLoggedIn')->willReturn(false); $requestMock = $this->createMock(Http::class); $requestMock->expects($this->once())->method('getPostValue')->willReturn($post); + + $this->searchCriteriaBuilder + ->expects($this->at(0)) + ->method('addFilter') + ->with('increment_id', trim($incrementId)) + ->willReturnSelf(); + + $this->searchCriteriaBuilder + ->expects($this->at(1)) + ->method('addFilter') + ->with('store_id', $this->storeModelMock->getId()) + ->willReturnSelf(); + $this->salesOrderMock->expects($this->any())->method('getId')->willReturn($incrementId); $billingAddressMock = $this->createPartialMock( Address::class, - ['getLastname', 'getEmail'] + ['getLastname', 'getEmail', 'getPostcode'] ); - $billingAddressMock->expects($this->once())->method('getLastname')->willReturn(($post['oar_billing_lastname'])); - $billingAddressMock->expects($this->once())->method('getEmail')->willReturn(($post['oar_email'])); + $billingAddressMock->expects($this->once())->method('getLastname') + ->willReturn(trim($post['oar_billing_lastname'])); + $billingAddressMock->expects($this->any())->method('getEmail')->willReturn(trim($post['oar_email'])); + $billingAddressMock->expects($this->any())->method('getPostcode')->willReturn(trim($post['oar_zip'])); $this->salesOrderMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); $this->salesOrderMock->expects($this->once())->method('getProtectCode')->willReturn($protectedCode); $metaDataMock = $this->createMock(PublicCookieMetadata::class); @@ -190,10 +205,49 @@ public function testLoadValidOrderNotEmptyPost() $this->assertTrue($this->guest->loadValidOrder($requestMock)); } + /** + * Load valid order with non empty post data provider. + * + * @return array + */ + public function loadValidOrderNotEmptyPostDataProvider() + { + return [ + [ + [ + 'oar_order_id' => '1', + 'oar_type' => 'email', + 'oar_billing_lastname' => 'White', + 'oar_email' => 'test@magento-test.com', + 'oar_zip' => '', + + ] + ], + [ + [ + 'oar_order_id' => ' 14 ', + 'oar_type' => 'email', + 'oar_billing_lastname' => 'Black ', + 'oar_email' => ' test1@magento-test.com ', + 'oar_zip' => '', + ] + ], + [ + [ + 'oar_order_id' => ' 14 ', + 'oar_type' => 'zip', + 'oar_billing_lastname' => 'Black ', + 'oar_email' => ' test1@magento-test.com ', + 'oar_zip' => '123456 ', + ] + ] + ]; + } + public function testLoadValidOrderStoredCookie() { $protectedCode = 'protectedCode'; - $incrementId = 1; + $incrementId = '1'; $cookieData = $protectedCode . ':' . $incrementId; $cookieDataHash = base64_encode($cookieData); $this->sessionMock->expects($this->once())->method('isLoggedIn')->willReturn(false); @@ -201,6 +255,19 @@ public function testLoadValidOrderStoredCookie() ->method('getCookie') ->with(Guest::COOKIE_NAME) ->willReturn($cookieDataHash); + + $this->searchCriteriaBuilder + ->expects($this->at(0)) + ->method('addFilter') + ->with('increment_id', trim($incrementId)) + ->willReturnSelf(); + + $this->searchCriteriaBuilder + ->expects($this->at(1)) + ->method('addFilter') + ->with('store_id', $this->storeModelMock->getId()) + ->willReturnSelf(); + $this->salesOrderMock->expects($this->any())->method('getId')->willReturn($incrementId); $this->salesOrderMock->expects($this->once())->method('getProtectCode')->willReturn($protectedCode); $metaDataMock = $this->createMock(PublicCookieMetadata::class); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 99e00a74f1ba3..8bc739e9c68fd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -273,6 +273,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -283,6 +288,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index eaf57ad1bfc56..4a909a21e2558 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -272,6 +272,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -282,6 +287,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 81ed71ae7bb67..713b38f7d7f4a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -284,6 +284,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -296,6 +301,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php index 5909ebd76feb1..77cd6a058df6f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php @@ -270,12 +270,12 @@ public function testExecute($orderId, $items, $notify, $appendComment) ->method('setState') ->with(Order::STATE_PROCESSING) ->willReturnSelf(); - $this->orderMock->expects($this->once()) + $this->orderMock->expects($this->exactly(2)) ->method('getState') - ->willReturn(Order::STATE_PROCESSING); + ->willReturn(Order::STATE_NEW); $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') - ->with(Order::STATE_PROCESSING) + ->with(Order::STATE_NEW) ->willReturn('Processing'); $this->orderMock->expects($this->once()) ->method('setStatus') @@ -294,7 +294,7 @@ public function testExecute($orderId, $items, $notify, $appendComment) ->method('notify') ->with($this->orderMock, $this->shipmentMock, $this->shipmentCommentCreationMock); } - $this->shipmentMock->expects($this->exactly(2)) + $this->shipmentMock->expects($this->exactly(1)) ->method('getEntityId') ->willReturn(2); $this->assertEquals( diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index de062029fb53b..ab524a0f552f6 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -769,7 +769,7 @@ <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment ID"/> <column xsi:type="int" name="order_id" unsigned="true" nullable="false" identity="false" comment="Order ID"/> - <column xsi:type="timestamp" name="order_created_at" on_update="true" nullable="false" + <column xsi:type="timestamp" name="order_created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Order Increment ID"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml index e0b7dae8fdb1a..1fc8d41ce0900 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml @@ -42,7 +42,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index f6b1240402477..9105b4be8cda2 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -53,7 +53,7 @@ <label translate="true">Purchase Point</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml index 1e60e4a806fce..c88bc91a16641 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml @@ -42,7 +42,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml index 9e02c31a20635..f6474b5db2fd8 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml @@ -42,7 +42,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml index cf536c27a0ac3..09be15c5a3cf9 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml @@ -51,7 +51,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml index ac1233c5e4961..4b6c8b3518e06 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml @@ -51,7 +51,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml index 5f8ebde290664..8a11bc63a4318 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml @@ -51,7 +51,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index a329524c58d41..8f13e6d9dab0d 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -1507,12 +1507,17 @@ define([ if (action === 'change') { var confirmText = message.replace(/%s/, customerGroupOption.text); confirmText = confirmText.replace(/%s/, currentCustomerGroupTitle); - if (confirm(confirmText)) { - $$('#' + groupIdHtmlId + ' option').each(function (o) { - o.selected = o.readAttribute('value') == groupId; - }); - this.accountGroupChange(); - } + confirm({ + content: confirmText, + actions: { + confirm: function() { + $$('#' + groupIdHtmlId + ' option').each(function (o) { + o.selected = o.readAttribute('value') == groupId; + }); + this.accountGroupChange(); + }.bind(this) + } + }) } else if (action === 'inform') { alert({ content: message + '\n' + groupMessage diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.xml new file mode 100644 index 0000000000000..b12bdf56e0ed8 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.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="AdminOpenCartPriceRulesPageActionGroup"> + <annotations> + <description>Open cart price rules page.</description> + </annotations> + + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="openCartPriceRulesPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml index ed2dd16b7df9d..5f2b40dc63e2a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create a cart price rule of type Buy X get Y free --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml index 34152ea06745c..88853b2c40d9a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml @@ -31,8 +31,7 @@ </after> <!--Create cart price rule as per data and verify AssertCartPriceRuleSuccessSaveMessage--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{CartPriceRuleConditionAndFreeShippingApplied.name}}" stepKey="fillRuleName"/> <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{CartPriceRuleConditionAndFreeShippingApplied.description}}" stepKey="fillDescription"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml index 9ac73ceae586e..25d9d431d1c51 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml @@ -33,8 +33,7 @@ </after> <!--Create cart price rule as per data and verify AssertCartPriceRuleSuccessSaveMessage--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{CartPriceRuleConditionNotApplied.name}}" stepKey="fillRuleName"/> <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{CartPriceRuleConditionNotApplied.description}}" stepKey="fillDescription"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index f956d036d7080..e206633808057 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml @@ -47,8 +47,7 @@ <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> <!-- Create a cart price rule based on a coupon code --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml index 557a585858868..16af210066997 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create a cart price rule based on a coupon code --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 953d142a49ab1..79672b5bdd559 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create a cart price rule --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml index 34714e9637d46..da8c8e4bc1f9d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml @@ -31,8 +31,7 @@ </after> <!--Create cart price rule as per data and verify AssertCartPriceRuleSuccessSaveMessage--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{CartPriceRuleConditionAppliedForSubtotal.name}}" stepKey="fillRuleName"/> <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{CartPriceRuleConditionAppliedForSubtotal.description}}" stepKey="fillDescription"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml index a3e6331e31cf6..f6e736c73db74 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml @@ -38,8 +38,7 @@ </after> <!--Create cart price rule as per data and verify AssertCartPriceRuleSuccessSaveMessage--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{CartPriceRuleConditionAppliedForCategory.name}}" stepKey="fillRuleName"/> <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{CartPriceRuleConditionAppliedForCategory.description}}" stepKey="fillDescription"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml index e9f7f3ec6c70a..5f110f7074f6f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml @@ -31,8 +31,7 @@ </after> <!--Create cart price rule as per data and verify AssertCartPriceRuleSuccessSaveMessage--> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{CartPriceRuleConditionAppliedForWeight.name}}" stepKey="fillRuleName"/> <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{CartPriceRuleConditionAppliedForWeight.description}}" stepKey="fillDescription"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml index 0d98abfba3f62..2c3574906848c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create a cart price rule for $10 Fixed amount discount --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index bc4139435ab55..1b24480b5808b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create a cart price rule for Fixed amount discount for whole cart --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml index 56c4506196d24..83648cec149d0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml @@ -26,8 +26,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml index 23e472518ba84..724860b12603c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml @@ -36,8 +36,7 @@ </after> <!-- Create a cart price rule for 50 percent of product price --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml index ad1ff69a60901..d60a81dcdcef9 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -96,8 +96,7 @@ </after> <!-- Create the rule --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 631c516153fa2..96b3990dfd063 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -40,8 +40,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Search Cart Price Rule and go to edit Cart Price Rule --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="$$createSalesRule.name$$" stepKey="fillFieldFilterByName"/> <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="clickSearchButton"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml index eef5dadfbe5d8..ea96fa41e5cad 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml @@ -37,8 +37,7 @@ </after> <!-- Create the rule... --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml index c5f4e8a07f622..56486d2331bd6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml @@ -107,8 +107,7 @@ </after> <!-- Create the rule --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml index 69097e3269fcb..62c494b988bbd 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml @@ -37,8 +37,7 @@ </after> <!-- Create the rule... --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml index 18057965c28e1..70ed09df7a2cc 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml @@ -38,8 +38,7 @@ </after> <!-- Create the rule... --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml index c13b74b6990d0..da9ca9055d31b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml @@ -37,8 +37,7 @@ </after> <!-- Create the rule... --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml index 97b75ae772f08..ce0d814e50308 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml @@ -38,8 +38,7 @@ </after> <!-- Create the rule... --> - <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminOpenCartPriceRulesPageActionGroup" stepKey="amOnCartPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> diff --git a/app/code/Magento/Search/Model/Autocomplete.php b/app/code/Magento/Search/Model/Autocomplete.php index 45957e8795744..57364e4c36bde 100644 --- a/app/code/Magento/Search/Model/Autocomplete.php +++ b/app/code/Magento/Search/Model/Autocomplete.php @@ -30,11 +30,11 @@ public function __construct( */ public function getItems() { - $data = [[]]; + $data = []; foreach ($this->dataProviders as $dataProvider) { $data[] = $dataProvider->getItems(); } - return array_merge(...$data); + return array_merge([], ...$data); } } diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index eea6a950d7ce5..16d0b0b4ddcd9 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -42,6 +42,7 @@ public function __construct(SynonymReader $synReader) * 3 => [ 0 => "british", 1 => "english" ], * 4 => [ 0 => "queen", 1 => "monarch" ] * ] + * * @param string $phrase * @return array * @throws \Magento\Framework\Exception\LocalizedException @@ -136,6 +137,9 @@ private function getSearchPattern(array $words): string { $patterns = []; for ($lastItem = count($words); $lastItem > 0; $lastItem--) { + $words = array_map(function ($word) { + return preg_quote($word, '/'); + }, $words); $phrase = implode("\s+", \array_slice($words, 0, $lastItem)); $patterns[] = '^' . $phrase . ','; $patterns[] = ',' . $phrase . ','; diff --git a/app/code/Magento/Search/Model/SynonymGroupRepository.php b/app/code/Magento/Search/Model/SynonymGroupRepository.php index dbc2b66b1f047..c670235d67adb 100644 --- a/app/code/Magento/Search/Model/SynonymGroupRepository.php +++ b/app/code/Magento/Search/Model/SynonymGroupRepository.php @@ -150,7 +150,7 @@ private function create(SynonymGroupInterface $synonymGroup, $errorOnMergeConfli */ private function merge(SynonymGroupInterface $synonymGroupToMerge, array $matchingGroupIds) { - $mergedSynonyms = [[]]; + $mergedSynonyms = []; foreach ($matchingGroupIds as $groupId) { /** @var SynonymGroup $synonymGroupModel */ $synonymGroupModel = $this->synonymGroupFactory->create(); @@ -160,7 +160,7 @@ private function merge(SynonymGroupInterface $synonymGroupToMerge, array $matchi } $mergedSynonyms[] = explode(',', $synonymGroupToMerge->getSynonymGroup()); - return array_unique(array_merge(...$mergedSynonyms)); + return array_unique(array_merge([], ...$mergedSynonyms)); } /** diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.xml new file mode 100644 index 0000000000000..5bc63bf730de0 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.xml @@ -0,0 +1,20 @@ +<?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="AdminSetGlobalSearchValueActionGroup"> + <arguments> + <argument name="textSearch" type="string" defaultValue=""/> + </arguments> + + <click selector="{{AdminGlobalSearchSection.globalSearch}}" stepKey="clickSearchBtn"/> + <waitForElementVisible selector="{{AdminGlobalSearchSection.globalSearchActive}}" stepKey="waitForSearchInputVisible"/> + <fillField selector="{{AdminGlobalSearchSection.globalSearchInput}}" userInput="{{textSearch}}" stepKey="fillSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml index 0ba61283548cf..a529000e20923 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml @@ -11,5 +11,8 @@ <section name="AdminGlobalSearchSection"> <element name="globalSearch" type="button" selector=".search-global-label"/> <element name="globalSearchActive" type="block" selector=".search-global-field._active"/> + <element name="globalSearchInput" type="input" selector=".search-global-input"/> + <element name="globalSearchSuggestedCategoryText" type="text" selector="//span[contains(text(), 'Category')]"/> + <element name="globalSearchSuggestedCategoryLink" type="text" selector="//span[contains(text(), 'Category')]/preceding-sibling::a"/> </section> </sections> diff --git a/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php index 8751c8a4f3ec0..9e6d087f72f99 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php @@ -49,9 +49,9 @@ protected function setUp(): void */ public function testGetSynonymsForPhrase() { - $phrase = 'Elizabeth is the british queen'; + $phrase = 'Elizabeth/Angela is the british queen'; $expected = [ - 0 => [ 0 => "Elizabeth" ], + 0 => [ 0 => "Elizabeth/Angela" ], 1 => [ 0 => "is" ], 2 => [ 0 => "the" ], 3 => [ 0 => "british", 1 => "english" ], diff --git a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml index 42ebf1454fb7e..c95604f0afa49 100644 --- a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml +++ b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml @@ -65,7 +65,7 @@ <label translate="true">Store View</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> @@ -76,7 +76,7 @@ <label translate="true">Website</label> <dataScope>website_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php index 618d941f7047e..edb572dfdd4d1 100644 --- a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php @@ -6,10 +6,6 @@ namespace Magento\SendFriend\Model\ResourceModel; /** - * SendFriend Log Resource Model - * - * @author Magento Core Team <core@magentocommerce.com> - * * @api * @since 100.0.2 */ @@ -32,6 +28,7 @@ protected function _construct() * @param int $ip * @param int $startTime * @param int $websiteId + * * @return int * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -46,7 +43,7 @@ public function getSendCount($object, $ip, $startTime, $websiteId = null) AND time>=:time AND website_id=:website_id' ); - $bind = ['ip' => ip2long($ip), 'time' => $startTime, 'website_id' => (int)$websiteId]; + $bind = ['ip' => $ip, 'time' => $startTime, 'website_id' => (int)$websiteId]; $row = $connection->fetchRow($select, $bind); return $row['count']; @@ -58,14 +55,16 @@ public function getSendCount($object, $ip, $startTime, $websiteId = null) * @param int $ip * @param int $startTime * @param int $websiteId + * * @return $this */ public function addSendItem($ip, $startTime, $websiteId) { $this->getConnection()->insert( $this->getMainTable(), - ['ip' => ip2long($ip), 'time' => $startTime, 'website_id' => $websiteId] + ['ip' => $ip, 'time' => $startTime, 'website_id' => $websiteId] ); + return $this; } @@ -73,6 +72,7 @@ public function addSendItem($ip, $startTime, $websiteId) * Delete Old logs * * @param int $time + * * @return $this */ public function deleteLogsBefore($time) diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml index 47ef68cc9d765..ceae9c546bd3b 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml @@ -16,4 +16,10 @@ <data key="title">Best Way</data> <data key="methodName">Table Rate</data> </entity> + <!-- Set Table Rate Shipping method Condition --> + <entity name="TableRateShippingMethodConfig" type="shipping_method"> + <data key="package_weight">Weight vs. Destination</data> + <data key="package_value_with_discount">Price vs. Destination</data> + <data key="package_qty"># of Items vs. Destination</data> + </entity> </entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index 188b12c6a91c3..0c0372850a3c4 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -13,13 +13,14 @@ <features value="Configuration"/> <stories value="Disable configuration inputs"/> <title value="Check that all input fields disabled after executing CLI app:config:dump"/> - <description value="Check that all input fields disabled after executing CLI app:config:dump"/> + <description value="Check that all input fields disabled after executing CLI app:config:dump. Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again."/> <severity value="MAJOR"/> <testCaseId value="MC-11158"/> <useCaseId value="MAGETWO-96428"/> <group value="configuration"/> </annotations> <before> + <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> diff --git a/app/code/Magento/Store/Controller/Store/Redirect.php b/app/code/Magento/Store/Controller/Store/Redirect.php index 45924b5b0d28a..c20e3b31e09b1 100644 --- a/app/code/Magento/Store/Controller/Store/Redirect.php +++ b/app/code/Magento/Store/Controller/Store/Redirect.php @@ -21,10 +21,14 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; use Magento\Store\Model\StoreSwitcher\HashGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; /** * Builds correct url to target store (group) and performs redirect. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionInterface { @@ -47,6 +51,14 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @var StoreManagerInterface */ private $storeManager; + /** + * @var RedirectDataGenerator|null + */ + private $redirectDataGenerator; + /** + * @var ContextInterfaceFactory|null + */ + private $contextFactory; /** * @param Context $context @@ -55,8 +67,11 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @param Generic $session * @param SidResolverInterface $sidResolver * @param HashGenerator $hashGenerator - * @param StoreManagerInterface $storeManager + * @param StoreManagerInterface|null $storeManager + * @param RedirectDataGenerator|null $redirectDataGenerator + * @param ContextInterfaceFactory|null $contextFactory * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -65,13 +80,19 @@ public function __construct( Generic $session, SidResolverInterface $sidResolver, HashGenerator $hashGenerator, - StoreManagerInterface $storeManager = null + StoreManagerInterface $storeManager = null, + ?RedirectDataGenerator $redirectDataGenerator = null, + ?ContextInterfaceFactory $contextFactory = null ) { parent::__construct($context); $this->storeRepository = $storeRepository; $this->storeResolver = $storeResolver; $this->hashGenerator = $hashGenerator; $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + $this->redirectDataGenerator = $redirectDataGenerator + ?: ObjectManager::getInstance()->get(RedirectDataGenerator::class); + $this->contextFactory = $contextFactory + ?: ObjectManager::getInstance()->get(ContextInterfaceFactory::class); } /** @@ -85,7 +106,6 @@ public function execute() $currentStore = $this->storeRepository->getById($this->storeResolver->getCurrentStoreId()); $targetStoreCode = $this->_request->getParam(StoreResolver::PARAM_NAME); $fromStoreCode = $this->_request->getParam('___from_store'); - $error = null; if ($targetStoreCode === null) { return $this->_redirect($currentStore->getBaseUrl()); @@ -97,30 +117,33 @@ public function execute() /** @var Store $targetStore */ $targetStore = $this->storeRepository->get($targetStoreCode); $this->storeManager->setCurrentStore($targetStore); - } catch (NoSuchEntityException $e) { - $error = __("Requested store is not found ({$fromStoreCode})"); - } - - if ($error !== null) { - $this->messageManager->addErrorMessage($error); - $this->_redirect->redirect($this->_response, $currentStore->getBaseUrl()); - } else { $encodedUrl = $this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED); + $redirectData = $this->redirectDataGenerator->generate( + $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $this->_redirect->getRedirectUrl() + ] + ) + ); $query = [ '___from_store' => $fromStore->getCode(), StoreResolverInterface::PARAM_NAME => $targetStoreCode, ActionInterface::PARAM_NAME_URL_ENCODED => $encodedUrl, + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), ]; - - $customerHash = $this->hashGenerator->generateHash($fromStore); - $query = array_merge($query, $customerHash); - $arguments = [ '_nosid' => true, '_query' => $query ]; $this->_redirect->redirect($this->_response, 'stores/store/switch', $arguments); + } catch (NoSuchEntityException $e) { + $this->messageManager->addErrorMessage(__("Requested store is not found ({$fromStoreCode})")); + $this->_redirect->redirect($this->_response, $currentStore->getBaseUrl()); } return null; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/Context.php b/app/code/Magento/Store/Model/StoreSwitcher/Context.php new file mode 100644 index 0000000000000..c67dc3d67b01a --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/Context.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store switcher context + */ +class Context implements ContextInterface +{ + /** + * @var StoreInterface + */ + private $fromStore; + /** + * @var StoreInterface + */ + private $targetStore; + /** + * @var string + */ + private $redirectUrl; + + /** + * @param StoreInterface $fromStore + * @param StoreInterface $targetStore + * @param string $redirectUrl + */ + public function __construct( + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ) { + $this->fromStore = $fromStore; + $this->targetStore = $targetStore; + $this->redirectUrl = $redirectUrl; + } + + /** + * @inheritDoc + */ + public function getFromStore(): StoreInterface + { + return $this->fromStore; + } + + /** + * @inheritDoc + */ + public function getTargetStore(): StoreInterface + { + return $this->targetStore; + } + + /** + * @inheritDoc + */ + public function getRedirectUrl(): string + { + return $this->redirectUrl; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php new file mode 100644 index 0000000000000..a18c7cc9ccc27 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store switcher context interface + */ +interface ContextInterface +{ + /** + * Store to switch from + * + * @return StoreInterface + */ + public function getFromStore(): StoreInterface; + + /** + * Store to switch to + * + * @return StoreInterface + */ + public function getTargetStore(): StoreInterface; + + /** + * The URL to redirect after switching store + * + * @return string + */ + public function getRedirectUrl(): string; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php index d1858939434b7..3c2320df2ed1a 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php @@ -8,7 +8,6 @@ namespace Magento\Store\Model\StoreSwitcher; use Magento\Authorization\Model\UserContextInterface; -use Magento\Framework\App\ActionInterface; use Magento\Framework\App\DeploymentConfig as DeploymentConfig; use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Url\Helper\Data as UrlHelper; @@ -17,6 +16,10 @@ /** * Generate one time token and build redirect url + * + * @deplacated No longer used + * @see RedirectDataGenerator + * @see RedirectDataValidator */ class HashGenerator { diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php index 909fe9f6683f8..45e93a5af06de 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php @@ -7,71 +7,82 @@ namespace Magento\Store\Model\StoreSwitcher; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Model\ResourceModel\CustomerRepository; -use Magento\Customer\Model\Session as CustomerSession; -use Magento\Framework\App\DeploymentConfig as DeploymentConfig; use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Url\Helper\Data as UrlHelper; use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\StoreSwitcher\HashGenerator\HashData; use Magento\Store\Model\StoreSwitcherInterface; +use Psr\Log\LoggerInterface; /** * Process one time token and build redirect url * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class HashProcessor implements StoreSwitcherInterface { - /** - * @var HashGenerator - */ - private $hashGenerator; - /** * @var RequestInterface */ private $request; - + /** + * @var RedirectDataPostprocessorInterface + */ + private $postprocessor; + /** + * @var RedirectDataSerializerInterface + */ + private $dataSerializer; /** * @var ManagerInterface */ private $messageManager; - /** - * @var customerSession + * @var RedirectDataInterfaceFactory */ - private $customerSession; - + private $dataFactory; /** - * @var CustomerRepositoryInterface + * @var ContextInterfaceFactory */ - private $customerRepository; + private $contextFactory; + /** + * @var RedirectDataValidator + */ + private $dataValidator; + /** + * @var LoggerInterface + */ + private $logger; /** - * @param HashGenerator $hashGenerator * @param RequestInterface $request + * @param RedirectDataPostprocessorInterface $postprocessor + * @param RedirectDataSerializerInterface $dataSerializer * @param ManagerInterface $messageManager - * @param CustomerRepository $customerRepository - * @param CustomerSession $customerSession + * @param ContextInterfaceFactory $contextFactory + * @param RedirectDataInterfaceFactory $dataFactory + * @param RedirectDataValidator $dataValidator + * @param LoggerInterface $logger */ public function __construct( - HashGenerator $hashGenerator, RequestInterface $request, + RedirectDataPostprocessorInterface $postprocessor, + RedirectDataSerializerInterface $dataSerializer, ManagerInterface $messageManager, - CustomerRepository $customerRepository, - CustomerSession $customerSession + ContextInterfaceFactory $contextFactory, + RedirectDataInterfaceFactory $dataFactory, + RedirectDataValidator $dataValidator, + LoggerInterface $logger ) { - $this->hashGenerator = $hashGenerator; $this->request = $request; + $this->postprocessor = $postprocessor; + $this->dataSerializer = $dataSerializer; $this->messageManager = $messageManager; - $this->customerSession = $customerSession; - $this->customerRepository = $customerRepository; + $this->contextFactory = $contextFactory; + $this->dataFactory = $dataFactory; + $this->dataValidator = $dataValidator; + $this->logger = $logger; } /** @@ -85,41 +96,39 @@ public function __construct( */ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string { - $customerId = $this->request->getParam('customer_id'); - - if ($customerId) { - $fromStoreCode = (string)$this->request->getParam('___from_store'); - $timeStamp = (string)$this->request->getParam('time_stamp'); - $signature = (string)$this->request->getParam('signature'); - - $error = null; + $timestamp = (int) $this->request->getParam('time_stamp'); + $signature = (string) $this->request->getParam('signature'); + $data = (string) $this->request->getParam('data'); + $context = $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $redirectUrl + ] + ); + $redirectDataObject = $this->dataFactory->create( + [ + 'signature' => $signature, + 'timestamp' => $timestamp, + 'data' => $data + ] + ); - $data = new HashData( - [ - "customer_id" => $customerId, - "time_stamp" => $timeStamp, - "___from_store" => $fromStoreCode - ] - ); - - if ($redirectUrl && $this->hashGenerator->validateHash($signature, $data)) { - try { - $customer = $this->customerRepository->getById($customerId); - if (!$this->customerSession->isLoggedIn()) { - $this->customerSession->setCustomerDataAsLoggedIn($customer); - } - } catch (NoSuchEntityException $e) { - $error = __('The requested customer does not exist.'); - } catch (LocalizedException $e) { - $error = __('There was an error retrieving the customer record.'); - } + try { + if ($redirectUrl && $this->dataValidator->validate($context, $redirectDataObject)) { + $this->postprocessor->process($context, $this->dataSerializer->unserialize($data)); } else { - $error = __('The requested store cannot be found. Please check the request and try again.'); - } - - if ($error !== null) { - $this->messageManager->addErrorMessage($error); + throw new LocalizedException( + __('The requested store cannot be found. Please check the request and try again.') + ); } + } catch (LocalizedException $exception) { + $this->messageManager->addErrorMessage($exception->getMessage()); + } catch (\Throwable $exception) { + $this->logger->error($exception); + $this->messageManager->addErrorMessage( + __('Something went wrong.') + ); } return $redirectUrl; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php new file mode 100644 index 0000000000000..58185ea3d712a --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data + */ +class RedirectData implements RedirectDataInterface +{ + /** + * @var string + */ + private $signature; + /** + * @var string + */ + private $data; + /** + * @var int + */ + private $timestamp; + + /** + * @param string $signature + * @param string $data + * @param int $timestamp + */ + public function __construct( + string $signature, + string $data, + int $timestamp + ) { + $this->signature = $signature; + $this->data = $data; + $this->timestamp = $timestamp; + } + + /** + * @inheritDoc + */ + public function getSignature(): string + { + return $this->signature; + } + + /** + * @inheritDoc + */ + public function getData(): string + { + return $this->data; + } + + /** + * @inheritDoc + */ + public function getTimestamp(): int + { + return $this->timestamp; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php new file mode 100644 index 0000000000000..5360d403d1388 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use InvalidArgumentException; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Math\Random; +use Magento\Framework\Serialize\Serializer\Json; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Store switcher redirect data cache serializer + */ +class RedirectDataCacheSerializer implements RedirectDataSerializerInterface +{ + private const CACHE_KEY_PREFIX = 'store_switch_'; + private const CACHE_LIFE_TIME = 10; + private const CACHE_ID_LENGTH = 32; + + /** + * @var CacheInterface + */ + private $cache; + /** + * @var Json + */ + private $json; + /** + * @var Random + */ + private $random; + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Json $json + * @param Random $random + * @param CacheInterface $cache + * @param LoggerInterface $logger + */ + public function __construct( + Json $json, + Random $random, + CacheInterface $cache, + LoggerInterface $logger + ) { + $this->cache = $cache; + $this->json = $json; + $this->random = $random; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function serialize(array $data): string + { + $token = $this->random->getRandomString(self::CACHE_ID_LENGTH); + $cacheKey = self::CACHE_KEY_PREFIX . $token; + $this->cache->save($this->json->serialize($data), $cacheKey, [], self::CACHE_LIFE_TIME); + + return $token; + } + + /** + * @inheritDoc + */ + public function unserialize(string $data): array + { + if (strlen($data) !== self::CACHE_ID_LENGTH) { + throw new InvalidArgumentException("Invalid cache key '$data' supplied."); + } + + $cacheKey = self::CACHE_KEY_PREFIX . $data; + $json = $this->cache->load($cacheKey); + if (!$json) { + throw new InvalidArgumentException('Couldn\'t retrieve data from cache.'); + } + $result = $this->json->unserialize($json); + try { + $this->cache->remove($cacheKey); + } catch (Throwable $exception) { + $this->logger->error($exception); + } + + return $result; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php new file mode 100644 index 0000000000000..3ff0375a0c348 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; +use Psr\Log\LoggerInterface; + +/** + * Store switcher redirect data collector + */ +class RedirectDataGenerator +{ + /** + * @var RedirectDataPreprocessorInterface + */ + private $preprocessor; + /** + * @var RedirectDataSerializerInterface + */ + private $dataSerializer; + /** + * @var RedirectDataInterfaceFactory + */ + private $dataFactory; + /** + * @var Encryptor + */ + private $encryptor; + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Encryptor $encryptor + * @param RedirectDataPreprocessorInterface $preprocessor + * @param RedirectDataSerializerInterface $dataSerializer + * @param RedirectDataInterfaceFactory $dataFactory + * @param LoggerInterface $logger + */ + public function __construct( + Encryptor $encryptor, + RedirectDataPreprocessorInterface $preprocessor, + RedirectDataSerializerInterface $dataSerializer, + RedirectDataInterfaceFactory $dataFactory, + LoggerInterface $logger + ) { + $this->preprocessor = $preprocessor; + $this->dataSerializer = $dataSerializer; + $this->dataFactory = $dataFactory; + $this->encryptor = $encryptor; + $this->logger = $logger; + } + + /** + * Collect data to be redirected to the target store + * + * @param ContextInterface $context + * @return RedirectDataInterface + */ + public function generate(ContextInterface $context): RedirectDataInterface + { + $data = $this->preprocessor->process($context, []); + try { + $dataStr = $this->dataSerializer->serialize($data); + } catch (\Throwable $exception) { + $this->logger->error($exception); + $dataStr = ''; + } + $timestamp = time(); + $token = implode( + ',', + [ + $dataStr, + $timestamp, + $context->getFromStore()->getCode(), + $context->getTargetStore()->getCode(), + ] + ); + $signature = $this->encryptor->hash($token, Encryptor::HASH_VERSION_SHA256); + + return $this->dataFactory->create( + [ + 'data' => $dataStr, + 'timestamp' => $timestamp, + 'signature' => $signature + ] + ); + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php new file mode 100644 index 0000000000000..f7fc066634b63 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data interface + */ +interface RedirectDataInterface +{ + /** + * Redirect data signature + * + * @return string + */ + public function getSignature(): string; + + /** + * Data to redirect from store to store + * + * @return string + */ + public function getData(): string; + + /** + * Expire date of the redirect data + * + * @return int + */ + public function getTimestamp(): int; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php new file mode 100644 index 0000000000000..579ab80f31897 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data post-processors collection + */ +class RedirectDataPostprocessorComposite implements RedirectDataPostprocessorInterface +{ + /** + * @var RedirectDataPostprocessorInterface[] + */ + private $processors; + + /** + * @param RedirectDataPostprocessorInterface[] $processors + */ + public function __construct(array $processors = []) + { + $this->processors = $processors; + } + + /** + * @inheritdoc + */ + public function process(ContextInterface $context, array $data): void + { + foreach ($this->processors as $processor) { + $processor->process($context, $data); + } + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php new file mode 100644 index 0000000000000..de117915e23da --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data post-processor interface + */ +interface RedirectDataPostprocessorInterface +{ + /** + * Process data redirected from origin source + * + * @param ContextInterface $context + * @param array $data + */ + public function process(ContextInterface $context, array $data): void; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php new file mode 100644 index 0000000000000..4b93df1cdc677 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data pre-processors collection + */ +class RedirectDataPreprocessorComposite implements RedirectDataPreprocessorInterface +{ + /** + * @var RedirectDataPreprocessorInterface[] + */ + private $processors; + + /** + * @param RedirectDataPreprocessorInterface[] $processors + */ + public function __construct(array $processors = []) + { + $this->processors = $processors; + } + + /** + * @inheritdoc + */ + public function process(ContextInterface $context, array $data): array + { + foreach ($this->processors as $processor) { + $data = $processor->process($context, $data); + } + + return $data; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php new file mode 100644 index 0000000000000..d28a7dd776ab7 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data pre-processor interface + */ +interface RedirectDataPreprocessorInterface +{ + /** + * Collect data to be redirected to target store + * + * @param ContextInterface $context + * @param array $data + * @return array + */ + public function process(ContextInterface $context, array $data): array; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php new file mode 100644 index 0000000000000..0f7cde4d94ccd --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data serializer interface + */ +interface RedirectDataSerializerInterface +{ + /** + * Serialize provided data and return the serialized data + * + * @param array $data + * @return string + */ + public function serialize(array $data): string; + + /** + * Unserialize provided data and return the unserialized data + * + * @param string $data + * @return array + */ + public function unserialize(string $data): array; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php new file mode 100644 index 0000000000000..9200e80cae05c --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; + +/** + * Store switcher redirect data validator + */ +class RedirectDataValidator +{ + private const TIMEOUT = 5; + /** + * @var Encryptor + */ + private $encryptor; + + /** + * @param Encryptor $encryptor + */ + public function __construct( + Encryptor $encryptor + ) { + $this->encryptor = $encryptor; + } + + /** + * Validate data redirected from origin store + * + * @param ContextInterface $context + * @param RedirectDataInterface $redirectData + * @return bool + */ + public function validate(ContextInterface $context, RedirectDataInterface $redirectData) + { + $timeStamp = $redirectData->getTimestamp(); + $signature = $redirectData->getSignature(); + $value = implode( + ',', + [ + $redirectData->getData(), + $timeStamp, + $context->getFromStore()->getCode(), + $context->getTargetStore()->getCode() + ] + ); + return time() - $timeStamp <= self::TIMEOUT + && $this->encryptor->validateHash($value, $signature); + } +} diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.xml new file mode 100644 index 0000000000000..a3be7b0d8a8c4 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.xml @@ -0,0 +1,16 @@ +<?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="AdminOpenFirstRowInStoresGridActionGroup"> + + <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.xml new file mode 100644 index 0000000000000..6af4a4f159a7e --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.xml @@ -0,0 +1,16 @@ +<?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="AdminOpenStoreInFirstRowInStoresGridActionGroup"> + + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml index 09a33d5eb86a6..40a912617ee0b 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml @@ -51,8 +51,8 @@ <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> <argument name="storeGroupName" value="{{staticStoreGroup.name}}"/> </actionGroup> - <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> - <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <actionGroup ref="AdminOpenFirstRowInStoresGridActionGroup" stepKey="clickFirstRow"/> + <!--Update created Store group as per requirement and accept alert message--> <actionGroup ref="EditCustomStoreGroupAcceptWarningMessageActionGroup" stepKey="updateCustomStoreGroup"> <argument name="website" value="{{customWebsite.name}}"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml index 1c5d58c13538e..02125aab26496 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml @@ -41,8 +41,8 @@ <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> </actionGroup> - <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> - <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <actionGroup ref="AdminOpenFirstRowInStoresGridActionGroup" stepKey="clickFirstRow"/> + <!--Update created Store group as per requirement--> <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createNewCustomStoreGroup"> <argument name="website" value="{{_defaultWebsite.name}}"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml index c7c846c51af4d..b4aac676f2bc9 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml @@ -39,8 +39,8 @@ <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchCreatedStoreViewInGrid"> <argument name="storeViewName" value="{{storeViewData.name}}"/> </actionGroup> - <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> - <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <actionGroup ref="AdminOpenStoreInFirstRowInStoresGridActionGroup" stepKey="clickStoreViewFirstRowInGrid"/> + <!--Update created store view as per requirements--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="updateStoreView"> <argument name="StoreGroup" value="_defaultStoreGroup"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml index eaebc7fdaf74a..fc17bc7f0f10a 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml @@ -21,6 +21,9 @@ </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> diff --git a/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php b/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php index 91fff641338db..7d873ee6c1d8e 100755 --- a/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php +++ b/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php @@ -22,7 +22,10 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; use Magento\Store\Model\StoreSwitcher\HashGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -163,6 +166,11 @@ protected function setUp(): void ->method('getCurrentStoreId') ->willReturnSelf(); + $redirectDataGenerator = $this->createMock(RedirectDataGenerator::class); + $contextFactory = $this->createMock(ContextInterfaceFactory::class); + $contextFactory->method('create') + ->willReturn($this->createMock(ContextInterface::class)); + $objectManager = new ObjectManagerHelper($this); $context = $objectManager->getObject( Context::class, @@ -182,6 +190,8 @@ protected function setUp(): void 'sidResolver' => $this->sidResolverMock, 'hashGenerator' => $this->hashGeneratorMock, 'context' => $context, + 'redirectDataGenerator' => $redirectDataGenerator, + 'contextFactory' => $contextFactory, ] ); } @@ -220,11 +230,6 @@ public function testRedirect(string $defaultStoreViewCode, string $storeCode): v ->expects($this->once()) ->method('getCode') ->willReturn($defaultStoreViewCode); - $this->hashGeneratorMock - ->expects($this->once()) - ->method('generateHash') - ->with($this->fromStoreMock) - ->willReturn([]); $this->storeManagerMock ->expects($this->once()) ->method('setCurrentStore') @@ -239,7 +244,10 @@ public function testRedirect(string $defaultStoreViewCode, string $storeCode): v '_query' => [ 'uenc' => $defaultStoreViewCode, '___from_store' => $defaultStoreViewCode, - '___store' => $storeCode + '___store' => $storeCode, + 'data' => '', + 'time_stamp' => 0, + 'signature' => '', ] ] ); diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php new file mode 100644 index 0000000000000..89dc1d1c99ebd --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use InvalidArgumentException; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Message\ManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\HashProcessor; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class HashProcessorTest extends TestCase +{ + /** + * @var RequestInterface|MockObject + */ + private $request; + /** + * @var RedirectDataPostprocessorInterface|MockObject + */ + private $postprocessor; + /** + * @var RedirectDataSerializerInterface|MockObject + */ + private $dataSerializer; + /** + * @var ManagerInterface|MockObject + */ + private $messageManager; + /** + * @var RedirectDataValidator|MockObject + */ + private $dataValidator; + /** + * @var StoreInterface|MockObject + */ + private $store1; + /** + * @var StoreInterface|MockObject + */ + private $store2; + /** + * @var HashProcessor + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->request = $this->createMock(RequestInterface::class); + $this->postprocessor = $this->createMock(RedirectDataPostprocessorInterface::class); + $this->dataSerializer = $this->createMock(RedirectDataSerializerInterface::class); + $this->messageManager = $this->createMock(ManagerInterface::class); + $contextFactory = $this->createMock(ContextInterfaceFactory::class); + $dataFactory = $this->createMock(RedirectDataInterfaceFactory::class); + $this->dataValidator = $this->createMock(RedirectDataValidator::class); + $logger = $this->createMock(LoggerInterface::class); + $this->store1 = $this->createMock(StoreInterface::class); + $this->store2 = $this->createMock(StoreInterface::class); + $this->model = new HashProcessor( + $this->request, + $this->postprocessor, + $this->dataSerializer, + $this->messageManager, + $contextFactory, + $dataFactory, + $this->dataValidator, + $logger + ); + + $contextFactory->method('create') + ->willReturn($this->createMock(ContextInterface::class)); + $dataFactory->method('create') + ->willReturnCallback( + function (array $data) { + return $this->createConfiguredMock( + RedirectDataInterface::class, + [ + 'getTimestamp' => $data['timestamp'], + 'getData' => $data['data'], + 'getSignature' => $data['signature'], + ] + ); + } + ); + } + + public function testShouldProcessIfDataValidationPassed(): void + { + $redirectUrl = '/category-1/category-1.1.html'; + $this->request->method('getParam') + ->willReturnMap( + [ + ['time_stamp', null, time() - 1], + ['data', null, '{"customer_id":1}'], + ['signature', null, 'randomstring'], + ] + ); + $this->dataValidator->method('validate') + ->willReturn(true); + $this->dataSerializer->method('unserialize') + ->with('{"customer_id":1}') + ->willReturnCallback( + function ($arg) { + return json_decode($arg, true); + } + ); + $this->postprocessor->expects($this->once()) + ->method('process') + ->with($this->isInstanceOf(ContextInterface::class), ['customer_id' => 1]); + $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); + } + + public function testShouldNotProcessIfDataValidationFailed(): void + { + $redirectUrl = '/category-1/category-1.1.html'; + $this->dataValidator->method('validate') + ->willReturn(false); + $this->postprocessor->expects($this->never()) + ->method('process'); + $this->messageManager->expects($this->once()) + ->method('addErrorMessage') + ->with('The requested store cannot be found. Please check the request and try again.'); + + $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); + } + + public function testShouldNotProcessIfDataUnserializationFailed(): void + { + $redirectUrl = '/category-1/category-1.1.html'; + $this->dataValidator->method('validate') + ->willReturn(true); + $this->dataSerializer->method('unserialize') + ->willThrowException(new InvalidArgumentException('Invalid token supplied')); + $this->postprocessor->expects($this->never()) + ->method('process'); + $this->messageManager->expects($this->once()) + ->method('addErrorMessage') + ->with('Something went wrong.'); + + $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php new file mode 100644 index 0000000000000..c21d785b268a9 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use InvalidArgumentException; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Math\Random; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Model\StoreSwitcher\RedirectDataCacheSerializer; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use RuntimeException; + +class RedirectDataCacheSerializerTest extends TestCase +{ + private const RANDOM_STRING = '7ddf32e17a6ac5ce04a8ecbf782ca509'; + /** + * @var CacheInterface|MockObject + */ + private $cache; + /** + * @var RedirectDataCacheSerializer + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->cache = $this->createMock(CacheInterface::class); + $random = $this->createMock(Random::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataCacheSerializer( + new Json(), + $random, + $this->cache, + $logger + ); + $random->method('getRandomString')->willReturn(self::RANDOM_STRING); + } + + public function testSerialize(): void + { + $this->cache->expects($this->once()) + ->method('save') + ->with( + '{"customer_id":1}', + 'store_switch_' . self::RANDOM_STRING, + [], + 10 + ); + $this->assertEquals(self::RANDOM_STRING, $this->model->serialize(['customer_id' => 1])); + } + + public function testSerializeShouldThrowExceptionIfCannotSaveCache(): void + { + $exception = new RuntimeException('Failed to connect to cache server'); + $this->expectExceptionObject($exception); + $this->cache->expects($this->once()) + ->method('save') + ->willThrowException($exception); + $this->assertEquals(self::RANDOM_STRING, $this->model->serialize(['customer_id' => 1])); + } + + public function testUnserialize(): void + { + $this->cache->expects($this->once()) + ->method('load') + ->with('store_switch_' . self::RANDOM_STRING) + ->willReturn('{"customer_id":1}'); + $this->assertEquals(['customer_id' => 1], $this->model->unserialize(self::RANDOM_STRING)); + } + + public function testUnserializeShouldThrowExceptionIfCacheHasExpired(): void + { + $this->expectExceptionObject(new InvalidArgumentException('Couldn\'t retrieve data from cache.')); + $this->cache->expects($this->once()) + ->method('load') + ->with('store_switch_' . self::RANDOM_STRING) + ->willReturn(null); + $this->assertEquals(['customer_id' => 1], $this->model->unserialize(self::RANDOM_STRING)); + } + + public function testUnserializeShouldThrowExceptionIfCacheKeyIsInvalid(): void + { + $this->expectExceptionObject(new InvalidArgumentException('Invalid cache key \'abc\' supplied.')); + $this->cache->expects($this->never()) + ->method('load'); + $this->assertEquals(['customer_id' => 1], $this->model->unserialize('abc')); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php new file mode 100644 index 0000000000000..67270f5f70dce --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataGeneratorTest extends TestCase +{ + /** + * @var RedirectDataPreprocessorInterface|MockObject + */ + private $preprocessor; + /** + * @var RedirectDataSerializerInterface|MockObject + */ + private $dataSerializer; + /** + * @var ContextInterface|MockObject + */ + private $context; + /** + * @var RedirectDataGenerator + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->preprocessor = $this->createMock(RedirectDataPreprocessorInterface::class); + $this->dataSerializer = $this->createMock(RedirectDataSerializerInterface::class); + $dataFactory = $this->createMock(RedirectDataInterfaceFactory::class); + $encryptor = $this->createMock(Encryptor::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataGenerator( + $encryptor, + $this->preprocessor, + $this->dataSerializer, + $dataFactory, + $logger + ); + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + $encryptor->method('hash') + ->willReturnCallback( + function (string $arg1) { + // phpcs:ignore Magento2.Security.InsecureFunction + return md5($arg1); + } + ); + $dataFactory->method('create') + ->willReturnCallback( + function (array $data) { + return $this->createConfiguredMock( + RedirectDataInterface::class, + [ + 'getTimestamp' => $data['timestamp'], + 'getData' => $data['data'], + 'getSignature' => $data['signature'], + ] + ); + } + ); + } + + public function testGenerate(): void + { + $this->preprocessor->method('process') + ->willReturn(['customer_id' => 1]); + $this->dataSerializer->method('serialize') + ->willReturnCallback('json_encode'); + $redirectData = $this->model->generate($this->context); + $time = time(); + $this->assertEqualsWithDelta($time, $redirectData->getTimestamp(), 1); + $time = $redirectData->getTimestamp(); + $this->assertEquals('{"customer_id":1}', $redirectData->getData()); + // phpcs:ignore Magento2.Security.InsecureFunction + $this->assertEquals(md5("{\"customer_id\":1},{$time},fr,en"), $redirectData->getSignature()); + } + + public function testShouldGenerateEmptyDataIfDataSerializationFailed(): void + { + $this->dataSerializer->method('serialize') + ->willThrowException(new \InvalidArgumentException('Failed to connect to cache server')); + + $redirectData = $this->model->generate($this->context); + $time = time(); + $this->assertEqualsWithDelta($time, $redirectData->getTimestamp(), 1); + $time = $redirectData->getTimestamp(); + $this->assertEquals('', $redirectData->getData()); + // phpcs:ignore Magento2.Security.InsecureFunction + $this->assertEquals(md5(",{$time},fr,en"), $redirectData->getSignature()); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php new file mode 100644 index 0000000000000..9960fad2071be --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataValidator; +use PHPUnit\Framework\TestCase; + +class RedirectDataValidatorTest extends TestCase +{ + /** + * @var RedirectDataValidator + */ + private $model; + /** + * @var ContextInterface + */ + private $context; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $encryptor = $this->createMock(Encryptor::class); + $this->model = new RedirectDataValidator( + $encryptor + ); + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + $encryptor->method('validateHash') + ->willReturnCallback( + function (string $value, string $hash) { + // phpcs:ignore Magento2.Security.InsecureFunction + return md5($value) === $hash; + } + ); + } + + /** + * @param array $params + * @param bool $result + * @dataProvider validationDataProvider + */ + public function testValidation(array $params, bool $result): void + { + $originalData = '{"customer_id":1}'; + $timestamp = time() - $params['elapsedTime']; + $fromStoreCode = $params['fromStoreCode'] ?? $this->context->getFromStore()->getCode(); + $targetStoreCode = $params['targetStoreCode'] ?? $this->context->getTargetStore()->getCode(); + // phpcs:ignore Magento2.Security.InsecureFunction + $signature = md5("{$originalData},{$timestamp},{$fromStoreCode},{$targetStoreCode}"); + $redirectData = $this->createConfiguredMock( + RedirectDataInterface::class, + [ + 'getTimestamp' => $params['timestamp'] ?? $timestamp, + 'getData' => $params['data'] ?? $originalData, + 'getSignature' => $params['signature'] ?? $signature, + ] + ); + $this->assertEquals($result, $this->model->validate($this->context, $redirectData)); + } + + /** + * @return array + */ + public function validationDataProvider(): array + { + return [ + [ + [ + 'elapsedTime' => 1, + ], + true + ], + [ + [ + 'elapsedTime' => 6, + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'data' => '{"customer_id":2}' + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'fromStoreCode' => 'es' + + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'targetStoreCode' => 'de' + + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'signature' => 'abcd1efgh2ijkl3mnop4qrst5uvwx6yz' + + ], + false + ] + ]; + } +} diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 2da9e91e1fddd..ccfec562ba103 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -26,6 +26,11 @@ <preference for="Magento\Framework\App\ScopeTreeProviderInterface" type="Magento\Store\Model\ScopeTreeProvider"/> <preference for="Magento\Framework\App\ScopeValidatorInterface" type="Magento\Store\Model\ScopeValidator"/> <preference for="Magento\Store\Model\StoreSwitcherInterface" type="Magento\Store\Model\StoreSwitcher" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface" type="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorComposite" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface" type="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorComposite" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface" type="Magento\Store\Model\StoreSwitcher\RedirectDataCacheSerializer" /> + <preference for="Magento\Store\Model\StoreSwitcher\ContextInterface" type="Magento\Store\Model\StoreSwitcher\Context" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataInterface" type="Magento\Store\Model\StoreSwitcher\RedirectData" /> <type name="Magento\Framework\App\Http\Context"> <arguments> <argument name="default" xsi:type="array"> diff --git a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php index 939facd02c02d..612573ff493ad 100644 --- a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php +++ b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php @@ -21,7 +21,7 @@ protected function calculateWithTaxInPrice(QuoteDetailsItemInterface $item, $qua $this->taxClassManagement->getTaxClassId($item->getTaxClassKey()) ); $rate = $this->calculationTool->getRate($taxRateRequest); - $storeRate = $storeRate = $this->calculationTool->getStoreRate($taxRateRequest, $this->storeId); + $storeRate = $this->calculationTool->getStoreRate($taxRateRequest, $this->storeId); $discountTaxCompensationAmount = 0; $applyTaxAfterDiscount = $this->config->applyTaxAfterDiscount($this->storeId); diff --git a/app/code/Magento/Tax/Model/Calculation/UnitBaseCalculator.php b/app/code/Magento/Tax/Model/Calculation/UnitBaseCalculator.php index ed469e822d937..655fcc9749cb3 100644 --- a/app/code/Magento/Tax/Model/Calculation/UnitBaseCalculator.php +++ b/app/code/Magento/Tax/Model/Calculation/UnitBaseCalculator.php @@ -10,7 +10,15 @@ class UnitBaseCalculator extends AbstractCalculator { /** - * {@inheritdoc} + * Determines the rounding operation type and rounds the amount + * + * @param float $amount + * @param string $rate + * @param bool $direction + * @param string $type + * @param bool $round + * @param QuoteDetailsItemInterface $item + * @return float|string */ protected function roundAmount( $amount, @@ -31,7 +39,12 @@ protected function roundAmount( } /** - * {@inheritdoc} + * Calculate tax details for quote item with tax in price with given quantity + * + * @param QuoteDetailsItemInterface $item + * @param int $quantity + * @param bool $round + * @return \Magento\Tax\Api\Data\TaxDetailsItemInterface */ protected function calculateWithTaxInPrice(QuoteDetailsItemInterface $item, $quantity, $round = true) { @@ -39,7 +52,7 @@ protected function calculateWithTaxInPrice(QuoteDetailsItemInterface $item, $qua $this->taxClassManagement->getTaxClassId($item->getTaxClassKey()) ); $rate = $this->calculationTool->getRate($taxRateRequest); - $storeRate = $storeRate = $this->calculationTool->getStoreRate($taxRateRequest, $this->storeId); + $storeRate = $this->calculationTool->getStoreRate($taxRateRequest, $this->storeId); // Calculate $priceInclTax $applyTaxAfterDiscount = $this->config->applyTaxAfterDiscount($this->storeId); @@ -104,7 +117,12 @@ protected function calculateWithTaxInPrice(QuoteDetailsItemInterface $item, $qua } /** - * {@inheritdoc} + * Calculate tax details for quote item with tax not in price with given quantity + * + * @param QuoteDetailsItemInterface $item + * @param int $quantity + * @param bool $round + * @return \Magento\Tax\Api\Data\TaxDetailsItemInterface */ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $quantity, $round = true) { diff --git a/app/code/Magento/Tax/Model/Plugin/OrderSave.php b/app/code/Magento/Tax/Model/Plugin/OrderSave.php index 38952eec02ca1..b46c5b51a9db2 100644 --- a/app/code/Magento/Tax/Model/Plugin/OrderSave.php +++ b/app/code/Magento/Tax/Model/Plugin/OrderSave.php @@ -50,11 +50,14 @@ public function afterSave( } /** + * Save order tax + * * @param \Magento\Sales\Api\Data\OrderInterface $order * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ protected function saveOrderTax(\Magento\Sales\Api\Data\OrderInterface $order) { @@ -176,7 +179,9 @@ protected function saveOrderTax(\Magento\Sales\Api\Data\OrderInterface $order) } elseif (isset($quoteItemId['associated_item_id'])) { //This item is associated with a product item $item = $order->getItemByQuoteItemId($quoteItemId['associated_item_id']); - $associatedItemId = $item->getId(); + if ($item !== null && $item->getId()) { + $associatedItemId = $item->getId(); + } } $data = [ diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php index 7863b70f6626a..d34e863d56c54 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php @@ -206,7 +206,7 @@ public function getOptionRates() { $size = self::TAX_RULES_CHUNK_SIZE; $page = 1; - $rates = [[]]; + $rates = []; do { $offset = $size * ($page - 1); $this->getSelect()->reset(); @@ -222,6 +222,6 @@ public function getOptionRates() $page++; } while ($this->getSize() > $offset); - return array_merge(...$rates); + return array_merge([], ...$rates); } } diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml index fde43cd10e3ea..fd0cb31fd8655 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml @@ -123,4 +123,17 @@ <entity name="TaxRuleZeroRate" type="taxRule"> <data key="name" unique="suffix">TaxNameZeroRate</data> </entity> + <entity name="DefaultTaxRuleWithCustomTaxRate" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">0</data> + <data key="priority">0</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <var key="tax_rate_ids" entityType="taxRate" entityKey="id"/> + <data key="calculate_subtotal">false</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php index 2925ebef958b6..d98bd4a0722ee 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -175,50 +175,51 @@ public function verifyItemTaxes($expectedItemTaxes) } /** + * Test for order afterSave + * * @dataProvider afterSaveDataProvider + * @param array $appliedTaxes + * @param array $itemAppliedTaxes + * @param array $expectedTaxes + * @param array $expectedItemTaxes + * @param int|null $itemId + * @return void */ public function testAfterSave( - $appliedTaxes, - $itemAppliedTaxes, - $expectedTaxes, - $expectedItemTaxes - ) { + array $appliedTaxes, + array $itemAppliedTaxes, + array $expectedTaxes, + array $expectedItemTaxes, + ?int $itemId + ): void { $orderMock = $this->setupOrderMock(); $extensionAttributeMock = $this->setupExtensionAttributeMock(); - $extensionAttributeMock->expects($this->any()) - ->method('getConvertingFromQuote') + $extensionAttributeMock->method('getConvertingFromQuote') ->willReturn(true); - $extensionAttributeMock->expects($this->any()) - ->method('getAppliedTaxes') + $extensionAttributeMock->method('getAppliedTaxes') ->willReturn($appliedTaxes); - $extensionAttributeMock->expects($this->any()) - ->method('getItemAppliedTaxes') + $extensionAttributeMock->method('getItemAppliedTaxes') ->willReturn($itemAppliedTaxes); $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) ->disableOriginalConstructor() ->setMethods(['getId']) ->getMock(); - $orderItemMock->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(self::ORDER_ITEM_ID); - $orderMock->expects($this->once()) - ->method('getAppliedTaxIsSaved') + $orderItemMock->method('getId') + ->willReturn($itemId); + $orderMock->method('getAppliedTaxIsSaved') ->willReturn(false); - $orderMock->expects($this->once()) - ->method('getExtensionAttributes') + $orderMock->method('getExtensionAttributes') ->willReturn($extensionAttributeMock); - $orderMock->expects($this->atLeastOnce()) - ->method('getItemByQuoteItemId') + $itemByQuoteId = $itemId ? $orderItemMock : $itemId; + $orderMock->method('getItemByQuoteItemId') ->with(self::ITEMID) - ->willReturn($orderItemMock); - $orderMock->expects($this->atLeastOnce()) - ->method('getEntityId') + ->willReturn($itemByQuoteId); + $orderMock->method('getEntityId') ->willReturn(self::ORDERID); - $orderMock->expects($this->once()) - ->method('setAppliedTaxIsSaved') + $orderMock->method('setAppliedTaxIsSaved') ->with(true); $this->verifyOrderTaxes($expectedTaxes); @@ -228,10 +229,12 @@ public function testAfterSave( } /** + * After save data provider + * * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function afterSaveDataProvider() + public function afterSaveDataProvider(): array { return [ //one item with shipping @@ -485,6 +488,257 @@ public function afterSaveDataProvider() 'taxable_item_type' => 'shipping', ], ], + 'item_id' => self::ORDER_ITEM_ID, + ], + 'associated_item_with_empty_order_quote_item' => [ + 'applied_taxes' => [ + [ + 'amount' => 0.66, + 'base_amount' => 0.66, + 'percent' => 11, + 'id' => 'ILUS', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ] + ], + ], + [ + 'amount' => 0.2, + 'base_amount' => 0.2, + 'percent' => 3.33, + 'id' => 'CityTax', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ] + ], + ], + ], + 'item_applied_taxes' => [ + //item tax, three tax rates + [ + //first two taxes are combined + 'item_id' => null, + 'type' => 'product', + 'associated_item_id' => self::ITEMID, + 'applied_taxes' => [ + [ + 'amount' => 0.11, + 'base_amount' => 0.11, + 'percent' => 11, + 'id' => 'ILUS', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ] + ], + ], + //city tax + [ + 'amount' => 0.03, + 'base_amount' => 0.03, + 'percent' => 3.33, + 'id' => 'CityTax', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ] + ], + ], + ], + ], + //shipping tax + [ + //first two taxes are combined + 'item_id' => null, + 'type' => 'shipping', + 'associated_item_id' => null, + 'applied_taxes' => [ + [ + 'amount' => 0.55, + 'base_amount' => 0.55, + 'percent' => 11, + 'id' => 'ILUS', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ] + ], + ], + //city tax + [ + 'amount' => 0.17, + 'base_amount' => 0.17, + 'percent' => 3.33, + 'id' => 'CityTax', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ] + ], + ], + ], + ], + ], + 'expected_order_taxes' => [ + //state tax + '35' => [ + 'order_id' => self::ORDERID, + 'code' => 'IL', + 'title' => 'IL', + 'hidden' => 0, + 'percent' => 6, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.66, + 'base_amount' => 0.66, + 'process' => 0, + 'base_real_amount' => 0.36, + ], + //federal tax + '36' => [ + 'order_id' => self::ORDERID, + 'code' => 'US', + 'title' => 'US', + 'hidden' => 0, + 'percent' => 5, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.66, //combined amount + 'base_amount' => 0.66, + 'process' => 0, + 'base_real_amount' => 0.3, //portion for specific rate + ], + //city tax + '37' => [ + 'order_id' => self::ORDERID, + 'code' => 'CityTax', + 'title' => 'CityTax', + 'hidden' => 0, + 'percent' => 3, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.2, //combined amount + 'base_amount' => 0.2, + 'process' => 0, + 'base_real_amount' => 0.18018018018018, //this number is meaningless since this is single rate + ], + ], + 'expected_item_taxes' => [ + [ + //state tax for item + 'item_id' => null, + 'tax_id' => '35', + 'tax_percent' => 6, + 'associated_item_id' => null, + 'amount' => 0.11, + 'base_amount' => 0.11, + 'real_amount' => 0.06, + 'real_base_amount' => 0.06, + 'taxable_item_type' => 'product', + ], + [ + //state tax for shipping + 'item_id' => null, + 'tax_id' => '35', + 'tax_percent' => 6, + 'associated_item_id' => null, + 'amount' => 0.55, + 'base_amount' => 0.55, + 'real_amount' => 0.3, + 'real_base_amount' => 0.3, + 'taxable_item_type' => 'shipping', + ], + [ + //federal tax for item + 'item_id' => null, + 'tax_id' => '36', + 'tax_percent' => 5, + 'associated_item_id' => null, + 'amount' => 0.11, + 'base_amount' => 0.11, + 'real_amount' => 0.05, + 'real_base_amount' => 0.05, + 'taxable_item_type' => 'product', + ], + [ + //federal tax for shipping + 'item_id' => null, + 'tax_id' => '36', + 'tax_percent' => 5, + 'associated_item_id' => null, + 'amount' => 0.55, + 'base_amount' => 0.55, + 'real_amount' => 0.25, + 'real_base_amount' => 0.25, + 'taxable_item_type' => 'shipping', + ], + [ + //city tax for item + 'item_id' => null, + 'tax_id' => '37', + 'tax_percent' => 3.33, + 'associated_item_id' => null, + 'amount' => 0.03, + 'base_amount' => 0.03, + 'real_amount' => 0.03, + 'real_base_amount' => 0.03, + 'taxable_item_type' => 'product', + ], + [ + //city tax for shipping + 'item_id' => null, + 'tax_id' => '37', + 'tax_percent' => 3.33, + 'associated_item_id' => null, + 'amount' => 0.17, + 'base_amount' => 0.17, + 'real_amount' => 0.17, + 'real_base_amount' => 0.17, + 'taxable_item_type' => 'shipping', + ], + ], + 'item_id' => null, ], ]; } diff --git a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php index e528f9e88d8a4..c998c02d46b3c 100644 --- a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php +++ b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php @@ -94,17 +94,17 @@ public function getPageLayoutsConfig() protected function getConfigFiles() { if (!$this->configFiles) { - $configFiles = []; $this->configFiles = $this->cacheModel->load(self::CACHE_KEY_LAYOUTS); if (!empty($this->configFiles)) { //if value in cache is corrupted. $this->configFiles = $this->serializer->unserialize($this->configFiles); } if (empty($this->configFiles)) { + $configFiles = []; foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { $configFiles[] = $this->fileCollector->getFilesContent($theme, 'layouts.xml'); } - $this->configFiles = array_merge(...$configFiles); + $this->configFiles = array_merge([], ...$configFiles); $this->cacheModel->save($this->serializer->serialize($this->configFiles), self::CACHE_KEY_LAYOUTS); } } diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php index ef2df77e7daff..3600992011ed6 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php @@ -111,7 +111,7 @@ public function getComponentName() public function convertDate($date, $hour = 0, $minute = 0, $second = 0, $setUtcTimeZone = true) { try { - $dateObj = $this->localeDate->date($date, $this->getLocale(), false); + $dateObj = $this->localeDate->date($date, $this->getLocale(), false, false); $dateObj->setTime($hour, $minute, $second); //convert store date to default date in UTC timezone without DST if ($setUtcTimeZone) { diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 73bef62910644..e7dc245d47d6f 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -54,13 +54,14 @@ define([ this.$fileInput = fileInput; _.extend(this.uploaderConfig, { - dropZone: $(fileInput).closest(this.dropZone), - change: this.onFilesChoosed.bind(this), - drop: this.onFilesChoosed.bind(this), - add: this.onBeforeFileUpload.bind(this), - done: this.onFileUploaded.bind(this), - start: this.onLoadingStart.bind(this), - stop: this.onLoadingStop.bind(this) + dropZone: $(fileInput).closest(this.dropZone), + change: this.onFilesChoosed.bind(this), + drop: this.onFilesChoosed.bind(this), + add: this.onBeforeFileUpload.bind(this), + fail: this.onFail.bind(this), + done: this.onFileUploaded.bind(this), + start: this.onLoadingStart.bind(this), + stop: this.onLoadingStop.bind(this) }); $(fileInput).fileupload(this.uploaderConfig); @@ -328,11 +329,12 @@ define([ * May be used for implementation of additional validation rules, * e.g. total files and a total size rules. * - * @param {Event} e - Event object. + * @param {Event} event - Event object. * @param {Object} data - File data that will be uploaded. */ - onFilesChoosed: function (e, data) { - // no option exists in fileuploader for restricting upload chains to single files; this enforces that policy + onFilesChoosed: function (event, data) { + // no option exists in file uploader for restricting upload chains to single files + // this enforces that policy if (!this.isMultipleFiles) { data.files.splice(1); } @@ -341,13 +343,13 @@ define([ /** * Handler which is invoked prior to the start of a file upload. * - * @param {Event} e - Event object. + * @param {Event} event - Event object. * @param {Object} data - File data that will be uploaded. */ - onBeforeFileUpload: function (e, data) { - var file = data.files[0], - allowed = this.isFileAllowed(file), - target = $(e.target); + onBeforeFileUpload: function (event, data) { + var file = data.files[0], + allowed = this.isFileAllowed(file), + target = $(event.target); if (this.disabled()) { this.notifyError($t('The file upload field is disabled.')); @@ -356,7 +358,7 @@ define([ } if (allowed.passed) { - target.on('fileuploadsend', function (event, postData) { + target.on('fileuploadsend', function (eventBound, postData) { postData.data.append('param_name', this.paramName); }.bind(data)); @@ -386,16 +388,25 @@ define([ }); }, + /** + * @param {Event} event + * @param {Object} data + */ + onFail: function (event, data) { + console.error(data.jqXHR.responseText); + console.error(data.jqXHR.status); + }, + /** * Handler of the file upload complete event. * - * @param {Event} e + * @param {Event} event * @param {Object} data */ - onFileUploaded: function (e, data) { + onFileUploaded: function (event, data) { var uploadedFilename = data.files[0].name, - file = data.result, - error = file.error; + file = data.result, + error = file.error; error ? this.aggregateError(uploadedFilename, error) : @@ -469,10 +480,10 @@ define([ * Handler of the preview image load event. * * @param {Object} file - File associated with an image. - * @param {Event} e + * @param {Event} event */ - onPreviewLoad: function (file, e) { - var img = e.currentTarget; + onPreviewLoad: function (file, event) { + var img = event.currentTarget; file.previewWidth = img.naturalWidth; file.previewHeight = img.naturalHeight; diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index b6e539bdadcb9..4d0e3efee35aa 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1135,7 +1135,7 @@ protected function _getXmlTracking($trackings) </TrackRequest> XMLAuth; - $trackingResponses[] = $this->asyncHttpClient->request( + $trackingResponses[$tracking] = $this->asyncHttpClient->request( new Request( $url, Request::METHOD_POST, @@ -1144,13 +1144,9 @@ protected function _getXmlTracking($trackings) ) ); } - foreach ($trackingResponses as $response) { + foreach ($trackingResponses as $tracking => $response) { $httpResponse = $response->get(); - if ($httpResponse->getStatusCode() >= 400) { - $xmlResponse = ''; - } else { - $xmlResponse = $httpResponse->getBody(); - } + $xmlResponse = $httpResponse->getStatusCode() >= 400 ? '' : $httpResponse->getBody(); $this->_parseXmlTrackingResponse($tracking, $xmlResponse); } @@ -1362,10 +1358,11 @@ public function getAllowedMethods() protected function _formShipmentRequest(DataObject $request) { $packages = $request->getPackages(); + $shipmentItems = []; foreach ($packages as $package) { $shipmentItems[] = $package['items']; } - $shipmentItems = array_merge(...$shipmentItems); + $shipmentItems = array_merge([], ...$shipmentItems); $xmlRequest = $this->_xmlElFactory->create( ['data' => '<?xml version = "1.0" ?><ShipmentConfirmRequest xml:lang="en-US"/>'] @@ -1528,24 +1525,18 @@ protected function _formShipmentRequest(DataObject $request) } if ($deliveryConfirmation && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_PACKAGE) { - $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); - $serviceOptionsNode->addChild( - 'DeliveryConfirmation' - )->addChild( - 'DCISType', - $deliveryConfirmation - ); + $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); + $serviceOptionsNode + ->addChild('DeliveryConfirmation') + ->addChild('DCISType', $deliveryConfirmation); } } if (isset($deliveryConfirmation) && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_SHIPMENT) { $serviceOptionsNode = $shipmentPart->addChild('ShipmentServiceOptions'); - $serviceOptionsNode->addChild( - 'DeliveryConfirmation' - )->addChild( - 'DCISType', - $deliveryConfirmation - ); + $serviceOptionsNode + ->addChild('DeliveryConfirmation') + ->addChild('DCISType', $deliveryConfirmation); } $shipmentPart->addChild('PaymentInformation') @@ -1627,6 +1618,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) try { $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); } catch (Throwable $e) { + $response = $this->_xmlElFactory->create(['data' => '']); $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; } @@ -1800,6 +1792,7 @@ protected function _doShipmentRequest(DataObject $request) $this->setXMLAccessRequest(); $xmlRequest = $this->_xmlAccessRequest . $rawXmlRequest; $xmlResponse = $this->_getCachedQuotes($xmlRequest); + $debugData = []; if ($xmlResponse === null) { $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index e6b03755bea47..6430f71765fe4 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -64,7 +64,9 @@ public function resolve( $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; - $url = $args['url']; + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlParts = parse_url($args['url']); + $url = $urlParts['path'] ?? $args['url']; if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); } @@ -81,6 +83,9 @@ public function resolve( 'redirectCode' => $this->redirectType, 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) ]; + if (!empty($urlParts['query'])) { + $resultArray['relative_url'] .= '?' . $urlParts['query']; + } if (empty($resultArray['id'])) { throw new GraphQlNoSuchEntityException( diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml deleted file mode 100644 index 6a0d0c9210396..0000000000000 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?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="AdminDeleteRoleSection"> - <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> - <element name="salesRole" selector="//td[contains(text(), 'Sales')]" type="button"/> - <element name="role" parameterized="true" selector="//td[contains(@class,'col-role_name') and contains(text(), '{{roleName}}')]" type="button"/> - <element name="current_pass" type="button" selector="#current_password"/> - <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> - <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> - </section> -</sections> - diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml deleted file mode 100644 index 21ca1cb36f988..0000000000000 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?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="AdminDeleteUserSection"> - <element name="role" parameterized="true" selector="//td[contains(text(), '{{roleName}}')]" type="button"/> - <element name="password" selector="#user_current_password" type="input"/> - <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> - <element name="confirm" selector=".action-primary.action-accept" type="button"/> - </section> -</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml index e369d037d28f6..dba7dd4cd520c 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml @@ -9,7 +9,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminDeleteRoleSection"> <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> - <element name="role" parameterized="true" selector="//td[contains(text(), '{{args}}')]" type="button"/> + <element name="salesRole" selector="//td[contains(text(), 'Sales')]" type="button"/> + <element name="role" parameterized="true" selector="//td[contains(@class,'col-role_name') and contains(text(), '{{roleName}}')]" type="button"/> <element name="current_pass" type="button" selector="#current_password"/> <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml index d4718ca43d6cf..3937ee75c6b7d 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml @@ -11,7 +11,7 @@ <element name="theUser" selector="//td[contains(text(), '{{userName}}')]" type="button" parameterized="true"/> <element name="password" selector="#user_current_password" type="input"/> <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> - <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + <element name="confirm" selector=".action-primary.action-accept" type="button"/> <element name="role" parameterized="true" selector="//td[contains(text(), '{{args}}')]" type="button"/> </section> </sections> diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index f38c0f0978536..5ead1beb722dd 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -33,10 +33,8 @@ class Generator extends AbstractSchemaGenerator */ const ERROR_SCHEMA = '#/definitions/error-response'; - /** Unauthorized description */ const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized'; - /** Array signifier */ const ARRAY_SIGNIFIER = '[0]'; /** @@ -758,7 +756,7 @@ private function handleComplex($name, $type, $prefix, $isArray) ); } - return empty($queryNames) ? [] : array_merge(...$queryNames); + return array_merge([], ...$queryNames); } /** diff --git a/app/code/Magento/Weee/Model/Total/Quote/Weee.php b/app/code/Magento/Weee/Model/Total/Quote/Weee.php index 449c6cd688668..e7ae84c15a51f 100644 --- a/app/code/Magento/Weee/Model/Total/Quote/Weee.php +++ b/app/code/Magento/Weee/Model/Total/Quote/Weee.php @@ -306,12 +306,12 @@ protected function getNextIncrement() */ protected function recalculateParent(AbstractItem $item) { - $associatedTaxables = [[]]; + $associatedTaxables = []; foreach ($item->getChildren() as $child) { $associatedTaxables[] = $child->getAssociatedTaxables(); } $item->setAssociatedTaxables( - array_unique(array_merge(...$associatedTaxables)) + array_unique(array_merge([], ...$associatedTaxables)) ); } diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml index 6dab476115cee..a3af1dd95e70c 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml @@ -5,11 +5,12 @@ */ /** @var \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout $block */ +/** @var \Magento\Framework\Escaper $escaper */ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset class="fieldset"> - <legend class="legend"><span><?= $block->escapeHtml(__('Layout Updates')) ?></span></legend> + <legend class="legend"><span><?= $escaper->escapeHtml(__('Layout Updates')) ?></span></legend> <br /> <div class="widget-layout-updates"> <div id="page_group_container"></div> @@ -45,56 +46,56 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" script; foreach ($block->getDisplayOnContainers() as $container): $scriptString .= <<<script - '<div class="no-display {$block->escapeJs($container['code'])} group_container" '+ - 'id="{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '<div class="no-display {$escaper->escapeJs($container['code'])} group_container" '+ + 'id="{$escaper->escapeJs($container['name'])}_<%- data.id %>">'+ '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ - 'value="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}]" />'+ + 'value="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}]" />'+ '<input disabled="disabled" type="hidden" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][page_id]" '+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][page_id]" '+ 'value="<%- data.page_id %>" />'+ '<input disabled="disabled" type="hidden" class="layout_handle_pattern" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][layout_handle]" '+ - 'value="{$block->escapeJs($container['layout_handle'])}" />'+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][layout_handle]" '+ + 'value="{$escaper->escapeJs($container['layout_handle'])}" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label>{$block->escapeJs(__('%1', $container['label']))}</label></th>'+ - '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ - '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ + '<th><label>{$escaper->escapeJs(__('%1', $container['label']))}</label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ '<td>'+ '<input disabled="disabled" type="radio" class="radio for_all" '+ - 'id="all_{$block->escapeJs($container['name'])}_<%- data.id %>" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][for]" '+ + 'id="all_{$escaper->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][for]" '+ 'value="all" checked="checked" /> '+ - '<label for="all_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ - '{$block->escapeJs(__('All'))}</label><br />'+ + '<label for="all_{$escaper->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$escaper->escapeJs(__('All'))}</label><br />'+ '<input disabled="disabled" type="radio" class="radio for_specific" '+ - 'id="specific_{$block->escapeJs($container['name'])}_<%- data.id %>" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][for]" '+ + 'id="specific_{$escaper->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][for]" '+ 'value="specific" /> '+ - '<label for="specific_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ - '{$block->escapeJs(__('Specific %1', $container['label']))}</label>'+ + '<label for="specific_{$escaper->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$escaper->escapeJs(__('Specific %1', $container['label']))}</label>'+ script; $scriptString1 = $secureRenderer->renderEventListenerAsTag( "onclick", "WidgetInstance.togglePageGroupChooser(this)", - "all_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + "all_" . $escaper->escapeJs($container['name']) . "_<%- data.id %>" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString1 = $secureRenderer->renderEventListenerAsTag( "onclick", "WidgetInstance.togglePageGroupChooser(this)", - "specific_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + "specific_" . $escaper->escapeJs($container['name']) . "_<%- data.id %>" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString .= <<<script '</td>'+ @@ -111,26 +112,30 @@ script; '</tr>'+ '</tbody>'+ '</table>'+ - '<div class="no-display chooser_container" id="{$block->escapeJs($container['name'])}_ids_<%- data.id %>">'+ + '<div class="no-display chooser_container" id="{$escaper->escapeJs($container['name'])}_ids_<%- data.id %>">'+ '<input disabled="disabled" type="hidden" class="is_anchor_only" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][is_anchor_only]" '+ - 'value="{$block->escapeJs($container['is_anchor_only'])}" />'+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][is_anchor_only]" '+ + 'value="{$escaper->escapeJs($container['is_anchor_only'])}" />'+ '<input disabled="disabled" type="hidden" class="product_type_id" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][product_type_id]" '+ - 'value="{$block->escapeJs($container['product_type_id'])}" />'+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][product_type_id]" '+ + 'value="{$escaper->escapeJs($container['product_type_id'])}" />'+ '<p>' + '<input disabled="disabled" type="text" class="input-text entities" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][entities]" '+ - 'value="<%- data.{$block->escapeJs($container['name'])}_entities %>" readonly="readonly" /> ' + + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][entities]" '+ + 'value="<%- data.{$escaper->escapeJs($container['name'])}_entities %>" readonly="readonly" /> ' + '<a class="widget-option-chooser" href="#" '+ - 'title="{$block->escapeJs(__('Open Chooser'))}">' + - '<img src="{$block->escapeJs($block->getViewFileUrl('images/rule_chooser_trigger.gif'))}" '+ - 'alt="{$block->escapeJs(__('Open Chooser'))}" />' + + 'title="{$escaper->escapeJs(__('Open Chooser'))}">' + + '<img src="{$escaper->escapeJs( + $escaper->escapeUrl($block->getViewFileUrl('images/rule_chooser_trigger.gif')) + )}" '+ + 'alt="{$escaper->escapeJs(__('Open Chooser'))}" />' + '</a> ' + '<a id="widget-apply-<%- data.id %>" href="#" '+ - 'title="{$block->escapeJs(__('Apply'))}">' + - '<img src="{$block->escapeJs($block->getViewFileUrl('images/rule_component_apply.gif'))}" '+ - 'alt="{$block->escapeJs(__('Apply'))}" />' + + 'title="{$escaper->escapeJs(__('Apply'))}">' + + '<img src="{$escaper->escapeJs( + $escaper->escapeUrl($block->getViewFileUrl('images/rule_component_apply.gif')) + )}" '+ + 'alt="{$escaper->escapeJs(__('Apply'))}" />' + '</a>' + '</p>'+ '<div class="chooser"></div>'+ @@ -141,19 +146,19 @@ script; $scriptString1 = $secureRenderer->renderEventListenerAsTag( "onclick", "event.preventDefault(); - WidgetInstance.displayEntityChooser('" .$block->escapeJs($container['code']) . - "', '" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", - "div#" . $block->escapeJs($container['name']) . "_ids_<%- data.id %> a.widget-option-chooser" + WidgetInstance.displayEntityChooser('" .$escaper->escapeJs($container['code']) . + "', '" . $escaper->escapeJs($container['name']) . "_ids_<%- data.id %>')", + "div#" . $escaper->escapeJs($container['name']) . "_ids_<%- data.id %> a.widget-option-chooser" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString1 = $secureRenderer->renderEventListenerAsTag( 'onclick', "event.preventDefault(); - WidgetInstance.hideEntityChooser('" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", + WidgetInstance.hideEntityChooser('" . $escaper->escapeJs($container['name']) . "_ids_<%- data.id %>')", "a#widget-apply-<%- data.id %>" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString .= <<<script '</div>'+ @@ -175,8 +180,8 @@ $scriptString .= <<<script '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ - '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '<th> </th>'+ '</tr>'+ '</thead>'+ @@ -208,9 +213,9 @@ $scriptString .= <<<script '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label>{$block->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ - '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ - '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ + '<th><label>{$escaper->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ @@ -242,9 +247,9 @@ $scriptString .= <<<script '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label>{$block->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ - '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ - '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ + '<th><label>{$escaper->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ @@ -412,10 +417,10 @@ var WidgetInstance = { additional = {}; } if (type == 'categories') { - additional.url = '{$block->escapeJs($block->getCategoriesChooserUrl())}'; + additional.url = '{$escaper->escapeJs($escaper->escapeUrl($block->getCategoriesChooserUrl()))}'; additional.post_parameters = \$H({'is_anchor_only':$(chooser).down('input.is_anchor_only').value}); } else if (type == 'products') { - additional.url = '{$block->escapeUrl($block->getProductsChooserUrl())}'; + additional.url = '{$escaper->escapeJs($escaper->escapeUrl($block->getProductsChooserUrl()))}'; additional.post_parameters = \$H({'product_type_id':$(chooser).down('input.product_type_id').value}); } if (chooser && additional) { @@ -521,13 +526,13 @@ var WidgetInstance = { selected = ''; parameters = {}; if (type == 'block_reference') { - url = '{$block->escapeJs($block->getBlockChooserUrl())}'; + url = '{$escaper->escapeJs($escaper->escapeUrl($block->getBlockChooserUrl()))}'; if (additional.selectedBlock) { selected = additional.selectedBlock; } parameters.layout = value; } else if (type == 'block_template') { - url = '{$block->escapeJs($block->getTemplateChooserUrl())}'; + url = '{$escaper->escapeJs($escaper->escapeUrl($block->getTemplateChooserUrl()))}'; if (additional.selectedTemplate) { selected = additional.selectedTemplate; } diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php index 1cfa316c3cd01..3a4532d53624a 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php @@ -7,6 +7,7 @@ namespace Magento\Wishlist\Model\Wishlist\BuyRequest; +use Magento\Framework\Exception\LocalizedException; use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; /** @@ -32,15 +33,48 @@ public function execute(WishlistItem $wishlistItem, ?int $productId): array continue; } - [, $optionId, $optionValueId, $optionQuantity] = $optionData; + [$optionType, $optionId, $optionValueId, $optionQuantity] = $optionData; - $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; - $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + } + //for bundle options with custom quantity + foreach ($wishlistItem->getEnteredOptions() as $option) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValueId] = $optionData; + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $optionQuantity = $option->getValue(); + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } } return $bundleOptionsData; } + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 4) { + $errorMessage = __('Wrong format of the entered option data'); + throw new LocalizedException($errorMessage); + } + } + /** * Checks whether this provider is applicable for the current option * diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php index d143830064752..3599ad237da3a 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php @@ -7,6 +7,7 @@ namespace Magento\Wishlist\Model\Wishlist; +use Magento\Framework\Exception\LocalizedException; use Magento\Wishlist\Model\Item as WishlistItem; use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; @@ -63,7 +64,7 @@ public function __construct( public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput { foreach ($wishlistItemsIds as $wishlistItemId) { - $this->removeItemFromWishlist((int) $wishlistItemId); + $this->removeItemFromWishlist((int) $wishlistItemId, $wishlist); } return $this->prepareOutput($wishlist); @@ -73,12 +74,22 @@ public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOu * Remove product item from wishlist * * @param int $wishlistItemId + * @param Wishlist $wishlist * * @return void */ - private function removeItemFromWishlist(int $wishlistItemId): void + private function removeItemFromWishlist(int $wishlistItemId, Wishlist $wishlist): void { try { + if ($wishlist->getItem($wishlistItemId) == null) { + throw new LocalizedException( + __( + 'The wishlist item with ID "%id" does not belong to the wishlist', + ['id' => $wishlistItemId] + ) + ); + } + $wishlist->getItemCollection()->clear(); /** @var WishlistItem $wishlistItem */ $wishlistItem = $this->wishlistItemFactory->create(); $this->wishlistItemResource->load($wishlistItem, $wishlistItemId); @@ -90,6 +101,8 @@ private function removeItemFromWishlist(int $wishlistItemId): void } $this->wishlistItemResource->delete($wishlistItem); + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); } catch (\Exception $e) { $this->addError( __( diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php index 4abcada138362..ff3a8c135ce27 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -90,6 +90,15 @@ public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutpu private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void { try { + if ($wishlist->getItem($wishlistItemData->getId()) == null) { + throw new LocalizedException( + __( + 'The wishlist item with ID "%id" does not belong to the wishlist', + ['id' => $wishlistItemData->getId()] + ) + ); + } + $wishlist->getItemCollection()->clear(); $options = $this->buyRequestBuilder->build($wishlistItemData); /** @var WishlistItem $wishlistItem */ $wishlistItem = $this->wishlistItemFactory->create(); diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index 4b48bbe99ced2..f7be4a9edb13c 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -117,13 +117,12 @@ .product-image-photo { bottom: 0; display: block; - height: auto; left: 0; margin: auto; - max-width: 100%; position: absolute; right: 0; top: 0; + width: auto; } // diff --git a/app/design/frontend/Magento/luma/Magento_Customer/layout/customer_account.xml b/app/design/frontend/Magento/blank/Magento_Customer/layout/customer_account.xml similarity index 100% rename from app/design/frontend/Magento/luma/Magento_Customer/layout/customer_account.xml rename to app/design/frontend/Magento/blank/Magento_Customer/layout/customer_account.xml diff --git a/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less index 69ec01d71e104..bf77ab46712b2 100644 --- a/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less @@ -15,9 +15,21 @@ // _____________________________________________ & when (@media-common = true) { + .data.switch .counter { + .lib-css(color, @text__color__muted); + + &:before { + content: '('; + } + + &:after { + content: ')'; + } + } + .rating-summary { .lib-rating-summary(); - + .rating-result { margin-left: -5px; } @@ -359,3 +371,4 @@ } } } + diff --git a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less index 07317e1670a0b..ce1b009c24d42 100644 --- a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less @@ -65,7 +65,6 @@ // _____________________________________________ & when (@media-common = true) { - .swatch { &-attribute { &-label { @@ -155,7 +154,7 @@ padding: 4px 8px; &.selected { - .lib-css(background-color, @swatch-option-text__selected__background-color) !important; + .lib-css(background-color, @swatch-option-text__selected__background-color); } } @@ -201,6 +200,7 @@ top: 0; } } + &-disabled { border: 0; cursor: default; @@ -208,6 +208,7 @@ &:after { .lib-rotate(-30deg); + .lib-css(background, @swatch-option__disabled__background); content: ''; height: 2px; left: -4px; @@ -215,7 +216,6 @@ top: 10px; width: 42px; z-index: 995; - .lib-css(background, @swatch-option__disabled__background); } } @@ -226,6 +226,7 @@ &-tooltip { .lib-css(border, @swatch-option-tooltip__border); .lib-css(color, @swatch-option-tooltip__color); + .lib-css(background, @swatch-option-tooltip__background); display: none; max-height: 100%; min-height: 20px; @@ -234,7 +235,6 @@ position: absolute; text-align: center; z-index: 999; - .lib-css(background, @swatch-option-tooltip__background); &, &-layered { @@ -278,9 +278,9 @@ } &-layered { + .lib-css(background, @swatch-option-tooltip-layered__background); .lib-css(border, @swatch-option-tooltip-layered__border); .lib-css(color, @swatch-option-tooltip-layered__color); - .lib-css(background, @swatch-option-tooltip-layered__background); display: none; left: -47px; position: absolute; @@ -326,7 +326,6 @@ margin: 2px 0; padding: 2px; position: static; - z-index: 1; } &-visual-tooltip-layered { diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less index 8518b5bf76735..8f99550271967 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less @@ -3,6 +3,8 @@ // * See COPYING.txt for license details. // */ +@import 'module/_collapsible_navigation.less'; + // // Theme variables // _____________________________________________ diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/module/_collapsible_navigation.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/module/_collapsible_navigation.less similarity index 100% rename from app/design/frontend/Magento/luma/Magento_Theme/web/css/source/module/_collapsible_navigation.less rename to app/design/frontend/Magento/blank/Magento_Theme/web/css/source/module/_collapsible_navigation.less diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index e205b20efd17c..43694a8197963 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -98,13 +98,12 @@ .product-image-photo { bottom: 0; display: block; - height: auto; left: 0; margin: auto; - max-width: 100%; position: absolute; right: 0; top: 0; + width: auto; } // diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 5d9746317af55..2c8c52bdb7af2 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -267,7 +267,7 @@ .lib-icon-font-symbol( @_icon-font-content: @icon-trash ); - + &:hover { .lib-css(text-decoration, @link__text-decoration); } @@ -574,7 +574,7 @@ .widget { float: left; - + &.block { margin-bottom: @indent__base; } @@ -727,9 +727,14 @@ position: static; } } + &.discount { width: auto; } + + .actions-toolbar { + width: auto; + } } } diff --git a/app/design/frontend/Magento/luma/web/css/critical.css b/app/design/frontend/Magento/luma/web/css/critical.css index 922a1eefa89e8..c2b4b16f9aadd 100644 --- a/app/design/frontend/Magento/luma/web/css/critical.css +++ b/app/design/frontend/Magento/luma/web/css/critical.css @@ -1 +1 @@ -body{margin:0}.page-main{flex-grow:1}.product-image-wrapper{display:block;height:0;overflow:hidden;position:relative;z-index:1}.product-image-wrapper .product-image-photo{bottom:0;display:block;height:auto;left:0;margin:auto;max-width:100%;position:absolute;right:0;top:0}.product-image-container{display:inline-block}.modal-popup{position:fixed}.page-wrapper{display:flex;flex-direction:column;min-height:100vh}.action.skip:not(:focus),.block.newsletter .label,.minicart-wrapper .action.showcart .counter-label,.minicart-wrapper .action.showcart .text,.page-header .switcher .label,.product-item-actions .actions-secondary>.action span{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.alink,a{color:#006bb4;text-decoration:none}.page-header .panel.wrapper{background-color:#6e716e;color:#fff}.header.panel>.header.links{list-style:none none;float:right;font-size:0;margin-right:20px}.header.panel>.header.links>li{font-size:14px;margin:0 0 0 15px}.block-search .action.search,.block-search .block-title,.block-search .nested,.block.newsletter .title,.breadcrumbs .item,.nav-toggle,.no-display,.page-footer .switcher .options ul.dropdown,.page-header .switcher .options ul.dropdown{display:none}.block-search .label>span{height:1px;overflow:hidden;position:absolute}.logo{float:left;margin:0 0 10px 40px}.minicart-wrapper{float:right}.page-footer{margin-top:25px}.footer.content{border-top:1px solid #cecece;padding-top:20px}.block.newsletter .actions{display:table-cell;vertical-align:top;width:1%}.block-banners .banner-items,.block-banners-inline .banner-items,.block-event .slider-panel .slider,.footer.content ul,.product-items{margin:0;padding:0;list-style:none none}.copyright{background-color:#6e716e;color:#fff;box-sizing:border-box;display:block;padding:10px;text-align:center}.modal-popup,.modal-slide{visibility:hidden;opacity:0}input[type=email],input[type=number],input[type=password],input[type=search],input[type=text],input[type=url]{background:#fff;background-clip:padding-box;border:1px solid #c2c2c2;border-radius:1px;font-size:14px;height:32px;line-height:1.42857143;padding:0 9px;vertical-align:baseline;width:100%;box-sizing:border-box}.action.primary{background:#1979c3;border:1px solid #1979c3;color:#fff;font-weight:600;padding:7px 15px}.block.newsletter .form.subscribe{display:table}.footer.content .links a{color:#575757}.load.indicator{background-color:rgba(255,255,255,.7);z-index:9999;bottom:0;left:0;position:fixed;right:0;top:0;position:absolute}.load.indicator:before{background:transparent url(../images/loader-2.gif) no-repeat 50% 50%;border-radius:5px;height:160px;width:160px;bottom:0;box-sizing:border-box;content:'';left:0;margin:auto;position:absolute;right:0;top:0}.load.indicator>span{display:none}.loading-mask{bottom:0;left:0;margin:auto;position:fixed;right:0;top:0;z-index:100;background:rgba(255,255,255,.5)}.loading-mask .loader>img{bottom:0;left:0;margin:auto;position:fixed;right:0;top:0;z-index:100}.loading-mask .loader>p{display:none}body>.loading-mask{z-index:9999}._block-content-loading{position:relative}@media (min-width:768px),print{body,html{height:100%}.page-header{border:0;margin-bottom:0}.nav-sections-item-title,.section-item-content .switcher-currency,ul.header.links li.customer-welcome,ul.level0.submenu{display:none}.abs-add-clearfix-desktop:after,.abs-add-clearfix-desktop:before,.account .column.main .block.block-order-details-view:after,.account .column.main .block.block-order-details-view:before,.account .column.main .block:not(.widget) .block-content:after,.account .column.main .block:not(.widget) .block-content:before,.account .page-title-wrapper:after,.account .page-title-wrapper:before,.block-addresses-list .items.addresses:after,.block-addresses-list .items.addresses:before,.block-cart-failed .block-content:after,.block-cart-failed .block-content:before,.block-giftregistry-shared .item-options:after,.block-giftregistry-shared .item-options:before,.block-wishlist-management:after,.block-wishlist-management:before,.cart-container:after,.cart-container:before,.data.table .gift-wrapping .content:after,.data.table .gift-wrapping .content:before,.data.table .gift-wrapping .nested:after,.data.table .gift-wrapping .nested:before,.header.content:after,.header.content:before,.login-container:after,.login-container:before,.magento-rma-guest-returns .column.main .block.block-order-details-view:after,.magento-rma-guest-returns .column.main .block.block-order-details-view:before,.order-links:after,.order-links:before,.order-review-form:after,.order-review-form:before,.page-header .header.panel:after,.page-header .header.panel:before,.paypal-review .block-content:after,.paypal-review .block-content:before,.paypal-review-discount:after,.paypal-review-discount:before,.sales-guest-view .column.main .block.block-order-details-view:after,.sales-guest-view .column.main .block.block-order-details-view:before,[class^=sales-guest-] .column.main .block.block-order-details-view:after,[class^=sales-guest-] .column.main .block.block-order-details-view:before{content:'';display:table}.abs-add-clearfix-desktop:after,.account .column.main .block.block-order-details-view:after,.account .column.main .block:not(.widget) .block-content:after,.account .page-title-wrapper:after,.block-addresses-list .items.addresses:after,.block-cart-failed .block-content:after,.block-giftregistry-shared .item-options:after,.block-wishlist-management:after,.cart-container:after,.data.table .gift-wrapping .content:after,.data.table .gift-wrapping .nested:after,.header.content:after,.login-container:after,.magento-rma-guest-returns .column.main .block.block-order-details-view:after,.order-links:after,.order-review-form:after,.page-header .header.panel:after,.paypal-review .block-content:after,.paypal-review-discount:after,.sales-guest-view .column.main .block.block-order-details-view:after,[class^=sales-guest-] .column.main .block.block-order-details-view:after{clear:both}.block.category.event,.breadcrumbs,.footer.content,.header.content,.navigation,.page-header .header.panel,.page-main,.page-wrapper>.page-bottom,.page-wrapper>.widget,.top-container{box-sizing:border-box;margin-left:auto;margin-right:auto;max-width:1280px;padding-left:20px;padding-right:20px;width:auto}.panel.header{padding:10px 20px}.page-header .switcher{float:right;margin-left:15px;margin-right:-6px}.header.panel>.header.links>li>a{color:#fff}.header.content{padding:30px 20px 0}.logo{margin:-8px auto 25px 0}.minicart-wrapper{margin-left:13px}.compare.wrapper{list-style:none none}.nav-sections{margin-bottom:25px}.nav-sections-item-content>.navigation{display:block}.navigation{background:#f0f0f0;font-weight:700;height:inherit;left:auto;overflow:inherit;padding:0;position:relative;top:0;width:100%;z-index:3}.navigation ul{margin-top:0;margin-bottom:0;padding:0 8px;position:relative}.navigation .level0{margin:0 10px 0 0;display:inline-block}.navigation .level0>.level-top{color:#575757;line-height:47px;padding:0 12px}.page-main{width:100%}.page-footer{background:#f4f4f4;padding-bottom:25px}.footer.content .links{display:inline-block;padding-right:50px;vertical-align:top}.footer.content ul{padding-right:50px}.footer.content .links li{border:none;font-size:14px;margin:0 0 8px;padding:0}.footer.content .block{float:right}.block.newsletter{width:34%}}@media only screen and (max-width:767px){.compare.wrapper,.panel.wrapper,[class*=block-compare]{display:none}.footer.content .links>li{background:#f4f4f4;font-size:1.6rem;border-top:1px solid #cecece;margin:0 -15px;padding:0 15px}.page-header .header.panel,.page-main{padding-left:15px;padding-right:15px}.header.content{padding-top:10px}.nav-sections-items:after,.nav-sections-items:before{content:'';display:table}.nav-sections-items:after{clear:both}.nav-sections{width:100vw;position:fixed;left:-100vw}} +body{margin:0}.page-main{flex-grow:1}.product-image-wrapper{display:block;height:0;overflow:hidden;position:relative;z-index:1}.product-image-wrapper .product-image-photo{bottom:0;display:block;height:auto;left:0;margin:auto;max-width:100%;position:absolute;right:0;top:0;width:auto}.product-image-container{display:inline-block}.modal-popup{position:fixed}.page-wrapper{display:flex;flex-direction:column;min-height:100vh}.action.skip:not(:focus),.block.newsletter .label,.minicart-wrapper .action.showcart .counter-label,.minicart-wrapper .action.showcart .text,.page-header .switcher .label,.product-item-actions .actions-secondary>.action span{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.alink,a{color:#006bb4;text-decoration:none}.page-header .panel.wrapper{background-color:#6e716e;color:#fff}.header.panel>.header.links{list-style:none none;float:right;font-size:0;margin-right:20px}.header.panel>.header.links>li{font-size:14px;margin:0 0 0 15px}.block-search .action.search,.block-search .block-title,.block-search .nested,.block.newsletter .title,.breadcrumbs .item,.nav-toggle,.no-display,.page-footer .switcher .options ul.dropdown,.page-header .switcher .options ul.dropdown{display:none}.block-search .label>span{height:1px;overflow:hidden;position:absolute}.logo{float:left;margin:0 0 10px 40px}.minicart-wrapper{float:right}.page-footer{margin-top:25px}.footer.content{border-top:1px solid #cecece;padding-top:20px}.block.newsletter .actions{display:table-cell;vertical-align:top;width:1%}.block-banners .banner-items,.block-banners-inline .banner-items,.block-event .slider-panel .slider,.footer.content ul,.product-items{margin:0;padding:0;list-style:none none}.copyright{background-color:#6e716e;color:#fff;box-sizing:border-box;display:block;padding:10px;text-align:center}.modal-popup,.modal-slide{visibility:hidden;opacity:0}input[type=email],input[type=number],input[type=password],input[type=search],input[type=text],input[type=url]{background:#fff;background-clip:padding-box;border:1px solid #c2c2c2;border-radius:1px;font-size:14px;height:32px;line-height:1.42857143;padding:0 9px;vertical-align:baseline;width:100%;box-sizing:border-box}.action.primary{background:#1979c3;border:1px solid #1979c3;color:#fff;font-weight:600;padding:7px 15px}.block.newsletter .form.subscribe{display:table}.footer.content .links a{color:#575757}.load.indicator{background-color:rgba(255,255,255,.7);z-index:9999;bottom:0;left:0;position:fixed;right:0;top:0;position:absolute}.load.indicator:before{background:transparent url(../images/loader-2.gif) no-repeat 50% 50%;border-radius:5px;height:160px;width:160px;bottom:0;box-sizing:border-box;content:'';left:0;margin:auto;position:absolute;right:0;top:0}.load.indicator>span{display:none}.loading-mask{bottom:0;left:0;margin:auto;position:fixed;right:0;top:0;z-index:100;background:rgba(255,255,255,.5)}.loading-mask .loader>img{bottom:0;left:0;margin:auto;position:fixed;right:0;top:0;z-index:100}.loading-mask .loader>p{display:none}body>.loading-mask{z-index:9999}._block-content-loading{position:relative}@media (min-width:768px),print{body,html{height:100%}.page-header{border:0;margin-bottom:0}.nav-sections-item-title,.section-item-content .switcher-currency,ul.header.links li.customer-welcome,ul.level0.submenu{display:none}.abs-add-clearfix-desktop:after,.abs-add-clearfix-desktop:before,.account .column.main .block.block-order-details-view:after,.account .column.main .block.block-order-details-view:before,.account .column.main .block:not(.widget) .block-content:after,.account .column.main .block:not(.widget) .block-content:before,.account .page-title-wrapper:after,.account .page-title-wrapper:before,.block-addresses-list .items.addresses:after,.block-addresses-list .items.addresses:before,.block-cart-failed .block-content:after,.block-cart-failed .block-content:before,.block-giftregistry-shared .item-options:after,.block-giftregistry-shared .item-options:before,.block-wishlist-management:after,.block-wishlist-management:before,.cart-container:after,.cart-container:before,.data.table .gift-wrapping .content:after,.data.table .gift-wrapping .content:before,.data.table .gift-wrapping .nested:after,.data.table .gift-wrapping .nested:before,.header.content:after,.header.content:before,.login-container:after,.login-container:before,.magento-rma-guest-returns .column.main .block.block-order-details-view:after,.magento-rma-guest-returns .column.main .block.block-order-details-view:before,.order-links:after,.order-links:before,.order-review-form:after,.order-review-form:before,.page-header .header.panel:after,.page-header .header.panel:before,.paypal-review .block-content:after,.paypal-review .block-content:before,.paypal-review-discount:after,.paypal-review-discount:before,.sales-guest-view .column.main .block.block-order-details-view:after,.sales-guest-view .column.main .block.block-order-details-view:before,[class^=sales-guest-] .column.main .block.block-order-details-view:after,[class^=sales-guest-] .column.main .block.block-order-details-view:before{content:'';display:table}.abs-add-clearfix-desktop:after,.account .column.main .block.block-order-details-view:after,.account .column.main .block:not(.widget) .block-content:after,.account .page-title-wrapper:after,.block-addresses-list .items.addresses:after,.block-cart-failed .block-content:after,.block-giftregistry-shared .item-options:after,.block-wishlist-management:after,.cart-container:after,.data.table .gift-wrapping .content:after,.data.table .gift-wrapping .nested:after,.header.content:after,.login-container:after,.magento-rma-guest-returns .column.main .block.block-order-details-view:after,.order-links:after,.order-review-form:after,.page-header .header.panel:after,.paypal-review .block-content:after,.paypal-review-discount:after,.sales-guest-view .column.main .block.block-order-details-view:after,[class^=sales-guest-] .column.main .block.block-order-details-view:after{clear:both}.block.category.event,.breadcrumbs,.footer.content,.header.content,.navigation,.page-header .header.panel,.page-main,.page-wrapper>.page-bottom,.page-wrapper>.widget,.top-container{box-sizing:border-box;margin-left:auto;margin-right:auto;max-width:1280px;padding-left:20px;padding-right:20px;width:auto}.panel.header{padding:10px 20px}.page-header .switcher{float:right;margin-left:15px;margin-right:-6px}.header.panel>.header.links>li>a{color:#fff}.header.content{padding:30px 20px 0}.logo{margin:-8px auto 25px 0}.minicart-wrapper{margin-left:13px}.compare.wrapper{list-style:none none}.nav-sections{margin-bottom:25px}.nav-sections-item-content>.navigation{display:block}.navigation{background:#f0f0f0;font-weight:700;height:inherit;left:auto;overflow:inherit;padding:0;position:relative;top:0;width:100%;z-index:3}.navigation ul{margin-top:0;margin-bottom:0;padding:0 8px;position:relative}.navigation .level0{margin:0 10px 0 0;display:inline-block}.navigation .level0>.level-top{color:#575757;line-height:47px;padding:0 12px}.page-main{width:100%}.page-footer{background:#f4f4f4;padding-bottom:25px}.footer.content .links{display:inline-block;padding-right:50px;vertical-align:top}.footer.content ul{padding-right:50px}.footer.content .links li{border:none;font-size:14px;margin:0 0 8px;padding:0}.footer.content .block{float:right}.block.newsletter{width:34%}}@media only screen and (max-width:767px){.compare.wrapper,.panel.wrapper,[class*=block-compare]{display:none}.footer.content .links>li{background:#f4f4f4;font-size:1.6rem;border-top:1px solid #cecece;margin:0 -15px;padding:0 15px}.page-header .header.panel,.page-main{padding-left:15px;padding-right:15px}.header.content{padding-top:10px}.nav-sections-items:after,.nav-sections-items:before{content:'';display:table}.nav-sections-items:after{clear:both}.nav-sections{width:100vw;position:fixed;left:-100vw}} diff --git a/composer.lock b/composer.lock index 8a5d82536cee4..59920b387a331 100644 --- a/composer.lock +++ b/composer.lock @@ -8120,6 +8120,12 @@ "sftp", "storage" ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -8230,16 +8236,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8a106ea029f222f4354854636861273c7577bee9" + "reference": "c6760313811f2c04545a261c706d2a73dd727b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", - "reference": "8a106ea029f222f4354854636861273c7577bee9", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/c6760313811f2c04545a261c706d2a73dd727b9a", + "reference": "c6760313811f2c04545a261c706d2a73dd727b9a", "shasum": "" }, "require": { @@ -8317,7 +8323,7 @@ "magento", "testing" ], - "time": "2020-08-19T19:57:27+00:00" + "time": "2020-09-28T18:26:59+00:00" }, { "name": "mikey179/vfsstream", diff --git a/dev/tests/acceptance/staticRuleset.json b/dev/tests/acceptance/staticRuleset.json index 82cc9dfe74152..91521d5f16bcc 100644 --- a/dev/tests/acceptance/staticRuleset.json +++ b/dev/tests/acceptance/staticRuleset.json @@ -1,8 +1,8 @@ { - "tests": [ - "actionGroupArguments", - "deprecatedEntityUsage", - "annotations", - "pauseActionUsage" - ] + "tests": [ + "actionGroupArguments", + "deprecatedEntityUsage", + "annotations", + "pauseActionUsage" + ] } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php index 461ab6c989104..fd0519ab2b34e 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -249,6 +249,82 @@ public function testUpdateWithDefaultSortByAttribute() $this->createdCategories = [$categoryId]; } + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testUpdateUrlKey() + { + $this->_markTestAsRestOnly('Functionality available in REST mode only.'); + + $categoryId = 333; + $categoryData = [ + 'name' => 'Update Category Test Old Name', + 'custom_attributes' => [ + [ + 'attribute_code' => 'url_key', + 'value' => "Update Category Test Old Name", + ], + ], + ]; + $result = $this->updateCategory($categoryId, $categoryData); + $this->assertEquals($categoryId, $result['id']); + + $categoryData = [ + 'name' => 'Update Category Test New Name', + 'custom_attributes' => [ + [ + 'attribute_code' => 'url_key', + 'value' => "Update Category Test New Name", + ], + [ + 'attribute_code' => 'save_rewrites_history', + 'value' => 1, + ], + ], + ]; + $result = $this->updateCategory($categoryId, $categoryData); + $this->assertEquals($categoryId, $result['id']); + /** @var \Magento\Catalog\Model\Category $model */ + $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + $category = $model->load($categoryId); + $this->assertEquals("Update Category Test New Name", $category->getName()); + + // check for the url rewrite for the new name + $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $data = [ + UrlRewrite::ENTITY_ID => $categoryId, + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::REDIRECT_TYPE => 0, + ]; + + $urlRewrite = $storage->findOneByData($data); + + // Assert that a url rewrite is auto-generated for the category created from the data fixture + $this->assertNotNull($urlRewrite); + $this->assertEquals(1, $urlRewrite->getIsAutogenerated()); + $this->assertEquals($categoryId, $urlRewrite->getEntityId()); + $this->assertEquals(CategoryUrlRewriteGenerator::ENTITY_TYPE, $urlRewrite->getEntityType()); + $this->assertEquals('update-category-test-new-name.html', $urlRewrite->getRequestPath()); + + // check for the forward from the old name to the new name + $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $data = [ + UrlRewrite::ENTITY_ID => $categoryId, + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::REDIRECT_TYPE => 301, + ]; + + $urlRewrite = $storage->findOneByData($data); + + $this->assertNotNull($urlRewrite); + $this->assertEquals(0, $urlRewrite->getIsAutogenerated()); + $this->assertEquals($categoryId, $urlRewrite->getEntityId()); + $this->assertEquals(CategoryUrlRewriteGenerator::ENTITY_TYPE, $urlRewrite->getEntityType()); + $this->assertEquals('update-category-test-old-name.html', $urlRewrite->getRequestPath()); + + $this->deleteCategory($categoryId); + } + protected function getSimpleCategoryData($categoryData = []) { return [ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index c2e82e734cd9b..641253cc34c2c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -11,9 +11,11 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -564,10 +566,12 @@ public function testCategoryImage(?string $imagePrefix) ->addAttributeToFilter('name', ['eq' => 'Parent Image Category']) ->getFirstItem(); $categoryId = $categoryModel->getId(); + /** @var ResourceConnection $resourceConnection */ + $resourceConnection = Bootstrap::getObjectManager()->create(ResourceConnection::class); + $connection = $resourceConnection->getConnection(); if ($imagePrefix !== null) { // update image to account for different stored image formats - $connection = $categoryCollection->getConnection(); $productLinkField = $this->metadataPool ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) ->getLinkField(); @@ -577,20 +581,20 @@ public function testCategoryImage(?string $imagePrefix) $imageAttributeValue = $imagePrefix . basename($categoryModel->getImage()); if (!empty($imageAttributeValue)) { - $query = sprintf( + $sqlQuery = sprintf( 'UPDATE %s SET `value` = "%s" ' . 'WHERE `%s` = %d ' . 'AND `store_id`= %d ' . 'AND `attribute_id` = ' . '(SELECT `ea`.`attribute_id` FROM %s ea WHERE `ea`.`attribute_code` = "image" LIMIT 1)', - $connection->getTableName('catalog_category_entity_varchar'), + $resourceConnection->getTableName('catalog_category_entity_varchar'), $imageAttributeValue, $productLinkField, $categoryModel->getData($productLinkField), $defaultStoreId, - $connection->getTableName('eav_attribute') + $resourceConnection->getTableName('eav_attribute') ); - $connection->query($query); + $connection->query($sqlQuery); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 463b07c7261a6..f086a2211b51d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -12,16 +12,20 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; /** * Test loading of category tree + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryTest extends GraphQlAbstract { @@ -47,7 +51,7 @@ class CategoryTest extends GraphQlAbstract protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->categoryRepository = $this->objectManager->get(CategoryRepository::class); $this->store = $this->objectManager->get(Store::class); $this->metadataPool = $this->objectManager->get(MetadataPool::class); @@ -587,9 +591,12 @@ public function testCategoryImage(?string $imagePrefix) ->getFirstItem(); $categoryId = $categoryModel->getId(); + /** @var ResourceConnection $resourceConnection */ + $resourceConnection = Bootstrap::getObjectManager()->create(ResourceConnection::class); + $connection = $resourceConnection->getConnection(); + if ($imagePrefix !== null) { - // update image to account for different stored image formats - $connection = $categoryCollection->getConnection(); + // update image to account for different stored image format $productLinkField = $this->metadataPool ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) ->getLinkField(); @@ -599,20 +606,20 @@ public function testCategoryImage(?string $imagePrefix) $imageAttributeValue = $imagePrefix . basename($categoryModel->getImage()); if (!empty($imageAttributeValue)) { - $query = sprintf( + $sqlQuery = sprintf( 'UPDATE %s SET `value` = "%s" ' . 'WHERE `%s` = %d ' . 'AND `store_id`= %d ' . 'AND `attribute_id` = ' . '(SELECT `ea`.`attribute_id` FROM %s ea WHERE `ea`.`attribute_code` = "image" LIMIT 1)', - $connection->getTableName('catalog_category_entity_varchar'), + $resourceConnection->getTableName('catalog_category_entity_varchar'), $imageAttributeValue, $productLinkField, $categoryModel->getData($productLinkField), $defaultStoreId, - $connection->getTableName('eav_attribute') + $resourceConnection->getTableName('eav_attribute') ); - $connection->query($query); + $connection->query($sqlQuery); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php new file mode 100644 index 0000000000000..8cb7cec1b9a12 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Exception; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductAttributeStoreTest extends GraphQlAbstract +{ + /** + * Test that custom attribute labels are returned respecting store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php + * @throws LocalizedException + */ + public function testAttributeStoreLabels(): void + { + $this->attributeLabelTest('Test Configurable Default Store'); + $this->attributeLabelTest('Test Configurable Test Store', ['Store' => 'test']); + } + + /** + * @param $expectedLabel + * @param array $headers + * @throws LocalizedException + * @throws Exception + */ + private function attributeLabelTest($expectedLabel, array $headers = []): void + { + $query = <<<QUERY +{ + products(search:"Simple", + pageSize: 3 + currentPage: 1 + ) + { + aggregations + { + attribute_code + label + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertNotEmpty($response['products']['aggregations']); + $attributes = $response['products']['aggregations']; + foreach ($attributes as $attribute) { + if ($attribute['attribute_code'] === 'test_configurable') { + $this->assertEquals($expectedLabel, $attribute['label']); + } + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php index e059960074fbf..ce74a432dcba3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php @@ -45,18 +45,7 @@ public function testCMSPageUrlResolver() $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); $expectedEntityType = CmsPageUrlRewriteGenerator::ENTITY_TYPE; - $query - = <<<QUERY -{ - urlResolver(url:"{$requestPath}") - { - id - relative_url - type - redirectCode - } -} -QUERY; + $query = $this->createQuery($requestPath); $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); @@ -64,18 +53,7 @@ public function testCMSPageUrlResolver() $this->assertEquals(0, $response['urlResolver']['redirectCode']); // querying by non seo friendly url path should return seo friendly relative url - $query - = <<<QUERY -{ - urlResolver(url:"{$targetPath}") - { - id - relative_url - type - redirectCode - } -} -QUERY; + $query = $this->createQuery($targetPath); $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); @@ -83,6 +61,24 @@ public function testCMSPageUrlResolver() $this->assertEquals(0, $response['urlResolver']['redirectCode']); } + /** + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testResolveCMSPageWithQueryParameters() + { + $page = $this->objectManager->create(\Magento\Cms\Model\Page::class); + $page->load('page100'); + $cmsPageId = $page->getId(); + $requestPath = $page->getIdentifier(); + $requestPath .= '?key=value'; + + $query = $this->createQuery($requestPath); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['urlResolver']); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + } + /** * Test resolution of '/' path to home page */ @@ -98,10 +94,24 @@ public function testResolveSlash() $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); $page->load($homePageIdentifier); $homePageId = $page->getId(); - $query - = <<<QUERY + $query = $this->createQuery('/'); + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($homePageId, $response['urlResolver']['id']); + $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); + $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * @param string $path + * @return string + */ + private function createQuery(string $path): string + { + return <<<QUERY { - urlResolver(url:"/") + urlResolver(url:"{$path}") { id relative_url @@ -110,11 +120,5 @@ public function testResolveSlash() } } QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); - $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); - $this->assertEquals(0, $response['urlResolver']['redirectCode']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php index 9c89b80433afd..b5649cbf3bd64 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php @@ -156,7 +156,7 @@ public function testResetPasswordTokenEmptyValue() public function testResetPasswordTokenMismatched() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Cannot set the customer\'s password'); + $this->expectExceptionMessage('The password token is mismatched. Reset and try again'); $query = <<<QUERY mutation { resetPassword ( @@ -192,6 +192,55 @@ public function testNewPasswordEmptyValue() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @throws NoSuchEntityException + * @throws Exception + * @throws LocalizedException + */ + public function testNewPasswordCheckMinLength() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The password needs at least 8 characters. Create a new password and try again'); + $query = <<<QUERY +mutation { + resetPassword ( + email: "{$this->getCustomerEmail()}" + resetPasswordToken: "{$this->getResetPasswordToken()}" + newPassword: "new_" + ) +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @throws NoSuchEntityException + * @throws Exception + * @throws LocalizedException + */ + public function testNewPasswordCheckCharactersStrength() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'Minimum of different classes of characters in password is 3. ' . + 'Classes of characters: Lower Case, Upper Case, Digits, Special Characters.' + ); + $query = <<<QUERY +mutation { + resetPassword ( + email: "{$this->getCustomerEmail()}" + resetPasswordToken: "{$this->getResetPasswordToken()}" + newPassword: "new_password" + ) +} +QUERY; + $this->graphQlMutation($query); + } + /** * Check password reset for lock customer * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php index b97cd379e4384..04518fad47052 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -98,6 +98,46 @@ public function testAddBundleProductWithOptions(): void $this->assertEquals(Select::NAME, $bundleOptions[0]['type']); } + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @throws Exception + */ + public function testAddingBundleItemWithCustomOptionQuantity() + { + $response = $this->graphQlQuery($this->getProductQuery("bundle-product")); + $bundleItem = $response['products']['items'][0]; + $sku = $bundleItem['sku']; + $bundleOptions = $bundleItem['items']; + $customerId = 1; + $uId0 = $bundleOptions[0]['options'][0]['uid']; + $uId1 = $bundleOptions[1]['options'][0]['uid']; + $query= $this->getQueryWithCustomOptionQuantity($sku, 5, $uId0, $uId1); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $item */ + $item = $wishlist->getItemCollection()->getFirstItem(); + + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $response = $response['addProductsToWishlist']['wishlist']; + $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); + $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); + $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); + $this->assertEquals($item->getData('qty'), $response['items_v2'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); + $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); + $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals('Option 1', $bundleOptions[0]['label']); + $bundleOptionOneValues = $bundleOptions[0]['values']; + $this->assertEquals(7, $bundleOptionOneValues[0]['quantity']); + $this->assertEquals('Option 2', $bundleOptions[1]['label']); + $bundleOptionTwoValues = $bundleOptions[1]['values']; + $this->assertEquals(1, $bundleOptionTwoValues[0]['quantity']); + } + /** * Authentication header map * @@ -179,6 +219,118 @@ private function getQuery( MUTATION; } + /** + * Query with custom option quantity + * + * @param string $sku + * @param int $qty + * @param string $uId0 + * @param string $uId1 + * @param int $wishlistId + * @return string + */ + private function getQueryWithCustomOptionQuantity( + string $sku, + int $qty, + string $uId0, + string $uId1, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + entered_options: [ + { + uid:"{$uId0}", + value:"7" + }, + { + uid:"{$uId1}", + value:"7" + } + ] + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items_v2 { + id + description + quantity + added_at + ... on BundleWishlistItem { + bundle_options { + id + label + type + values { + id + label + quantity + price + } + } + } + } + } + } +} +MUTATION; + } + + /** + * Returns GraphQL query for retrieving a product with customizable options + * + * @param string $sku + * @return string + */ + private function getProductQuery(string $sku): string + { + return <<<QUERY +{ + products(search: "{$sku}") { + items { + sku + ... on BundleProduct { + items { + sku + option_id + required + type + title + options { + uid + label + product { + sku + } + can_change_quantity + id + price + + quantity + } + } + } + } + } +} +QUERY; + } + /** * @param int $optionId * @param int $selectionId diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index 13aaecbc7b733..dd7a54cff32a0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -55,6 +55,34 @@ public function testDeleteWishlistItemFromWishlist(): void $this->assertEmpty($wishlistResponse['items_v2']); } + /** + * Test deleting the wishlist item of another customer + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + */ + public function testUnauthorizedWishlistItemDelete() + { + $wishlist = $this->getWishlist(); + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $wishlist2 = $this->getWishlist('customer_two@example.com'); + $wishlist2Id = $wishlist2['customer']['wishlist']['id']; + $query = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id']); + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getHeaderMap('customer_two@example.com') + ); + self::assertEquals(1, $response['removeProductsFromWishlist']['wishlist']['items_count']); + self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items_v2']); + self::assertEquals( + 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', + $response['removeProductsFromWishlist']['user_errors'][0]['message'] + ); + } + /** * Authentication header map * @@ -116,9 +144,9 @@ private function getQuery( * * @throws Exception */ - public function getWishlist(): array + public function getWishlist(string $username = 'customer@example.com'): array { - return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap($username)); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 08273e7936640..bc00be18eed19 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -57,6 +57,37 @@ public function testUpdateSimpleProductFromWishlist(): void $this->assertEquals($description, $wishlistResponse['items_v2'][0]['description']); } + /** + * Test updating the wishlist item of another customer + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + */ + public function testUnauthorizedWishlistItemUpdate() + { + $wishlist = $this->getWishlist(); + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $wishlist2 = $this->getWishlist('customer_two@example.com'); + $wishlist2Id = $wishlist2['customer']['wishlist']['id']; + $qty = 2; + $description = 'New Description'; + $updateWishlistQuery = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id'], $qty, $description); + $response = $this->graphQlMutation( + $updateWishlistQuery, + [], + '', + $this->getHeaderMap('customer_two@example.com') + ); + self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']); + self::assertEquals( + 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', + $response['updateProductsInWishlist']['user_errors'][0]['message'] + ); + } + /** * Authentication header map * @@ -124,13 +155,14 @@ private function getQuery( /** * Get wishlist result * + * @param string $username * @return array * * @throws Exception */ - public function getWishlist(): array + public function getWishlist(string $username = 'customer@example.com'): array { - return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap($username)); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php index 2d8c308389452..64fc612120332 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php @@ -62,6 +62,7 @@ public function testConfigurableShipOrder() $shipmentId = (int)$this->_webApiCall($this->getServiceInfo($existingOrder), $requestData); $this->assertNotEmpty($shipmentId); + $shipment = null; try { $shipment = $this->shipmentRepository->get($shipmentId); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { @@ -89,6 +90,42 @@ public function testConfigurableShipOrder() ); } + /** + * Tests that order doesn't change a status from custom to the default after shipment creation. + * + * @magentoApiDataFixture Magento/Sales/_files/order_status.php + */ + public function testShipOrderStatusPreserve() + { + $incrementId = '100000001'; + $orderStatus = 'example'; + + /** @var Order $existingOrder */ + $order = $this->getOrder($incrementId); + $this->assertEquals($orderStatus, $order->getStatus()); + + $requestData = [ + 'orderId' => $order->getId() + ]; + /** @var OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + $requestData['items'][] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered(), + ]; + } + + $shipmentId = $this->_webApiCall($this->getServiceInfo($order), $requestData); + $this->assertNotEmpty($shipmentId); + $actualOrder = $this->getOrder($order->getIncrementId()); + + $this->assertEquals( + $order->getStatus(), + $actualOrder->getStatus(), + 'Failed asserting that Order status wasn\'t changed' + ); + } + /** * @magentoApiDataFixture Magento/Sales/_files/order_new.php */ @@ -214,6 +251,7 @@ public function testPartialShipOrderWithBundleShippedSeparately() $shipmentId = $this->_webApiCall($this->getServiceInfo($existingOrder), $requestData); $this->assertNotEmpty($shipmentId); + $shipment = null; try { $shipment = $this->shipmentRepository->get($shipmentId); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { @@ -268,6 +306,7 @@ public function testPartialShipOrderWithTwoBundleShippedSeparatelyContainsSameSi $shipmentId = $this->_webApiCall($this->getServiceInfo($order), $requestData); $this->assertNotEmpty($shipmentId); + $shipment = null; try { $shipment = $this->shipmentRepository->get($shipmentId); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { diff --git a/dev/tests/api-functional/testsuite/Magento/WebApiTest.php b/dev/tests/api-functional/testsuite/Magento/WebApiTest.php index 32670dfeb7b1b..cc07d7b761f2b 100644 --- a/dev/tests/api-functional/testsuite/Magento/WebApiTest.php +++ b/dev/tests/api-functional/testsuite/Magento/WebApiTest.php @@ -11,9 +11,16 @@ use Magento\TestFramework\Workaround\Override\Config; use Magento\TestFramework\Workaround\Override\WrapperGenerator; use PHPUnit\Framework\TestSuite; +use PHPUnit\TextUI\Configuration\Configuration as LegacyConfiguration; use PHPUnit\TextUI\Configuration\Registry; -use PHPUnit\TextUI\Configuration\TestSuiteCollection; -use PHPUnit\TextUI\Configuration\TestSuiteMapper; +use PHPUnit\TextUI\Configuration\TestSuite as LegacyTestSuiteConfiguration; +use PHPUnit\TextUI\Configuration\TestSuiteCollection as LegacyTestSuiteCollection; +use PHPUnit\TextUI\Configuration\TestSuiteMapper as LegacyTestSuiteMapper; +use PHPUnit\TextUI\XmlConfiguration\Configuration; +use PHPUnit\TextUI\XmlConfiguration\Loader; +use PHPUnit\TextUI\XmlConfiguration\TestSuite as TestSuiteConfiguration; +use PHPUnit\TextUI\XmlConfiguration\TestSuiteCollection; +use PHPUnit\TextUI\XmlConfiguration\TestSuiteMapper; /** * Web API tests wrapper. @@ -24,17 +31,17 @@ class WebApiTest extends TestSuite * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param string $className * @return TestSuite + * @throws \ReflectionException */ public static function suite($className) { $generator = new WrapperGenerator(); $overrideConfig = Config::getInstance(); - $configuration = Registry::getInstance()->get(self::getConfigurationFile()); + $configuration = self::getConfiguration(); $suitesConfig = $configuration->testSuite(); $suite = new TestSuite(); - /** @var \PHPUnit\TextUI\Configuration\TestSuite $suiteConfig */ foreach ($suitesConfig as $suiteConfig) { - $suites = (new TestSuiteMapper())->map(TestSuiteCollection::fromArray([$suiteConfig]), ''); + $suites = self::getSuites($suiteConfig); /** @var TestSuite $testSuite */ foreach ($suites as $testSuite) { /** @var TestSuite $test */ @@ -68,4 +75,39 @@ private static function getConfigurationFile(): string return $shortConfig ? $shortConfig : $longConfig; } + + /** + * Retrieve configuration depends on used phpunit version + * + * @return Configuration|LegacyConfiguration + */ + private static function getConfiguration() + { + // Compatibility with phpunit < 9.3 + if (!class_exists(Configuration::class)) { + // @phpstan-ignore-next-line + return Registry::getInstance()->get(self::getConfigurationFile()); + } + + // @phpstan-ignore-next-line + return (new Loader())->load(self::getConfigurationFile()); + } + + /** + * Retrieve test suites by suite config depends on used phpunit version + * + * @param TestSuiteConfiguration|LegacyTestSuiteConfiguration $suiteConfig + * @return TestSuite + */ + private static function getSuites($suiteConfig) + { + // Compatibility with phpunit < 9.3 + if (!class_exists(Configuration::class)) { + // @phpstan-ignore-next-line + return (new LegacyTestSuiteMapper())->map(LegacyTestSuiteCollection::fromArray([$suiteConfig]), ''); + } + + // @phpstan-ignore-next-line + return (new TestSuiteMapper())->map(TestSuiteCollection::fromArray([$suiteConfig]), ''); + } } diff --git a/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml b/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml index 42c21d544d01e..adf5af6f037ab 100644 --- a/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml +++ b/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml @@ -32,6 +32,11 @@ <supported>1</supported> </instant_purchase> </fake_vault> + <fake_no_model> + <!-- This method on purpose does not have a 'model' node. --> + <title>Fake Payment Method without <model> + 0 + - \ No newline at end of file + diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php deleted file mode 100644 index 17ffb5cf2748a..0000000000000 --- a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php +++ /dev/null @@ -1,22 +0,0 @@ -queueMessageResource = $queueMessageResource; + } + + /** + * Delete messages from queue + * + * @param string $topic + * @return void + */ + public function execute(string $topic): void + { + $connection = $this->queueMessageResource->getConnection(); + $condition = $connection->quoteInto(QueueManagement::MESSAGE_TOPIC . '= ?', $topic); + $connection->delete($this->queueMessageResource->getMainTable(), $condition); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php index f492e2cd09570..779cd24791b8c 100644 --- a/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php +++ b/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php @@ -14,7 +14,7 @@ */ class ShowTest extends AbstractBackendController { - private const REPORT_HOST = 'advancedreporting.rjmetrics.com'; + private const REPORT_HOST = 'docs.magento.com'; /** * @inheritDoc */ diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php index bf369ed28167b..a18f1e0799dfa 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php @@ -30,6 +30,11 @@ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase */ protected $productCollectionFactory; + /** + * @var \Magento\CatalogRule\Model\RuleFactory + */ + private $ruleFactory; + protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -43,6 +48,7 @@ protected function setUp(): void true, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); + $this->ruleFactory = $this->objectManager->get(\Magento\CatalogRule\Model\RuleFactory::class); } /** @@ -62,6 +68,8 @@ abstract public function getTestCases(); */ protected function prepareFixture($strategyModifiers, $productSku) { + $this->ruleFactory->create()->clearPriceRulesData(); + $bundleProduct = $this->productRepository->get($productSku); foreach ($strategyModifiers as $modifier) { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 6245e4e9f8de7..cd58cd2ac3819 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -10,6 +10,8 @@ use Magento\Framework\Acl\Builder; use Magento\Backend\App\Area\FrontNameResolver; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ProductMetadata; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\Framework\Registry; @@ -270,7 +272,7 @@ public function testSuggestCategoriesActionNoSuggestions(): void */ public function saveActionDataProvider(): array { - return [ + $result = [ 'default values' => [ [ 'id' => '2', @@ -390,6 +392,20 @@ public function saveActionDataProvider(): array ], ], ]; + + $productMetadataInterface = Bootstrap::getObjectManager()->get(ProductMetadataInterface::class); + if ($productMetadataInterface->getEdition() !== ProductMetadata::EDITION_NAME) { + /** + * Skip save custom_design_from and custom_design_to attributes, + * because this logic is rewritten on EE by Catalog Schedule + */ + foreach (array_keys($result['custom values']) as $index) { + unset($result['custom values'][$index]['custom_design_from']); + unset($result['custom values'][$index]['custom_design_to']); + } + } + + return $result; } /** @@ -398,6 +414,11 @@ public function saveActionDataProvider(): array */ public function testIncorrectDateFrom(): void { + $productMetadataInterface = Bootstrap::getObjectManager()->get(ProductMetadataInterface::class); + if ($productMetadataInterface->getEdition() !== ProductMetadata::EDITION_NAME) { + $this->markTestSkipped('Skipped, because this logic is rewritten on EE by Catalog Schedule'); + } + $data = [ 'name' => 'Test Category', 'attribute_set_id' => '3', diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php index e3b5bc8d5fd0d..c9ad7ad720daa 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php @@ -11,7 +11,10 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Store\Model\StoreManagerInterface; -use Magento\TestFramework\Catalog\Model\Indexer\Product\Flat\Action\Full as FlatIndexerFull; +use Magento\Catalog\Model\Indexer\Product\Flat\Action\Full as FlatIndexerFull; +use Magento\Catalog\Helper\Product\Flat\Indexer; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Helper\Bootstrap; /** * Test relation customization @@ -42,36 +45,76 @@ class RelationTest extends \Magento\TestFramework\Indexer\TestCase */ private $flatUpdated = []; + /** + * @var Indexer + */ + private $productIndexerHelper; + /** * @inheritdoc */ protected function setUp(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $tableBuilderMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder::class); - $flatTableBuilderMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class); + $objectManager = Bootstrap::getObjectManager(); - $productIndexerHelper = $objectManager->create( - \Magento\Catalog\Helper\Product\Flat\Indexer::class, - ['addChildData' => 1] + $this->productIndexerHelper = $objectManager->create( + Indexer::class, + ['addChildData' => true] ); $this->indexer = $objectManager->create( FlatIndexerFull::class, [ - 'productHelper' => $productIndexerHelper, - 'tableBuilder' => $tableBuilderMock, - 'flatTableBuilder' => $flatTableBuilderMock + 'productHelper' => $this->productIndexerHelper, ] ); - $this->storeManager = $objectManager->create(StoreManagerInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); $this->connection = $objectManager->get(ResourceConnection::class)->getConnection(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + foreach ($this->flatUpdated as $flatTable) { + $this->connection->dropColumn($flatTable, 'child_id'); + $this->connection->dropColumn($flatTable, 'is_child'); + } + } + + /** + * Test that SQL generated for relation customization is valid + * + * @return void + * @throws LocalizedException + * @throws \Exception + */ + public function testExecute() : void + { + $this->addChildColumns(); + try { + $result = $this->indexer->execute(); + } catch (LocalizedException $e) { + if ($e->getPrevious() instanceof \Zend_Db_Statement_Exception) { + $this->fail($e->getMessage()); + } + throw $e; + } + $this->assertInstanceOf(FlatIndexerFull::class, $result); + } + /** + * Add child columns to tables if needed + * + * @return void + */ + private function addChildColumns(): void + { foreach ($this->storeManager->getStores() as $store) { - $flatTable = $productIndexerHelper->getFlatTableName($store->getId()); - if ($this->connection->isTableExists($flatTable) && - !$this->connection->tableColumnExists($flatTable, 'child_id') && - !$this->connection->tableColumnExists($flatTable, 'is_child') + $flatTable = $this->productIndexerHelper->getFlatTableName($store->getId()); + if ($this->connection->isTableExists($flatTable) + && !$this->connection->tableColumnExists($flatTable, 'child_id') + && !$this->connection->tableColumnExists($flatTable, 'is_child') ) { $this->connection->addColumn( $flatTable, @@ -103,35 +146,4 @@ protected function setUp(): void } } } - - /** - * @inheritdoc - */ - protected function tearDown(): void - { - foreach ($this->flatUpdated as $flatTable) { - $this->connection->dropColumn($flatTable, 'child_id'); - $this->connection->dropColumn($flatTable, 'is_child'); - } - } - - /** - * Test that SQL generated for relation customization is valid - * - * @return void - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Exception - */ - public function testExecute() : void - { - $this->markTestSkipped('MC-19675'); - try { - $this->indexer->execute(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - if ($e->getPrevious() instanceof \Zend_Db_Statement_Exception) { - $this->fail($e->getMessage()); - } - throw $e; - } - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 2659f14c07c7a..7ee2c62453df5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -416,7 +416,7 @@ public function testDeleteWithMultiWebsites(): void $product->setWebsiteIds([$defaultWebsiteId, $secondWebsiteId]); $this->productRepository->save($product); // Assert that product image has roles in global scope only - $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $imageRolesPerStore = $this->getProductStoreImageRoles($product, $imageRoles); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); @@ -428,7 +428,7 @@ public function testDeleteWithMultiWebsites(): void $product->addData(array_fill_keys($imageRoles, $image)); $this->productRepository->save($product); // Assert that roles are assigned to product image for second store - $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $imageRolesPerStore = $this->getProductStoreImageRoles($product, $imageRoles); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); @@ -454,7 +454,7 @@ public function testDeleteWithMultiWebsites(): void $this->assertEmpty($product->getMediaGalleryEntries()); $this->assertFileDoesNotExist($path); // Load image roles - $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $imageRolesPerStore = $this->getProductStoreImageRoles($product, $imageRoles); // Assert that image roles are reset on global scope and removed on second store // as the product is no longer assigned to second website $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['image']); @@ -466,14 +466,17 @@ public function testDeleteWithMultiWebsites(): void /** * @param Product $product + * @param array $roles * @return array */ - private function getProductStoreImageRoles(Product $product): array + private function getProductStoreImageRoles(Product $product, array $roles = []): array { $imageRolesPerStore = []; $stores = array_keys($this->storeManager->getStores(true)); foreach ($this->galleryResource->getProductImages($product, $stores) as $role) { - $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + if (empty($roles) || in_array($role['attribute_code'], $roles)) { + $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + } } return $imageRolesPerStore; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php index c2ebfa4389ab2..69172f3edb34f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php @@ -50,7 +50,11 @@ 'is_visible_on_front' => 1, 'used_in_product_listing' => 1, 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], + 'frontend_label' => [ + Store::DEFAULT_STORE_ID => 'Test Configurable Admin Store', + Store::DISTRO_STORE_ID => 'Test Configurable Default Store', + $store->getId() => 'Test Configurable Test Store' + ], 'backend_type' => 'int', 'option' => [ 'value' => ['option_0' => [ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php index 6793051b5787b..60a8525dede24 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php @@ -24,12 +24,6 @@ } } -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); -foreach ($productCollection as $product) { - $product->delete(); -} - /** @var $category \Magento\Catalog\Model\Category */ $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); $category->load(333); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index a9699ea4a8050..669a9959de91c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -34,6 +34,7 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\TestFramework\Indexer\TestCase; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; use Psr\Log\LoggerInterface; @@ -50,8 +51,10 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * phpcs:disable Generic.PHP.NoSilencedErrors, Generic.Metrics.NestingLevel, Magento2.Functions.StaticFunction */ -class ProductTest extends \Magento\TestFramework\Indexer\TestCase +class ProductTest extends TestCase { + private const LONG_FILE_NAME_IMAGE = 'magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg'; + /** * @var \Magento\CatalogImportExport\Model\Import\Product */ @@ -729,7 +732,7 @@ function ($input) { ) ); // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $option = array_merge(...$option); + $option = array_merge([], ...$option); if (!empty($option['type']) && !empty($option['name'])) { $lastOptionKey = $option['type'] . '|' . $option['name']; @@ -1029,13 +1032,12 @@ function (\Magento\Framework\DataObject $item) { ) ); - $this->importDataForMediaTest('import_media_additional_images.csv'); + $this->importDataForMediaTest('import_media_additional_long_name_image.csv'); $product->cleanModelCache(); $product = $this->getProductBySku('simple_new'); $items = array_values($product->getMediaGalleryImages()->getItems()); - $images[] = ['file' => '/m/a/magento_additional_image_three.jpg', 'label' => '']; - $images[] = ['file' => '/m/a/magento_additional_image_four.jpg', 'label' => '']; - $this->assertCount(7, $items); + $images[] = ['file' => '/m/a/' . self::LONG_FILE_NAME_IMAGE, 'label' => '']; + $this->assertCount(6, $items); $this->assertEquals( $images, array_map( @@ -1047,6 +1049,23 @@ function (\Magento\Framework\DataObject $item) { ); } + /** + * Test import twice and check that image will not be duplicate + * + * @magentoDataFixture mediaImportImageFixture + * @return void + */ + public function testSaveMediaImageDuplicateImages(): void + { + $this->importDataForMediaTest('import_media.csv'); + $imagesCount = count($this->getProductBySku('simple_new')->getMediaGalleryImages()->getItems()); + + // import the same file again + $this->importDataForMediaTest('import_media.csv'); + + $this->assertCount($imagesCount, $this->getProductBySku('simple_new')->getMediaGalleryImages()->getItems()); + } + /** * Test that errors occurred during importing images are logged. * @@ -1089,6 +1108,10 @@ public static function mediaImportImageFixture() 'source' => __DIR__ . '/../../../../Magento/Catalog/_files/magento_thumbnail.jpg', 'dest' => $dirPath . '/magento_thumbnail.jpg', ], + [ + 'source' => __DIR__ . '/../../../../Magento/Catalog/_files/' . self::LONG_FILE_NAME_IMAGE, + 'dest' => $dirPath . '/' . self::LONG_FILE_NAME_IMAGE, + ], [ 'source' => __DIR__ . '/_files/magento_additional_image_one.jpg', 'dest' => $dirPath . '/magento_additional_image_one.jpg', @@ -2252,7 +2275,7 @@ function (ProductInterface $item) { $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); $collection - ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge(...$categoryIds))]) + ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge([], ...$categoryIds))]) ->load() ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv new file mode 100644 index 0000000000000..2d2a192ed6c7c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv @@ -0,0 +1,2 @@ +sku,additional_images +simple_new,magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php index e2f8a2d5e4c21..3f9ab815038cc 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php @@ -9,6 +9,9 @@ use Magento\Eav\Model\Config as EavConfig; use Magento\TestFramework\Helper\Bootstrap; +/** + * @magentoAppArea adminhtml + */ class AttributesTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php new file mode 100644 index 0000000000000..ab3eda8cf4e9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php @@ -0,0 +1,67 @@ +getBlockByIdentifier = $this->_objectManager->get(GetBlockByIdentifierInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->collectionFactory = $this->_objectManager->get(CollectionFactory::class); + } + + /** + * @magentoDataFixture Magento/Cms/_files/block_default_store.php + * + * @return void + */ + public function testDeleteBlock(): void + { + $defaultStoreId = (int)$this->storeManager->getStore('default')->getId(); + $blockId = $this->getBlockByIdentifier->execute('default_store_block', $defaultStoreId)->getId(); + $this->getRequest()->setMethod(Http::METHOD_POST) + ->setParams(['block_id' => $blockId]); + $this->dispatch('backend/cms/block/delete'); + $this->assertSessionMessages( + $this->containsEqual((string)__('You deleted the block.')), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('cms/block/index')); + $collection = $this->collectionFactory->getReport('cms_block_listing_data_source'); + $this->assertNull($collection->getItemByColumnValue(BlockInterface::IDENTIFIER, 'default_store_block')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php index 2ec87f758b812..33635d3678726 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php @@ -8,12 +8,16 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\Model\Session; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerNameGenerationInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Data\Customer as CustomerData; use Magento\Customer\Model\EmailNotification; +use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\Framework\App\Area; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Message\MessageInterface; @@ -21,6 +25,7 @@ use Magento\Newsletter\Model\SubscriberFactory; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractBackendController; use PHPUnit\Framework\MockObject\MockObject; @@ -54,6 +59,12 @@ class SaveTest extends AbstractBackendController /** @var StoreManagerInterface */ private $storeManager; + /** @var ResolverInterface */ + private $localeResolver; + + /** @var CustomerInterface */ + private $customer; + /** * @inheritdoc */ @@ -65,6 +76,19 @@ protected function setUp(): void $this->subscriberFactory = $this->_objectManager->get(SubscriberFactory::class); $this->session = $this->_objectManager->get(Session::class); $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->localeResolver = $this->_objectManager->get(ResolverInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->customer instanceof CustomerInterface) { + $this->customerRepository->delete($this->customer); + } + + parent::tearDown(); } /** @@ -418,6 +442,42 @@ public function testCreateSameEmailFormatDateError(): void $this->assertRedirect($this->stringContains($this->baseControllerUrl . 'new/key/')); } + /** + * @return void + */ + public function testCreateCustomerByAdminWithLocaleGB(): void + { + $this->localeResolver->setLocale('en_GB'); + $postData = array_replace_recursive( + $this->getDefaultCustomerData(), + [ + 'customer' => [ + CustomerData::DOB => '24/10/1990', + ], + ] + ); + $expectedData = array_replace_recursive( + $postData, + [ + 'customer' => [ + CustomerData::DOB => '1990-10-24', + ], + ] + ); + unset($expectedData['customer']['sendemail_store_id']); + $this->dispatchCustomerSave($postData); + $this->assertSessionMessages( + $this->containsEqual((string)__('You saved the customer.')), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains($this->baseControllerUrl . 'index/key/')); + $this->assertCustomerData( + $postData['customer'][CustomerData::EMAIL], + (int)$postData['customer'][CustomerData::WEBSITE_ID], + $expectedData + ); + } + /** * Default values for customer creation * @@ -438,7 +498,8 @@ private function getDefaultCustomerData(): array CustomerData::EMAIL => 'janedoe' . uniqid() . '@example.com', CustomerData::DOB => '01/01/2000', CustomerData::TAXVAT => '121212', - CustomerData::GENDER => 'Male', + CustomerData::GENDER => Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class) + ->get(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'gender')->getSource()->getOptionId('Male'), 'sendemail_store_id' => '1', ] ]; @@ -458,7 +519,6 @@ private function getExpectedCustomerData(array $defaultCustomerData): array [ 'customer' => [ CustomerData::DOB => '2000-01-01', - CustomerData::GENDER => '0', CustomerData::STORE_ID => 1, CustomerData::CREATED_IN => 'Default Store View', ], @@ -496,9 +556,8 @@ private function assertCustomerData( int $customerWebsiteId, array $expectedData ): void { - /** @var CustomerData $customerData */ - $customerData = $this->customerRepository->get($customerEmail, $customerWebsiteId); - $actualCustomerArray = $customerData->__toArray(); + $this->customer = $this->customerRepository->get($customerEmail, $customerWebsiteId); + $actualCustomerArray = $this->customer->__toArray(); foreach ($expectedData['customer'] as $key => $expectedValue) { $this->assertEquals( $expectedValue, diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php index fe92c295b47fa..e19d0a7364b27 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php @@ -16,6 +16,8 @@ require_once __DIR__ . '/GeneratorTest/SourceClassWithNamespace.php'; require_once __DIR__ . '/GeneratorTest/ParentClassWithNamespace.php'; require_once __DIR__ . '/GeneratorTest/SourceClassWithNamespaceExtension.php'; +require_once __DIR__ . '/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php'; +require_once __DIR__ . '/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespaceExtension.php'; /** * @magentoAppIsolation enabled @@ -24,6 +26,10 @@ class GeneratorTest extends TestCase { const CLASS_NAME_WITH_NAMESPACE = GeneratorTest\SourceClassWithNamespace::class; + const CLASS_NAME_WITH_NESTED_NAMESPACE = GeneratorTest\NestedNamespace\SourceClassWithNestedNamespace::class; + const EXTENSION_CLASS_NAME_WITH_NAMESPACE = GeneratorTest\SourceClassWithNamespaceExtension::class; + const EXTENSION_CLASS_NAME_WITH_NESTED_NAMESPACE = + GeneratorTest\NestedNamespace\SourceClassWithNestedNamespaceExtension::class; /** * @var Generator @@ -59,6 +65,7 @@ protected function setUp(): void /** @var Filesystem $filesystem */ $filesystem = $objectManager->get(Filesystem::class); $this->generatedDirectory = $filesystem->getDirectoryWrite(DirectoryList::GENERATED_CODE); + $this->generatedDirectory->create($this->testRelativePath); $this->logDirectory = $filesystem->getDirectoryRead(DirectoryList::LOG); $generatedDirectoryAbsolutePath = $this->generatedDirectory->getAbsolutePath(); $this->_ioObject = new Generator\Io(new Filesystem\Driver\File(), $generatedDirectoryAbsolutePath); @@ -98,78 +105,99 @@ protected function _clearDocBlock($classBody) } /** - * Generates a new file with Factory class and compares with the sample from the - * SourceClassWithNamespaceFactory.php.sample file. + * Generates a new class Factory file and compares with the sample. + * + * @param $className + * @param $generateType + * @param $expectedDataPath + * @dataProvider generateClassFactoryDataProvider */ - public function testGenerateClassFactoryWithNamespace() + public function testGenerateClassFactory($className, $generateType, $expectedDataPath) { - $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'Factory'; + $factoryClassName = $className . $generateType; $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $factory->create()); + $this->assertInstanceOf($className, $factory->create()); $content = $this->_clearDocBlock( file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) ); $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceFactory.php.sample') + file_get_contents(__DIR__ . $expectedDataPath) ); $this->assertEquals($expectedContent, $content); } /** - * Generates a new file with Proxy class and compares with the sample from the - * SourceClassWithNamespaceProxy.php.sample file. + * DataProvider for testGenerateClassFactory + * + * @return array */ - public function testGenerateClassProxyWithNamespace() + public function generateClassFactoryDataProvider() { - $proxyClassName = self::CLASS_NAME_WITH_NAMESPACE . '\Proxy'; - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($proxyClassName)); - $proxy = Bootstrap::getObjectManager()->create($proxyClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $proxy); - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($proxyClassName)) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceProxy.php.sample') - ); - $this->assertEquals($expectedContent, $content); + return [ + 'factory_with_namespace' => [ + 'className' => self::CLASS_NAME_WITH_NAMESPACE, + 'generateType' => 'Factory', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceFactory.php.sample' + ], + 'factory_with_nested_namespace' => [ + 'classToGenerate' => self::CLASS_NAME_WITH_NESTED_NAMESPACE, + 'generateType' => 'Factory', + 'expectedDataPath' => '/_expected/SourceClassWithNestedNamespaceFactory.php.sample' + ], + 'ext_interface_factory_with_namespace' => [ + 'classToGenerate' => self::EXTENSION_CLASS_NAME_WITH_NAMESPACE, + 'generateType' => 'InterfaceFactory', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceExtensionInterfaceFactory.php.sample' + ], + 'ext_interface_factory_with_nested_namespace' => [ + 'classToGenerate' => self::EXTENSION_CLASS_NAME_WITH_NESTED_NAMESPACE, + 'generateType' => 'InterfaceFactory', + 'expectedDataPath' => '/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample' + ], + ]; } /** - * Generates a new file with Interceptor class and compares with the sample from the - * SourceClassWithNamespaceInterceptor.php.sample file. + * @param $className + * @param $generateType + * @param $expectedDataPath + * @dataProvider generateClassDataProvider */ - public function testGenerateClassInterceptorWithNamespace() + public function testGenerateClass($className, $generateType, $expectedDataPath) { - $interceptorClassName = self::CLASS_NAME_WITH_NAMESPACE . '\Interceptor'; - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($interceptorClassName)); + $generateClassName = $className . $generateType; + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($generateClassName)); + $instance = Bootstrap::getObjectManager()->create($generateClassName); + $this->assertInstanceOf($className, $instance); $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($interceptorClassName)) + file_get_contents($this->_ioObject->generateResultFileName($generateClassName)) ); $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceInterceptor.php.sample') + file_get_contents(__DIR__ . $expectedDataPath) ); $this->assertEquals($expectedContent, $content); } /** - * Generates a new file with ExtensionInterfaceFactory class and compares with the sample from the - * SourceClassWithNamespaceExtensionInterfaceFactory.php.sample file. + * DataProvider for testGenerateClass + * + * @return array */ - public function testGenerateClassExtensionAttributesInterfaceFactoryWithNamespace() + public function generateClassDataProvider() { - $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'ExtensionInterfaceFactory'; - $this->generatedDirectory->create($this->testRelativePath); - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); - $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE . 'Extension', $factory->create()); - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceExtensionInterfaceFactory.php.sample') - ); - $this->assertEquals($expectedContent, $content); + return [ + 'proxy' => [ + 'className' => self::CLASS_NAME_WITH_NAMESPACE, + 'generateType' => '\Proxy', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceProxy.php.sample' + ], + 'interceptor' => [ + 'className' => self::CLASS_NAME_WITH_NAMESPACE, + 'generateType' => '\Interceptor', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceInterceptor.php.sample' + ] + ]; } /** @@ -183,7 +211,6 @@ public function testGeneratorClassWithErrorSaveClassFile() $regexpMsgPart = preg_quote($msgPart); $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches("/.*$regexpMsgPart.*/"); - $this->generatedDirectory->create($this->testRelativePath); $this->generatedDirectory->changePermissionsRecursively($this->testRelativePath, 0555, 0444); $generatorResult = $this->_generator->generateClass($factoryClassName); $this->assertFalse($generatorResult); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php new file mode 100644 index 0000000000000..6471a198b31f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php @@ -0,0 +1,170 @@ +_objectManager = $objectManager; + $this->_instanceName = $instanceName; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Magento\Framework\Code\GeneratorTest\NestedNamespace\SourceClassWithNestedNamespaceExtension + */ + public function create(array $data = []) + { + return $this->_objectManager->create($this->_instanceName, $data); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample new file mode 100644 index 0000000000000..1913968d199af --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample @@ -0,0 +1,48 @@ +_objectManager = $objectManager; + $this->_instanceName = $instanceName; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Magento\Framework\Code\GeneratorTest\NestedNamespace\SourceClassWithNestedNamespace + */ + public function create(array $data = []) + { + return $this->_objectManager->create($this->_instanceName, $data); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php index fb367fd557416..ca8cca878d091 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php @@ -7,15 +7,17 @@ */ namespace Magento\Framework\Filesystem\Directory; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem\DriverPool; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * Class ReadTest * Test for Magento\Framework\Filesystem\Directory\Read class */ -class WriteTest extends \PHPUnit\Framework\TestCase +class WriteTest extends TestCase { /** * Test data to be cleaned @@ -41,6 +43,8 @@ public function testInstance() * @param string $basePath * @param int $permissions * @param string $path + * @throws FileSystemException + * @throws ValidatorException */ public function testCreate($basePath, $permissions, $path) { @@ -64,6 +68,11 @@ public function createProvider() ]; } + /** + * Test for create outside + * + * @throws FileSystemException + */ public function testCreateOutside() { $exceptions = 0; @@ -91,6 +100,8 @@ public function testCreateOutside() * * @dataProvider deleteProvider * @param string $path + * @throws FileSystemException + * @throws ValidatorException */ public function testDelete($path) { @@ -111,6 +122,11 @@ public function deleteProvider() return [['subdir'], ['subdir/subsubdir']]; } + /** + * Test for delete outside + * + * @throws FileSystemException + */ public function testDeleteOutside() { $exceptions = 0; @@ -141,6 +157,8 @@ public function testDeleteOutside() * @param int $permissions * @param string $name * @param string $newName + * @throws FileSystemException + * @throws ValidatorException */ public function testRename($basePath, $permissions, $name, $newName) { @@ -164,6 +182,11 @@ public function renameProvider() return [['newDir1', 0777, 'first_name.txt', 'second_name.txt']]; } + /** + * Test for rename outside + * + * @throws FileSystemException + */ public function testRenameOutside() { $exceptions = 0; @@ -198,6 +221,8 @@ public function testRenameOutside() * @param int $permission * @param string $name * @param string $newName + * @throws FileSystemException + * @throws ValidatorException */ public function testRenameTargetDir($firstDir, $secondDir, $permission, $name, $newName) { @@ -231,6 +256,8 @@ public function renameTargetDirProvider() * @param int $permissions * @param string $name * @param string $newName + * @throws ValidatorException + * @throws FileSystemException */ public function testCopy($basePath, $permissions, $name, $newName) { @@ -255,6 +282,11 @@ public function copyProvider() ]; } + /** + * Test for copy outside + * + * @throws FileSystemException|ValidatorException + */ public function testCopyOutside() { $exceptions = 0; @@ -298,6 +330,8 @@ public function testCopyOutside() * @param int $permission * @param string $name * @param string $newName + * @throws FileSystemException + * @throws ValidatorException */ public function testCopyTargetDir($firstDir, $secondDir, $permission, $name, $newName) { @@ -327,6 +361,8 @@ public function copyTargetDirProvider() /** * Test for changePermissions method + * + * @throws FileSystemException|ValidatorException */ public function testChangePermissions() { @@ -335,6 +371,11 @@ public function testChangePermissions() $this->assertTrue($directory->changePermissions('test_directory', 0644)); } + /** + * Test for changePermissions outside + * + * @throws FileSystemException + */ public function testChangePermissionsOutside() { $exceptions = 0; @@ -359,6 +400,8 @@ public function testChangePermissionsOutside() /** * Test for changePermissionsRecursively method + * + * @throws FileSystemException|ValidatorException */ public function testChangePermissionsRecursively() { @@ -370,6 +413,11 @@ public function testChangePermissionsRecursively() $this->assertTrue($directory->changePermissionsRecursively('test_directory', 0777, 0644)); } + /** + * Test for changePermissionsRecursively outside + * + * @throws FileSystemException + */ public function testChangePermissionsRecursivelyOutside() { $exceptions = 0; @@ -400,6 +448,8 @@ public function testChangePermissionsRecursivelyOutside() * @param int $permissions * @param string $path * @param int $time + * @throws FileSystemException + * @throws ValidatorException */ public function testTouch($basePath, $permissions, $path, $time) { @@ -422,6 +472,11 @@ public function touchProvider() ]; } + /** + * Test for touch outside + * + * @throws FileSystemException + */ public function testTouchOutside() { $exceptions = 0; @@ -446,6 +501,8 @@ public function testTouchOutside() /** * Test isWritable method + * + * @throws FileSystemException|ValidatorException */ public function testIsWritable() { @@ -455,6 +512,11 @@ public function testIsWritable() $this->assertTrue($directory->isWritable('bar')); } + /** + * Test isWritable method outside + * + * @throws FileSystemException + */ public function testIsWritableOutside() { $exceptions = 0; @@ -485,6 +547,8 @@ public function testIsWritableOutside() * @param int $permissions * @param string $path * @param string $mode + * @throws FileSystemException + * @throws ValidatorException */ public function testOpenFile($basePath, $permissions, $path, $mode) { @@ -507,6 +571,11 @@ public function openFileProvider() ]; } + /** + * Test for openFile outside + * + * @throws FileSystemException + */ public function testOpenFileOutside() { $exceptions = 0; @@ -536,6 +605,8 @@ public function testOpenFileOutside() * @param string $path * @param string $content * @param string $extraContent + * @throws FileSystemException + * @throws ValidatorException */ public function testWriteFile($path, $content, $extraContent) { @@ -553,6 +624,8 @@ public function testWriteFile($path, $content, $extraContent) * @param string $path * @param string $content * @param string $extraContent + * @throws FileSystemException + * @throws ValidatorException */ public function testWriteFileAppend($path, $content, $extraContent) { @@ -573,6 +646,11 @@ public function writeFileProvider() return [['file1', '123', '456'], ['folder1/file1', '123', '456']]; } + /** + * Test for writeFile outside + * + * @throws FileSystemException + */ public function testWriteFileOutside() { $exceptions = 0; @@ -595,8 +673,24 @@ public function testWriteFileOutside() $this->assertEquals(3, $exceptions); } + /** + * Test for invalidDeletePath + * + * @throws ValidatorException + */ + public function testInvalidDeletePath() + { + $this->expectException(FileSystemException::class); + $directory = $this->getDirectoryInstance('newDir', 0777); + $invalidPath = 'invalidPath/../'; + $directory->create($invalidPath); + $directory->delete($invalidPath); + } + /** * Tear down + * + * @throws ValidatorException|FileSystemException */ protected function tearDown(): void { @@ -620,8 +714,8 @@ private function getDirectoryInstance($path, $permissions) { $fullPath = __DIR__ . '/../_files/' . $path; $objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Framework\Filesystem\Directory\WriteFactory $directoryFactory */ - $directoryFactory = $objectManager->create(\Magento\Framework\Filesystem\Directory\WriteFactory::class); + /** @var WriteFactory $directoryFactory */ + $directoryFactory = $objectManager->create(WriteFactory::class); $directory = $directoryFactory->create($fullPath, DriverPool::FILE, $permissions); $this->testDirectories[] = $directory; return $directory; diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php deleted file mode 100644 index a8e9059a84eb6..0000000000000 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php +++ /dev/null @@ -1,145 +0,0 @@ -dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } - - /** - * Test request is served from cache - * - * @magentoDataFixture Magento/Catalog/_files/category_product.php - */ - public function testSecondRequestIsServedFromCache() - { - $categoryId ='333'; - $query - = <<dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - - $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } - - /** - * Test cache tags are generated - * - * @magentoDataFixture Magento/Catalog/_files/category_tree.php - */ - public function testRequestCacheTagsForCategoryListOnMultipleIds(): void - { - $categoryId1 ='400'; - $categoryId2 = '401'; - $query - = <<dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } - - /** - * Test request is served from cache - * - * @magentoDataFixture Magento/Catalog/_files/category_tree.php - */ - public function testSecondRequestIsServedFromCacheOnMultipleIds() - { - $categoryId1 ='400'; - $categoryId2 = '401'; - $query - = <<dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - - $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } -} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php new file mode 100644 index 0000000000000..977a0ed5b144c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php @@ -0,0 +1,54 @@ +dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php new file mode 100644 index 0000000000000..51a9218e6a37a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php @@ -0,0 +1,53 @@ +dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Inventory/ParentItemProcessorTest.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Inventory/ParentItemProcessorTest.php new file mode 100644 index 0000000000000..4b430e7a71886 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Inventory/ParentItemProcessorTest.php @@ -0,0 +1,59 @@ +objectManager = Bootstrap::getObjectManager(); + } + + /** + * Test stock status parent product if children are out of stock + * + * @magentoDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock.php + * + * @return void + */ + public function testOutOfStockParentProduct(): void + { + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get('simple_100000001'); + $product->setStockData(['qty' => 0, 'is_in_stock' => 0]); + $productRepository->save($product); + /** @var StockItemRepository $stockItemRepository */ + $stockItemRepository = $this->objectManager->create(StockItemRepository::class); + /** @var StockRegistryInterface $stockRegistry */ + $stockRegistry = $this->objectManager->create(StockRegistryInterface::class); + $stockItem = $stockRegistry->getStockItemBySku('grouped'); + $stockItem = $stockItemRepository->get($stockItem->getItemId()); + + $this->assertEquals(false, $stockItem->getIsInStock()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php new file mode 100644 index 0000000000000..834d6a4f06b65 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php @@ -0,0 +1,99 @@ +queueManagement = $this->_objectManager->get(QueueManagement::class); + $this->queueMessageResource = $this->_objectManager->get(Message::class); + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->deleteTopicRelatedMessages = $this->_objectManager->get(DeleteTopicRelatedMessages::class); + $this->deleteTopicRelatedMessages->execute(self::TOPIC_NAME); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->deleteTopicRelatedMessages->execute(self::TOPIC_NAME); + + parent::tearDown(); + } + + /** + * @magentoConfigFixture default_store admin/security/use_form_key 1 + * + * @return void + */ + public function testExecute(): void + { + $expectedSessionMessage = (string)__('Message is added to queue, wait to get your file soon.' + . ' Make sure your cron job is running to export the file'); + $fileFormat = 'csv'; + $filter = ['price' => [0, 1000]]; + $this->getRequest()->setMethod(Http::METHOD_POST) + ->setPostValue(['export_filter' => [$filter]]) + ->setParams( + [ + 'entity' => ProductAttributeInterface::ENTITY_TYPE_CODE, + 'file_format' => $fileFormat, + ] + ); + $this->dispatch('backend/admin/export/export'); + $this->assertSessionMessages($this->containsEqual($expectedSessionMessage)); + $this->assertRedirect($this->stringContains('/export/index/key/')); + $messages = $this->queueManagement->readMessages('export'); + $this->assertCount(1, $messages); + $message = reset($messages); + $this->assertEquals(self::TOPIC_NAME, $message[QueueManagement::MESSAGE_TOPIC]); + $body = $this->json->unserialize($message[QueueManagement::MESSAGE_BODY]); + $this->assertStringContainsString(ProductAttributeInterface::ENTITY_TYPE_CODE, $body['file_name']); + $this->assertEquals($fileFormat, $body['file_format']); + $actualFilter = $this->json->unserialize($body['export_filter']); + $this->assertCount(1, $actualFilter); + $this->assertEquals($filter, reset($actualFilter)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php new file mode 100644 index 0000000000000..a016ba1d962b9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php @@ -0,0 +1,100 @@ +objectManager = Bootstrap::getObjectManager(); + $this->queue = $this->objectManager->create(Queue::class, ['queueName' => 'export']); + $this->messageEncoder = $this->objectManager->get(MessageEncoder::class); + $this->consumer = $this->objectManager->get(Consumer::class); + $this->directory = $this->objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->csvReader = $this->objectManager->get(Csv::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->filePath && $this->directory->isExist($this->filePath)) { + $this->directory->delete($this->filePath); + } + + parent::tearDown(); + } + + /** + * @magentoConfigFixture default_store admin/security/use_form_key 1 + * + * @magentoDataFixture Magento/ImportExport/_files/export_queue_data.php + * @magentoDataFixture Magento/Catalog/_files/product_virtual.php + * + * @return void + */ + public function testProcess(): void + { + $envelope = $this->queue->dequeue(); + $decodedMessage = $this->messageEncoder->decode('import_export.export', $envelope->getBody()); + $this->consumer->process($decodedMessage); + $this->filePath = 'export/' . $decodedMessage->getFileName(); + $this->assertTrue($this->directory->isExist($this->filePath)); + $data = $this->csvReader->getData($this->directory->getAbsolutePath($this->filePath)); + $this->assertCount(2, $data); + $skuPosition = array_search(ProductInterface::SKU, array_keys($data)); + $this->assertNotFalse($skuPosition); + $this->assertEquals('simple2', $data[1][$skuPosition]); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php new file mode 100644 index 0000000000000..1fc71ffbf3975 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php @@ -0,0 +1,28 @@ +requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ExportInfoFactory $exportInfoFactory */ +$exportInfoFactory = $objectManager->get(ExportInfoFactory::class); +/** @var PublisherInterface $messagePublisher */ +$messagePublisher = $objectManager->get(PublisherInterface::class); +$dataObject = $exportInfoFactory->create( + 'csv', + ProductAttributeInterface::ENTITY_TYPE_CODE, + [ProductInterface::SKU => 'simple2'], + [] +); +$messagePublisher->publish('import_export.export', $dataObject); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_rollback.php b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_rollback.php new file mode 100644 index 0000000000000..f1d2ba67aa035 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_rollback.php @@ -0,0 +1,17 @@ +get(DeleteTopicRelatedMessages::class); +$deleteTopicRelatedMessages->execute('import_export.export'); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/IntegrationTest.php b/dev/tests/integration/testsuite/Magento/IntegrationTest.php index b7c692b49812d..4626a10884290 100644 --- a/dev/tests/integration/testsuite/Magento/IntegrationTest.php +++ b/dev/tests/integration/testsuite/Magento/IntegrationTest.php @@ -11,9 +11,16 @@ use Magento\TestFramework\Workaround\Override\Config; use Magento\TestFramework\Workaround\Override\WrapperGenerator; use PHPUnit\Framework\TestSuite; +use PHPUnit\TextUI\Configuration\Configuration as LegacyConfiguration; use PHPUnit\TextUI\Configuration\Registry; -use PHPUnit\TextUI\Configuration\TestSuiteCollection; -use PHPUnit\TextUI\Configuration\TestSuiteMapper; +use PHPUnit\TextUI\Configuration\TestSuite as LegacyTestSuiteConfiguration; +use PHPUnit\TextUI\Configuration\TestSuiteCollection as LegacyTestSuiteCollection; +use PHPUnit\TextUI\Configuration\TestSuiteMapper as LegacyTestSuiteMapper; +use PHPUnit\TextUI\XmlConfiguration\Configuration; +use PHPUnit\TextUI\XmlConfiguration\Loader; +use PHPUnit\TextUI\XmlConfiguration\TestSuite as TestSuiteConfiguration; +use PHPUnit\TextUI\XmlConfiguration\TestSuiteCollection; +use PHPUnit\TextUI\XmlConfiguration\TestSuiteMapper; /** * Integration tests wrapper. @@ -24,20 +31,20 @@ class IntegrationTest extends TestSuite * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param string $className * @return TestSuite + * @throws \ReflectionException */ public static function suite($className) { $generator = new WrapperGenerator(); $overrideConfig = Config::getInstance(); - $configuration = Registry::getInstance()->get(self::getConfigurationFile()); + $configuration = self::getConfiguration(); $suitesConfig = $configuration->testSuite(); $suite = new TestSuite(); - /** @var \PHPUnit\TextUI\Configuration\TestSuite $suiteConfig */ foreach ($suitesConfig as $suiteConfig) { if ($suiteConfig->name() === 'Magento Integration Tests') { continue; } - $suites = (new TestSuiteMapper())->map(TestSuiteCollection::fromArray([$suiteConfig]), ''); + $suites = self::getSuites($suiteConfig); /** @var TestSuite $testSuite */ foreach ($suites as $testSuite) { /** @var TestSuite $test */ @@ -71,4 +78,39 @@ private static function getConfigurationFile(): string return $shortConfig ? $shortConfig : $longConfig; } + + /** + * Retrieve configuration depends on used phpunit version + * + * @return Configuration|LegacyConfiguration + */ + private static function getConfiguration() + { + // Compatibility with phpunit < 9.3 + if (!class_exists(Configuration::class)) { + // @phpstan-ignore-next-line + return Registry::getInstance()->get(self::getConfigurationFile()); + } + + // @phpstan-ignore-next-line + return (new Loader())->load(self::getConfigurationFile()); + } + + /** + * Retrieve test suites by suite config depends on used phpunit version + * + * @param TestSuiteConfiguration|LegacyTestSuiteConfiguration $suiteConfig + * @return TestSuite + */ + private static function getSuites($suiteConfig) + { + // Compatibility with phpunit < 9.3 + if (!class_exists(Configuration::class)) { + // @phpstan-ignore-next-line + return (new LegacyTestSuiteMapper())->map(LegacyTestSuiteCollection::fromArray([$suiteConfig]), ''); + } + + // @phpstan-ignore-next-line + return (new TestSuiteMapper())->map(TestSuiteCollection::fromArray([$suiteConfig]), ''); + } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php index b949a68ed4c1c..df542dd622864 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php @@ -21,8 +21,8 @@ use Magento\Framework\Search\Request\Config; use Magento\Framework\View\LayoutInterface; use Magento\LayeredNavigation\Block\Navigation; -use Magento\LayeredNavigation\Block\Navigation\Search as SearchNavigationBlock; use Magento\LayeredNavigation\Block\Navigation\Category as CategoryNavigationBlock; +use Magento\LayeredNavigation\Block\Navigation\Search as SearchNavigationBlock; use Magento\Search\Model\Search; use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; @@ -124,6 +124,38 @@ protected function getCategoryFiltersAndAssert( } } + /** + * Tests getFilters method from navigation block layer state on category page. + * + * @param array $products + * @param array $expectation + * @param string $categoryName + * @param string|null $filterValue + * @param int $productsCount + * @return void + */ + protected function getCategoryActiveFiltersAndAssert( + array $products, + array $expectation, + string $categoryName, + string $filterValue, + int $productsCount + ): void { + $this->updateAttribute(['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS]); + $this->updateProducts($products, $this->getAttributeCode()); + $this->clearInstanceAndReindexSearch(); + $this->navigationBlock->getRequest()->setParams($this->getRequestParams($filterValue)); + $this->navigationBlock->getLayer()->setCurrentCategory( + $this->loadCategory($categoryName, Store::DEFAULT_STORE_ID) + ); + $this->navigationBlock->setLayout($this->layout); + $activeFilters = $this->navigationBlock->getLayer()->getState()->getFilters(); + $this->assertCount(1, $activeFilters); + $currentFilter = reset($activeFilters); + $this->assertActiveFilter($expectation, $currentFilter); + $this->assertEquals($productsCount, $this->navigationBlock->getLayer()->getProductCollection()->getSize()); + } + /** * Tests getFilters method from navigation block on search page. * @@ -152,6 +184,37 @@ protected function getSearchFiltersAndAssert( } } + /** + * Tests getFilters method from navigation block layer state on search page. + * + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + protected function getSearchActiveFiltersAndAssert( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->updateAttribute( + ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS, 'is_filterable_in_search' => 1] + ); + $this->updateProducts($products, $this->getAttributeCode()); + $this->clearInstanceAndReindexSearch(); + $this->navigationBlock->getRequest()->setParams( + array_merge($this->getRequestParams($filterValue), ['q' => $this->getSearchString()]) + ); + $this->navigationBlock->setLayout($this->layout); + $activeFilters = $this->navigationBlock->getLayer()->getState()->getFilters(); + $this->assertCount(1, $activeFilters); + $currentFilter = reset($activeFilters); + $this->assertActiveFilter($expectation, $currentFilter); + $this->assertEquals($productsCount, $this->navigationBlock->getLayer()->getProductCollection()->getSize()); + } + /** * Returns filter with specified attribute. * @@ -303,4 +366,37 @@ protected function getSearchString(): string { return 'Simple Product'; } + + /** + * Adds params for filtering. + * + * @param string $filterValue + * @return array + */ + protected function getRequestParams(string $filterValue): array + { + $attribute = $this->attributeRepository->get($this->getAttributeCode()); + $filterValue = $attribute->usesSource() + ? $attribute->getSource()->getOptionId($filterValue) + : $filterValue; + + return [$this->getAttributeCode() => $filterValue]; + } + + /** + * Asserts active filter data. + * + * @param array $expectation + * @param Item $currentFilter + * @return void + */ + protected function assertActiveFilter(array $expectation, Item $currentFilter): void + { + $this->assertEquals($expectation['label'], $currentFilter->getData('label')); + $this->assertEquals($expectation['count'], $currentFilter->getData('count')); + $this->assertEquals( + $this->getAttributeCode(), + $currentFilter->getFilter()->getData('attribute_model')->getAttributeCode() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php index 24787bc3c4ca8..52c2c6ea6f66e 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php @@ -69,6 +69,58 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_boolean_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'selected_yes_option_in_all_products' => [ + 'products_data' => ['simple1000' => 'Yes', 'simple1001' => 'Yes'], + 'expectation' => ['label' => 'Yes', 'count' => 0], + 'filter_value' => 'Yes', + 'products_count' => 2, + ], + 'selected_yes_option_in_one_product' => [ + 'products_data' => ['simple1000' => 'Yes', 'simple1001' => 'No'], + 'expectation' => ['label' => 'Yes', 'count' => 0], + 'filter_value' => 'Yes', + 'products_count' => 1, + ], + 'selected_no_option_in_all_products' => [ + 'products_data' => ['simple1000' => 'No', 'simple1001' => 'No'], + 'expectation' => ['label' => 'No', 'count' => 0], + 'filter_value' => 'No', + 'products_count' => 2, + ], + 'selected_no_option_in_one_product' => [ + 'products_data' => ['simple1000' => 'Yes', 'simple1001' => 'No'], + 'expectation' => ['label' => 'No', 'count' => 0], + 'filter_value' => 'No', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php index f8391c60a30cf..abc8fa9201eba 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php @@ -7,9 +7,10 @@ namespace Magento\LayeredNavigation\Block\Navigation\Category; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; use Magento\Catalog\Model\Layer\Resolver; use Magento\LayeredNavigation\Block\Navigation\AbstractFiltersTest; -use Magento\Catalog\Model\Layer\Filter\AbstractFilter; +use Magento\Store\Model\Store; /** * Provides tests for custom multiselect filter in navigation block on category page. @@ -72,6 +73,58 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/multiselect_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 1'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_two_options' => [ + 'products_data' => ['simple1000' => 'Option 1,Option 2', 'simple1001' => 'Option 1,Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 2, + ], + 'filter_by_second_option_in_products_with_hybrid_options' => [ + 'products_data' => ['simple1000' => 'Option 1,Option 2', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 2, + ], + ]; + } + /** * @inheritdoc */ @@ -87,4 +140,26 @@ protected function getAttributeCode(): string { return 'multiselect_attribute'; } + + /** + * @inheritdoc + */ + protected function updateProducts( + array $products, + string $attributeCode, + int $storeId = Store::DEFAULT_STORE_ID + ): void { + $attribute = $this->attributeRepository->get($attributeCode); + + foreach ($products as $productSku => $stringValue) { + $product = $this->productRepository->get($productSku, false, $storeId, true); + $values = explode(',', $stringValue); + $productValue = []; + foreach ($values as $value) { + $productValue[] = $attribute->usesSource() ? $attribute->getSource()->getOptionId($value) : $value; + } + $product->addData([$attribute->getAttributeCode() => implode(',', $productValue)]); + $this->productRepository->save($product); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php index 97928463620f4..db3bcb10c8364 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php @@ -14,7 +14,6 @@ use Magento\Catalog\Model\Layer\Filter\AbstractFilter; use Magento\Catalog\Model\Layer\Filter\Item; use Magento\Store\Model\ScopeInterface as StoreScope; -use Magento\Store\Model\Store; /** * Provides price filter tests with different price ranges calculation in navigation block on category page. @@ -163,6 +162,56 @@ public function getFiltersDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @dataProvider getActiveFiltersDataProvider + * @param array $config + * @param array $products + * @param array $expectation + * @param string $filterValue + * @return void + */ + public function testGetActiveFilters(array $config, array $products, array $expectation, string $filterValue): void + { + $this->applyCatalogConfig($config); + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, 1); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getActiveFiltersDataProvider(): array + { + return [ + 'auto_calculation' => [ + 'config' => ['catalog/layered_navigation/price_range_calculation' => 'auto'], + 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], + 'expectation' => ['label' => '$10.00 - $19.99', 'count' => 0], + 'filter_value' => '10-20', + ], + 'improved_calculation' => [ + 'config' => [ + 'catalog/layered_navigation/price_range_calculation' => 'improved', + 'catalog/layered_navigation/interval_division_limit' => 3, + ], + 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], + 'expectation' => ['label' => '$0.00 - $19.99', 'count' => 0], + 'filter_value' => '0-20', + ], + 'manual_calculation' => [ + 'config' => [ + 'catalog/layered_navigation/price_range_calculation' => 'manual', + 'catalog/layered_navigation/price_range_step' => 10, + 'catalog/layered_navigation/price_range_max_intervals' => 10, + ], + 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 30.00], + 'expectation' => ['label' => '$10.00 - $19.99', 'count' => 0], + 'filter_value' => '10-20', + ], + ]; + } + /** * @inheritdoc */ @@ -209,4 +258,17 @@ protected function applyCatalogConfig(array $config): void $this->scopeConfig->setValue($path, $value, StoreScope::SCOPE_STORE, ScopeInterface::SCOPE_DEFAULT); } } + + /** + * @inheritdoc + */ + protected function assertActiveFilter(array $expectation, Item $currentFilter): void + { + $this->assertEquals($expectation['label'], strip_tags((string)$currentFilter->getData('label'))); + $this->assertEquals($expectation['count'], $currentFilter->getData('count')); + $this->assertEquals( + $this->getAttributeCode(), + $currentFilter->getFilter()->getData('attribute_model')->getAttributeCode() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php index e2278239be242..014438b4906bd 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php @@ -71,6 +71,52 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 1'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php index 8f03ae3eed229..664b54ed92161 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php @@ -71,6 +71,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_boolean_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php index 9220a81f507ee..1d5d57fcddddd 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php @@ -74,6 +74,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Catalog/_files/multiselect_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php index d9ac02b2bff11..6f7d040d1ad9c 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php @@ -41,6 +41,21 @@ public function testGetFilters(array $config, array $products, array $expectatio ); } + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @dataProvider getActiveFiltersDataProvider + * @param array $config + * @param array $products + * @param array $expectation + * @param string $filterValue + * @return void + */ + public function testGetActiveFilters(array $config, array $products, array $expectation, string $filterValue): void + { + $this->applyCatalogConfig($config); + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, 1); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php index d44994de7e31c..2133ab445b8f9 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php @@ -72,6 +72,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php index 62dae6ba1c5e9..ac8aff07cb811 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php @@ -116,6 +116,7 @@ public function testExecuteWithZeroByteImage() * * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php * @magentoDataFixture Magento/MediaStorage/_files/product_with_missed_image.php + * @magentoDbIsolation disabled */ public function testDatabaseStorageMissingFile() { diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php index 056ba4ae93cc6..c96e3213d7a06 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php @@ -52,7 +52,7 @@ protected function setUp(): void /** * test for \Magento\MediaStorage\Model\File\Storage\Database::deleteFolder() * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php * @magentoConfigFixture current_store system/media_storage_configuration/media_storage 1 * @magentoConfigFixture current_store system/media_storage_configuration/media_database default_setup diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index dca0ef14663f4..5c16f9fb58a4d 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -3,31 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\MessageQueue\Model\Cron; +use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\Lock\Backend\Database; -use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; -use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\ShellInterface; use Magento\Framework\Filesystem; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Lock\Backend\Database; +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\ShellInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * Tests the different cases of consumers running by ConsumersRunner * - * {@inheritdoc} * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase +class ConsumersRunnerTest extends TestCase { /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -69,7 +74,7 @@ class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase private $appConfig; /** - * @var ShellInterface|\PHPUnit\Framework\MockObject\MockObject + * @var ShellInterface|MockObject */ private $shellMock; @@ -83,7 +88,7 @@ class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->shellMock = $this->getMockBuilder(ShellInterface::class) ->getMockForAbstractClass(); $resourceConnection = $this->objectManager->create(ResourceConnection::class); @@ -106,7 +111,7 @@ protected function setUp(): void ->willReturnCallback( function ($command, $arguments) { $command = vsprintf($command, $arguments); - $params = \Magento\TestFramework\Helper\Bootstrap::getInstance()->getAppInitParams(); + $params = Bootstrap::getInstance()->getAppInitParams(); $params['MAGE_DIRS']['base']['path'] = BP; $params = 'INTEGRATION_TEST_PARAMS="' . urldecode(http_build_query($params)) . '"'; $command = str_replace('bin/magento', 'dev/tests/integration/bin/magento', $command); @@ -117,6 +122,52 @@ function ($command, $arguments) { ); } + /** + * @param string $specificConsumer + * @param int $maxMessage + * @param string $command + * @param array $expectedArguments + * + * @return void + * @dataProvider runDataProvider + */ + public function testArgumentMaxMessages( + string $specificConsumer, + int $maxMessage, + string $command, + array $expectedArguments + ) { + $config = $this->config; + $config['cron_consumers_runner'] = ['consumers' => [$specificConsumer], 'max_messages' => $maxMessage]; + $this->writeConfig($config); + $this->shellMock->expects($this->any()) + ->method('execute') + ->with($command, $expectedArguments); + + $this->consumersRunner->run(); + } + + /** + * @return array + */ + public function runDataProvider() + { + return [ + [ + 'specificConsumer' => 'exportProcessor', + 'max_messages' => 10, + 'command' => PHP_BINARY . ' ' . BP . '/bin/magento queue:consumers:start %s %s %s', + 'expectedArguments' => ['exportProcessor', '--single-thread', '--max-messages=10'], + ], + [ + 'specificConsumer' => 'exportProcessor', + 'max_messages' => 5000, + 'command' => PHP_BINARY . ' ' . BP . '/bin/magento queue:consumers:start %s %s %s', + 'expectedArguments' => ['exportProcessor', '--single-thread', '--max-messages=100'], + ], + ]; + } + /** * Tests running of specific consumer and his re-running when it is working * diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php index 3aadad7e9ebec..de6501ee78986 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ProductRepository; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\TestCase; @@ -60,11 +61,13 @@ public function testMarkQuoteRecollectAfterChangeProductPrice(): void $product->setPrice((float)$product->getPrice() + 10); $this->productRepository->save($product); + /** @var QuoteResource $quoteResource */ + $quoteResource = $quote->getResource(); /** @var AdapterInterface $connection */ - $connection = $quote->getResource()->getConnection(); + $connection = $quoteResource->getConnection(); $select = $connection->select() ->from( - $connection->getTableName('quote'), + $quoteResource->getTable('quote'), ['updated_at', 'trigger_recollect'] )->where( "reserved_order_id = 'test_order_with_simple_product_without_address'" diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php index facb4879650b1..26ae82120b2c7 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php @@ -10,10 +10,15 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Type; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Vat; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\StateException; use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver; +use Magento\Quote\Observer\Frontend\Quote\Address\VatValidator; use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Store\Model\StoreManagerInterface; @@ -21,6 +26,7 @@ use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * Class for testing QuoteManagement model @@ -106,6 +112,28 @@ public function testSubmit(): void } } + /** + * Verify guest customer place order with auto-group assigment. + * + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture default_store customer/create_account/tax_calculation_address_type shipping + * @magentoConfigFixture default_store customer/create_account/viv_intra_union_group 2 + * @magentoConfigFixture default_store customer/create_account/viv_on_each_transaction 1 + * + * @return void + */ + public function testSubmitGuestCustomer(): void + { + $this->mockVatValidation(); + $quote = $this->getQuoteByReservedOrderId->execute('guest_quote'); + $this->cartManagement->placeOrder($quote->getId()); + $quoteAfterOrderPlaced = $this->getQuoteByReservedOrderId->execute('guest_quote'); + self::assertEquals(2, $quoteAfterOrderPlaced->getCustomerGroupId()); + self::assertEquals(3, $quoteAfterOrderPlaced->getCustomerTaxClassId()); + } + /** * Tries to create order with product that has child items and one of them was deleted. * @@ -231,4 +259,33 @@ private function makeProductOutOfStock(string $sku): void $stockItem->setIsInStock(false); $this->productRepository->save($product); } + + /** + * Makes customer vat validator 'check vat number' response successful. + * + * @return void + */ + private function mockVatValidation(): void + { + $vatMock = $this->getMockBuilder(Vat::class) + ->setConstructorArgs( + [ + 'scopeConfig' => $this->objectManager->get(ScopeConfigInterface::class), + 'logger' => $this->objectManager->get(LoggerInterface::class), + ] + ) + ->onlyMethods(['checkVatNumber']) + ->getMock(); + $gatewayResponse = new DataObject([ + 'is_valid' => true, + 'request_date' => 'testData', + 'request_identifier' => 'testRequestIdentifier', + 'request_success' => true, + ]); + $vatMock->method('checkVatNumber')->willReturn($gatewayResponse); + $this->objectManager->removeSharedInstance(CollectTotalsObserver::class); + $this->objectManager->removeSharedInstance(VatValidator::class); + $this->objectManager->removeSharedInstance(Vat::class); + $this->objectManager->addSharedInstance($vatMock, Vat::class); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php index 0a8db20d86966..493bf7ec37ec3 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php @@ -10,7 +10,10 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\App\Config; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -46,6 +49,7 @@ protected function setUp(): void $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Form::class); $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); $this->registry = $this->objectManager->get(Registry::class); + $this->objectManager->removeSharedInstance(Config::class); } /** @@ -65,11 +69,86 @@ protected function tearDown(): void */ public function testGetFormValues(): void { - $this->registry->unregister('order_address'); - $order = $this->orderFactory->create()->loadByIncrementId(100000001); - $address = $order->getShippingAddress(); - $this->registry->register('order_address', $address); + $address = $this->getOrderAddress('100000001'); + $this->registerOrderAddress($address); $formValues = $this->block->getFormValues(); $this->assertEquals($address->getData(), $formValues); } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoConfigFixture default_store general/country/default US + * @magentoConfigFixture default_store general/country/allow US + * @magentoConfigFixture fixture_second_store_store general/country/default UY + * @magentoConfigFixture fixture_second_store_store general/country/allow UY + * @return void + */ + public function testCountryIdInAllowedList(): void + { + $address = $this->getOrderAddress('100000001'); + $this->registerOrderAddress($address); + $this->assertEquals('US', $address->getCountryId()); + $this->assertCountryField('US'); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoConfigFixture default_store general/country/default CA + * @magentoConfigFixture default_store general/country/allow CA + * @magentoConfigFixture fixture_second_store_store general/country/default UY + * @magentoConfigFixture fixture_second_store_store general/country/allow UY + * @return void + */ + public function testCountryIdInNotAllowedList(): void + { + $address = $this->getOrderAddress('100000001'); + $this->registerOrderAddress($address); + $this->assertCountryField('CA'); + } + + /** + * Prepares address edit from block. + * + * @param OrderAddressInterface $address + * @return void + */ + private function registerOrderAddress(OrderAddressInterface $address): void + { + $this->registry->unregister('order_address'); + $this->registry->register('order_address', $address); + } + + /** + * Return order billing address. + * + * @param string $orderIncrementId + * @return OrderAddressInterface + */ + private function getOrderAddress(string $orderIncrementId): OrderAddressInterface + { + /** @var OrderInterface $order */ + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + + return $order->getBillingAddress(); + } + + /** + * Asserts country field data. + * + * @param string $countryCode + * @return void + */ + private function assertCountryField(string $countryCode): void + { + $countryIdField = $this->block->getForm()->getElement('country_id'); + $this->assertEquals($countryCode, $countryIdField->getValue()); + $options = $countryIdField->getValues(); + $this->assertCount(1, $options); + $firstOption = reset($options); + $this->assertEquals($countryCode, $firstOption['value']); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php new file mode 100644 index 0000000000000..60313c4f45a9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php @@ -0,0 +1,114 @@ +creditmemoCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportCreditmemoDataProvider + * @param string $format + * @param bool $addIdToUrl + * @param string $namespace + * @return void + */ + public function testExportCreditmemo( + string $format, + bool $addIdToUrl, + string $namespace + ): void { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, $addIdToUrl ? (int)$order->getId() : null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] + ); + $creditmemos = $this->parseResponse($format, $response); + $creditmemo = $this->getCreditmemo('200000001'); + $exportedCreditmemo = reset($creditmemos); + $this->assertNotFalse($exportedCreditmemo); + $this->assertEquals( + $this->prepareDate($creditmemo->getCreatedAt(), 'America/Chicago'), + $exportedCreditmemo['Created'] + ); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedCreditmemo['Order Date'] + ); + } + + /** + * @return array + */ + public function exportCreditmemoDataProvider(): array + { + return [ + 'creditmemo_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_creditmemo_grid', + ], + 'creditmemo_grid_in_csv_from_order_view' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_creditmemo_grid', + ], + 'creditmemo_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_creditmemo_grid', + ], + 'creditmemo_grid_in_xml_from_order_view' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_creditmemo_grid', + ], + ]; + } + + /** + * Returns creditmemo by increment id. + * + * @param string $incrementId + * @return CreditmemoInterface + */ + private function getCreditmemo(string $incrementId): CreditmemoInterface + { + /** @var CreditmemoInterface $creditmemo */ + $creditmemo = $this->creditmemoCollectionFactory->create() + ->addAttributeToFilter(CreditmemoInterface::INCREMENT_ID, $incrementId) + ->getFirstItem(); + + return $creditmemo; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php new file mode 100644 index 0000000000000..271a99a8037ca --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php @@ -0,0 +1,162 @@ +orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + } + + /** + * Dispatches export request. + * + * @param string $url + * @param array $params + * @return string + */ + protected function dispatchExport(string $url, array $params): string + { + $this->_auth->getAuthStorage()->setIsFirstPageAfterLogin(false); + $this->getRequest()->setParams($params); + $this->getRequest()->setMethod(Http::METHOD_POST); + ob_start(); + $this->dispatch($url); + + return ob_get_clean(); + } + + /** + * Parses string response depends of format. + * + * @param string $format + * @param string $response + * @return array + */ + protected function parseResponse(string $format, string $response): array + { + $result = []; + if ($format === ExportBase::CSV_FORMAT) { + $result = $this->parseCsvResponse($response); + } elseif ($format === ExportBase::XML_FORMAT) { + $result = $this->parseXmlResponse($response); + } + + return $result; + } + + /** + * Converts string in scv format to assoc array. + * + * @param string $data + * @return array + */ + protected function parseCsvResponse(string $data): array + { + $result = []; + $data = str_getcsv($data, PHP_EOL); + $headers = str_getcsv(array_shift($data), ',', '"'); + foreach ($data as $row) { + $result[] = array_combine($headers, str_getcsv($row, ',', '"')); + } + + return $result; + } + + /** + * Converts string in xml format to assoc array. + * + * @param string $data + * @return array + */ + protected function parseXmlResponse(string $data): array + { + $xml = simplexml_load_string($data); + $xmlAsArray = []; + foreach ($xml->Worksheet->Table->Row as $item) { + $row = []; + foreach ($item->Cell as $cell) { + $data = (array)$cell->Data; + $row[] = reset($data); + } + $xmlAsArray[] = $row; + } + $result = []; + $headers = array_shift($xmlAsArray); + foreach ($xmlAsArray as $row) { + $result[] = array_combine($headers, $row); + } + + return $result; + } + + /** + * Returns order purchase date in timezone. + * + * @param string $date + * @param string $timezone + * @return string + */ + protected function prepareDate(string $date, string $timezone): string + { + $date = new \DateTime($date, new \DateTimeZone('UTC')); + $date->setTimezone(new \DateTimeZone($timezone)); + + return $date->format('M j, Y h:i:s A'); + } + + /** + * Returns order by increment id. + * + * @param string $incrementId + * @return OrderInterface + */ + protected function getOrder(string $incrementId): OrderInterface + { + return $this->orderFactory->create()->loadByIncrementId($incrementId); + } + + /** + * Returns export url. + * + * @param string $format + * @param int|null $orderId + * @return string + */ + protected function getExportUrl(string $format, ?int $orderId = null): string + { + $url = $format === self::CSV_FORMAT + ? 'backend/mui/export/gridToCsv/' + : 'backend/mui/export/gridToXml/'; + + return $orderId ? $url . 'order_id/' . $orderId : $url; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php new file mode 100644 index 0000000000000..c447568c4daf4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php @@ -0,0 +1,52 @@ +getOrder('200000001'); + $url = $this->getExportUrl($format, null); + $response = $this->dispatchExport( + $url, + ['namespace' => 'sales_order_grid', 'filters' => ['increment_id' => '200000001']] + ); + $orders = $this->parseResponse($format, $response); + $exportedOrder = reset($orders); + $this->assertNotFalse($exportedOrder); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedOrder['Purchase Date'] + ); + } + + /** + * @return array + */ + public function exportOrderDataProvider(): array + { + return [ + 'order_grid_in_csv' => ['format' => ExportBase::CSV_FORMAT], + 'order_grid_in_xml' => ['format' => ExportBase::XML_FORMAT], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php new file mode 100644 index 0000000000000..eb1cd59dde632 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php @@ -0,0 +1,109 @@ +invoiceFactory = $this->_objectManager->get(InvoiceInterfaceFactory::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportInvoiceDataProvider + * @param string $format + * @param bool $addIdToUrl + * @param string $namespace + * @return void + */ + public function testExportInvoice( + string $format, + bool $addIdToUrl, + string $namespace + ): void { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, $addIdToUrl ? (int)$order->getId() : null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] + ); + $invoices = $this->parseResponse($format, $response); + $invoice = $this->getInvoice('200000001'); + $exportedInvoice = reset($invoices); + $this->assertNotFalse($exportedInvoice); + $this->assertEquals( + $this->prepareDate($invoice->getCreatedAt(), 'America/Chicago'), + $exportedInvoice['Invoice Date'] + ); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedInvoice['Order Date'] + ); + } + + /** + * @return array + */ + public function exportInvoiceDataProvider(): array + { + return [ + 'invoice_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_invoice_grid', + ], + 'invoice_grid_in_csv_from_order_view' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_invoice_grid', + ], + 'invoice_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_invoice_grid', + ], + 'invoice_grid_in_xml_from_order_view' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_invoice_grid', + ], + ]; + } + + /** + * Returns invoice by increment id. + * + * @param string $incrementId + * @return InvoiceInterface + */ + private function getInvoice(string $incrementId): InvoiceInterface + { + return $this->invoiceFactory->create()->loadByIncrementId($incrementId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php new file mode 100644 index 0000000000000..d27fe0821c047 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php @@ -0,0 +1,109 @@ +shipmentFactory = $this->_objectManager->get(ShipmentInterfaceFactory::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportShipmentDataProvider + * @param string $format + * @param bool $addIdToUrl + * @param string $namespace + * @return void + */ + public function testExportShipment( + string $format, + bool $addIdToUrl, + string $namespace + ): void { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, $addIdToUrl ? (int)$order->getId() : null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] + ); + $shipments = $this->parseResponse($format, $response); + $shipment = $this->getShipment('200000001'); + $exportedShipment = reset($shipments); + $this->assertNotFalse($exportedShipment); + $this->assertEquals( + $this->prepareDate($shipment->getCreatedAt(), 'America/Chicago'), + $exportedShipment['Ship Date'] + ); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedShipment['Order Date'] + ); + } + + /** + * @return array + */ + public function exportShipmentDataProvider(): array + { + return [ + 'shipment_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_shipment_grid', + ], + 'shipment_grid_in_csv_from_order_view' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_shipment_grid', + ], + 'shipment_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_shipment_grid', + ], + 'shipment_grid_in_xml_from_order_view' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_shipment_grid', + ], + ]; + } + + /** + * Returns shipment by increment id. + * + * @param string $incrementId + * @return ShipmentInterface + */ + private function getShipment(string $incrementId): ShipmentInterface + { + return $this->shipmentFactory->create()->loadByIncrementId($incrementId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php index 3b32e7238cc76..6a508e9a95eec 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php @@ -136,6 +136,33 @@ public function testReorderByAnotherCustomer(): void } } + /** + * Reorder with JS calendar options + * + * @magentoDataFixture Magento/Sales/_files/order_with_js_date_option_product.php + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * + * @return void + */ + public function testReorderWithJSCalendar(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $items = $order->getItems(); + $orderItem = array_pop($items); + $orderRequestOptions = $orderItem->getProductOptionByCode('info_buyRequest')['options']; + $order->save(); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('checkout/cart')); + $this->quote = $this->checkoutSession->getQuote(); + $quoteItemsCollection = $this->quote->getItemsCollection(); + $this->assertCount(1, $quoteItemsCollection); + $items = $quoteItemsCollection->getItems(); + $quoteItem = array_pop($items); + $quoteRequestOptions = $quoteItem->getBuyRequest()->getOptions(); + $this->assertEquals($orderRequestOptions, $quoteRequestOptions); + } + /** * Dispatch reorder request. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php index 4e33dc398d7ad..6924e2db016dd 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php @@ -7,97 +7,107 @@ namespace Magento\Sales\Model; -use Magento\Config\Model\Config; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Store\Model\ScopeInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceSearchResultInterface; +use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Spi\InvoiceResourceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\TestCase; -class InvoiceEmailSenderHandlerTest extends \PHPUnit\Framework\TestCase +/** + * Checks sending emails to customers after creation/modification of invoice. + * + * @see \Magento\Sales\Model\EmailSenderHandler + */ +class InvoiceEmailSenderHandlerTest extends TestCase { - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection - */ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var InvoiceSearchResultInterface */ private $entityCollection; + /** @var EmailSenderHandler */ + private $emailSenderHandler; + + /** @var InvoiceIdentity */ + private $invoiceIdentity; + + /** @var InvoiceSender */ + private $invoiceSender; + + /** @var InvoiceResourceInterface */ + private $entityResource; + + /** @var TransportBuilderMock */ + private $transportBuilderMock; + /** - * @var \Magento\Sales\Model\EmailSenderHandler + * @inheritdoc */ - private $emailSender; - protected function setUp(): void { - /** @var \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity $invoiceIdentity */ - $invoiceIdentity = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity::class - ); - /** @var \Magento\Sales\Model\Order\Email\Sender\InvoiceSender $invoiceSender */ - $invoiceSender = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create( - \Magento\Sales\Model\Order\Email\Sender\InvoiceSender::class, - [ - 'identityContainer' => $invoiceIdentity, - ] - ); - $entityResource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Sales\Model\ResourceModel\Order\Invoice::class); - $this->entityCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection::class - ); - $this->emailSender = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\EmailSenderHandler::class, + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->invoiceIdentity = $this->objectManager->get(InvoiceIdentity::class); + $this->invoiceSender = $this->objectManager->get(InvoiceSender::class); + $this->entityResource = $this->objectManager->get(InvoiceResourceInterface::class); + $this->entityCollection = $this->objectManager->create(InvoiceSearchResultInterface::class); + $this->emailSenderHandler = $this->objectManager->create( + EmailSenderHandler::class, [ - 'emailSender' => $invoiceSender, - 'entityResource' => $entityResource, + 'emailSender' => $this->invoiceSender, + 'entityResource' => $this->entityResource, 'entityCollection' => $this->entityCollection, - 'identityContainer' => $invoiceIdentity, + 'identityContainer' => $this->invoiceIdentity, ] ); + $this->transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); } /** - * @magentoAppIsolation enabled - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/Sales/_files/invoice_list_different_stores.php + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Sales/_files/invoice_list_different_stores.php + * @magentoConfigFixture default/sales_email/general/async_sending 1 + * @magentoConfigFixture fixture_second_store_store sales_email/invoice/enabled 0 + * @return void */ - public function testInvoiceEmailSenderExecute() + public function testInvoiceEmailSenderExecute(): void { - $expectedResult = 1; - - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var Config $defConfig */ - $defConfig = $objectManager->create(Config::class); - $defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); - $defConfig->setDataByPath('sales_email/general/async_sending', 1); - $defConfig->save(); - - /** @var Config $storeConfig */ - $storeConfig = $objectManager->create(Config::class); - $storeConfig->setScope(ScopeInterface::SCOPE_STORES); - $storeConfig->setStore('fixture_second_store'); - $storeConfig->setDataByPath('sales_email/invoice/enabled', 0); - $storeConfig->save(); - - $sendCollection = clone $this->entityCollection; - $sendCollection->addFieldToFilter('send_email', ['eq' => 1]); - $sendCollection->addFieldToFilter('email_sent', ['null' => true]); - - $this->emailSender->sendEmails(); - - $this->assertCount($expectedResult, $sendCollection->getItems()); + $invoiceCollection = clone $this->entityCollection; + $invoiceCollection->addFieldToFilter('send_email', ['eq' => 1]); + $invoiceCollection->addFieldToFilter(InvoiceInterface::EMAIL_SENT, ['null' => true]); + $this->emailSenderHandler->sendEmails(); + $this->assertEquals(1, $invoiceCollection->getTotalCount()); } /** - * @inheritdoc - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Exception + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Sales/_files/invoice_with_send_email_flag.php + * @magentoConfigFixture default/sales_email/general/async_sending 1 + * @return void */ - protected function tearDown(): void + public function testSendEmailsCheckEmailReceived(): void { - /** @var \Magento\Config\Model\Config $defConfig */ - $defConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Config\Model\Config::class); - $defConfig->setScope(\Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT); - $defConfig->setDataByPath('sales_email/general/async_sending', 0); - $defConfig->save(); + $invoiceCollection = clone $this->entityCollection; + $this->emailSenderHandler->sendEmails(); + /** @var InvoiceInterface $invoice */ + $invoice = $invoiceCollection->getFirstItem(); + $this->assertNotNull($invoice->getId()); + $message = $this->transportBuilderMock->getSentMessage(); + $this->assertNotNull($message, 'The message is expected to be received'); + $subject = __('Invoice for your %1 order', $invoice->getStore()->getFrontendName())->render(); + $this->assertEquals($message->getSubject(), $subject); + $this->assertStringContainsString( + sprintf( + "Your Invoice #%s for Order #%s", + $invoice->getIncrementId(), + $invoice->getOrder()->getIncrementId() + ), + $message->getBody()->getParts()[0]->getRawContent() + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php index 55af8e9d2ee62..672709cbcd44b 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -8,61 +8,82 @@ namespace Magento\Sales\Model\Order\Email\Sender; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Model\ResourceModel\CustomerRepository; -use Magento\Sales\Model\Order; +use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; use Magento\Sales\Model\Order\Invoice; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\App\Area; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use PHPUnit\Framework\TestCase; +/** + * Checks the sending of order invoice email to the customer. + * + * @see \Magento\Sales\Model\Order\Email\Sender\InvoiceSender + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class InvoiceSenderTest extends TestCase { const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; const OLD_CUSTOMER_EMAIL = 'customer@null.com'; const ORDER_EMAIL = 'customer@null.com'; - /** - * @var CustomerRepository - */ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CustomerRepositoryInterface */ private $customerRepository; + /** @var InvoiceSender */ + private $invoiceSender; + + /** @var TransportBuilderMock */ + private $transportBuilderMock; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var InvoiceInterfaceFactory */ + private $invoiceFactory; + + /** @var InvoiceIdentity */ + private $invoiceIdentity; + /** - * @inheritDoc + * @inheritdoc */ protected function setUp(): void { parent::setUp(); - $this->customerRepository = Bootstrap::getObjectManager() - ->get(CustomerRepositoryInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->invoiceSender = $this->objectManager->get(InvoiceSender::class); + $this->transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->invoiceFactory = $this->objectManager->get(InvoiceInterfaceFactory::class); + $this->invoiceIdentity = $this->objectManager->get(InvoiceIdentity::class); } /** * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + * @return void */ - public function testSend() + public function testSend(): void { - Bootstrap::getInstance() - ->loadArea(Area::AREA_FRONTEND); - $order = Bootstrap::getObjectManager() - ->create(Order::class); - $order->loadByIncrementId('100000001'); + $order = $this->getOrder('100000001'); $order->setCustomerEmail('customer@example.com'); - - $invoice = Bootstrap::getObjectManager()->create( - Invoice::class - ); - $invoice->setOrder($order); + $invoice = $this->createInvoice($order); $invoice->setTotalQty(1); $invoice->setBaseSubtotal(50); $invoice->setBaseTaxAmount(10); $invoice->setBaseShippingAmount(5); - /** @var InvoiceSender $invoiceSender */ - $invoiceSender = Bootstrap::getObjectManager() - ->create(InvoiceSender::class); $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); @@ -76,22 +97,20 @@ public function testSend() * * @magentoDataFixture Magento/Sales/_files/order_with_customer.php * @magentoAppArea frontend + * @return void */ - public function testSendWhenCustomerEmailWasModified() + public function testSendWhenCustomerEmailWasModified(): void { $customer = $this->customerRepository->getById(1); $customer->setEmail(self::NEW_CUSTOMER_EMAIL); $this->customerRepository->save($customer); - - $order = $this->createOrder(); + $order = $this->getOrder('100000001'); $invoice = $this->createInvoice($order); - $invoiceIdentity = $this->createInvoiceEntity(); - $invoiceSender = $this->createInvoiceSender($invoiceIdentity); $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); - $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } @@ -101,18 +120,17 @@ public function testSendWhenCustomerEmailWasModified() * * @magentoDataFixture Magento/Sales/_files/order_with_customer.php * @magentoAppArea frontend + * @return void */ - public function testSendWhenCustomerEmailWasNotModified() + public function testSendWhenCustomerEmailWasNotModified(): void { - $order = $this->createOrder(); + $order = $this->getOrder('100000001'); $invoice = $this->createInvoice($order); - $invoiceIdentity = $this->createInvoiceEntity(); - $invoiceSender = $this->createInvoiceSender($invoiceIdentity); $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); - $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } @@ -122,59 +140,67 @@ public function testSendWhenCustomerEmailWasNotModified() * * @magentoDataFixture Magento/Sales/_files/order.php * @magentoAppArea frontend + * @return void */ - public function testSendWithoutCustomer() + public function testSendWithoutCustomer(): void { - $order = $this->createOrder(); + $order = $this->getOrder('100000001'); $invoice = $this->createInvoice($order); - /** @var InvoiceIdentity $invoiceIdentity */ - $invoiceIdentity = $this->createInvoiceEntity(); - /** @var InvoiceSender $invoiceSender */ - $invoiceSender = $this->createInvoiceSender($invoiceIdentity); - $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); - $this->assertEquals(self::ORDER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertEquals(self::ORDER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } - private function createInvoice(Order $order): Invoice + /** + * @magentoDataFixture Magento/Sales/_files/invoice.php + * @magentoConfigFixture default/sales_email/general/async_sending 1 + * @return void + */ + public function testSendWithAsyncSendingEnabled(): void { - $invoice = Bootstrap::getObjectManager()->create( - Invoice::class + $order = $this->getOrder('100000001'); + /** @var Invoice $invoice */ + $invoice = $order->getInvoiceCollection() + ->addAttributeToFilter(InvoiceInterface::ORDER_ID, $order->getID()) + ->getFirstItem(); + $result = $this->invoiceSender->send($invoice); + $this->assertFalse($result); + $invoice = $order->getInvoiceCollection()->clear()->getFirstItem(); + $this->assertEmpty($invoice->getEmailSent()); + $this->assertEquals('1', $invoice->getSendEmail()); + $this->assertNull( + $this->transportBuilderMock->getSentMessage(), + 'The message is not expected to be received.' ); - $invoice->setOrder($order); - - return $invoice; } - private function createOrder(): Order + /** + * Create invoice and set order + * + * @param OrderInterface $order + * @return InvoiceInterface + */ + private function createInvoice(OrderInterface $order): InvoiceInterface { - $order = Bootstrap::getObjectManager() - ->create(Order::class); - $order->loadByIncrementId('100000001'); - - return $order; - } + /** @var Invoice $invoice */ + $invoice = $this->invoiceFactory->create(); + $invoice->setOrder($order); - private function createInvoiceEntity(): InvoiceIdentity - { - return Bootstrap::getObjectManager()->create( - InvoiceIdentity::class - ); + return $invoice; } - private function createInvoiceSender(InvoiceIdentity $invoiceIdentity): InvoiceSender + /** + * Get order by increment_id + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrder(string $incrementId): OrderInterface { - return Bootstrap::getObjectManager() - ->create( - InvoiceSender::class, - [ - 'identityContainer' => $invoiceIdentity, - ] - ); + return $this->orderFactory->create()->loadByIncrementId($incrementId); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php index f6242432e2791..fabd2ef0021de 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php @@ -5,26 +5,35 @@ */ namespace Magento\Sales\Model\ResourceModel\Report\Shipping\Collection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Reports\Model\Item; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Grid\Collection as ShipmentGridCollection; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * Integration tests for shipments reports collection which is used to obtain shipment reports by shipment date. */ -class ShipmentTest extends \PHPUnit\Framework\TestCase +class ShipmentTest extends TestCase { /** - * @var \Magento\Sales\Model\ResourceModel\Report\Shipping\Collection\Shipment + * @var Shipment */ private $collection; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->collection = $this->objectManager->create( - \Magento\Sales\Model\ResourceModel\Report\Shipping\Collection\Shipment::class + Shipment::class ); $this->collection->setPeriod('day') ->setDateRange(null, null) @@ -43,11 +52,11 @@ public function testGetItems() $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); $order->loadByIncrementId('100000001'); $shipmentCreatedAt = $order->getShipmentsCollection()->getFirstItem()->getCreatedAt(); - /** @var \Magento\Framework\Stdlib\DateTime\DateTime $dateTime */ - $dateTime = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime\DateTimeFactory::class) + /** @var DateTime $dateTime */ + $dateTime = $this->objectManager->create(DateTimeFactory::class) ->create(); - /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone */ - $timezone = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); + /** @var TimezoneInterface $timezone */ + $timezone = $this->objectManager->create(TimezoneInterface::class); $shipmentCreatedAt = $timezone->formatDateTime( $shipmentCreatedAt, \IntlDateFormatter::SHORT, @@ -67,10 +76,44 @@ public function testGetItems() ], ]; $actualResult = []; - /** @var \Magento\Reports\Model\Item $reportItem */ + /** @var Item $reportItem */ foreach ($this->collection->getItems() as $reportItem) { $actualResult[] = array_intersect_key($reportItem->getData(), $expectedResult[0]); } $this->assertEquals($expectedResult, $actualResult); } + + /** + * Checks that order_created_at field does not change after sales_shipment_grid row update + * + * @magentoDataFixture Magento/Sales/_files/order_shipping.php + * @return void + */ + public function testOrderShipmentGridOrderCreatedAt(): void + { + $incrementId = '100000001'; + /** @var \Magento\Sales\Model\Order $order */ + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId($incrementId); + /** @var ShipmentGridCollection $grid */ + $grid = $this->objectManager->get(ShipmentGridCollection::class); + $grid->getSelect() + ->where('order_increment_id', $incrementId); + $itemId = $grid->getFirstItem() + ->getEntityId(); + $connection = $grid->getResource() + ->getConnection(); + $tableName = $grid->getMainTable(); + $connection->update( + $tableName, + ['customer_name' => 'Test'], + $connection->quoteInto('entity_id = ?', $itemId) + ); + $updatedRow = $connection->select() + ->where('entity_id = ?', $itemId) + ->from($tableName, ['order_created_at']); + $orderCreatedAt = $connection->fetchOne($updatedRow); + + $this->assertEquals($order->getCreatedAt(), $orderCreatedAt); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php index d613b60c1d52f..019b3114e04c8 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php @@ -40,14 +40,14 @@ $addressData = [ 'telephone' => 3234676, 'postcode' => 47676, - 'country_id' => 'US', + 'country_id' => 'DE', 'city' => 'CityX', 'street' => ['Black str, 48'], 'lastname' => 'Smith', 'firstname' => 'John', + 'vat_id' => 12345, 'address_type' => 'shipping', 'email' => 'some_email@mail.com', - 'region_id' => 1, ]; $billingAddress = $objectManager->create( @@ -66,6 +66,7 @@ $quote->setCustomerIsGuest(true) ->setStoreId($store->getId()) ->setReservedOrderId('guest_quote') + ->setCheckoutMethod('guest') ->setBillingAddress($billingAddress) ->setShippingAddress($shippingAddress) ->addProduct($product); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag.php b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag.php new file mode 100644 index 0000000000000..c23f7b8cfd423 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag.php @@ -0,0 +1,32 @@ +requireDataFixture('Magento/Sales/_files/order.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); +/** @var InvoiceService $invoiceService */ +$invoiceService = $objectManager->get(InvoiceManagementInterface::class); +/** @var Transaction $transactionSave */ +$transactionSave = $objectManager->get(Transaction::class); +/** @var Order $order */ +$order = $orderFactory->create()->loadByIncrementId('100000001'); + +$invoice = $invoiceService->prepareInvoice($order); +$invoice->register(); +$invoice->setSendEmail(true); +$order->setIsInProcess(true); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag_rollback.php new file mode 100644 index 0000000000000..07d468289f5b4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag_rollback.php @@ -0,0 +1,10 @@ +requireDataFixture('Magento/Sales/_files/order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php new file mode 100644 index 0000000000000..06f8954456471 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php @@ -0,0 +1,129 @@ +requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var InvoiceManagementInterface $invoiceService */ +$invoiceService = $objectManager->get(InvoiceManagementInterface::class); +/** @var ShipmentFactory $shipmentFactory */ +$shipmentFactory = $objectManager->get(ShipmentFactory::class); +/** @var CreditmemoFactory $creditmemoFactory */ +$creditmemoFactory = $objectManager->get(CreditmemoFactory::class); +/** @var CreditmemoItemInterfaceFactory $creditmemoItemFactory */ +$creditmemoItemFactory = $objectManager->get(CreditmemoItemInterfaceFactory::class); +/** @var CreditmemoRepositoryInterface $creditmemoRepository */ +$creditmemoRepository = $objectManager->get(CreditmemoRepositoryInterface::class); +/** @var CreditmemoItemRepositoryInterface $creditmemoItemRepository */ +$creditmemoItemRepository = $objectManager->get(CreditmemoItemRepositoryInterface::class); +$addressData = include __DIR__ . '/address_data.php'; +$product = $productRepository->get('simple'); +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType(OrderAddress::TYPE_BILLING); +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType(OrderAddress::TYPE_SHIPPING); +/** @var OrderPaymentInterface $payment */ +$payment = $objectManager->create(OrderPaymentInterface::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation('metadata', ['type' => 'free', 'fraudulent' => false]); +/** @var OrderItemInterface $orderItem */ +$orderItem = $objectManager->get(OrderItemInterfaceFactory::class)->create(); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()) + ->setName('Test item'); +/** @var OrderInterface $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create(); +$order->setIncrementId('200000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($storeManager->getStore('fixture_second_store')->getId()) + ->addItem($orderItem) + ->setPayment($payment); +$orderRepository->save($order); +//Create invoice +$invoice = $invoiceService->prepareInvoice($order); +$invoice->register(); +$invoice->setIncrementId($order->getIncrementId()); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager->create(Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); +//Create shipment +$items = []; +foreach ($order->getItems() as $item) { + $items[$item->getId()] = $item->getQtyOrdered(); +} +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); +$shipment->register(); +$shipment->setIncrementId($order->getIncrementId()); +$transactionSave = $objectManager->create(Transaction::class); +$transactionSave->addObject($shipment)->addObject($order)->save(); +//Create credit memo +/** @var CreditmemoFactory $creditmemoFactory */ +$creditmemoFactory = $objectManager->get(CreditmemoFactory::class); +$creditmemo = $creditmemoFactory->createByOrder($order, $order->getData()); +$creditmemo->setOrder($order); +$creditmemo->setState(Creditmemo::STATE_OPEN); +$creditmemo->setIncrementId($order->getIncrementId()); +$creditmemoRepository->save($creditmemo); +$orderItem->setName('Test item') + ->setQtyRefunded(2) + ->setQtyInvoiced(2) + ->setOriginalPrice($product->getPrice()); +$creditItem = $creditmemoItemFactory->create(); +$creditItem->setCreditmemo($creditmemo) + ->setName('Creditmemo item') + ->setOrderItemId($orderItem->getId()) + ->setQty(2) + ->setPrice($product->getPrice()); +$creditmemoItemRepository->save($creditItem); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php new file mode 100644 index 0000000000000..9a5b889fc7143 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php @@ -0,0 +1,51 @@ +get(OrderRepositoryInterface::class); +/** @var InvoiceRepositoryInterface $invoiceRepository */ +$invoiceRepository = $objectManager->get(InvoiceRepositoryInterface::class); +/** @var ShipmentRepositoryInterface $shipmentRepository */ +$shipmentRepository = $objectManager->get(ShipmentRepositoryInterface::class); +/** @var CreditmemoRepositoryInterface $creditmemoRepository */ +$creditmemoRepository = $objectManager->get(CreditmemoRepositoryInterface::class); +/** @var OrderInterface $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('200000001'); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +foreach ($order->getInvoiceCollection() as $invoice) { + $invoiceRepository->delete($invoice); +} +foreach ($order->getShipmentsCollection() as $shipment) { + $shipmentRepository->delete($shipment); +} +foreach ($order->getCreditmemosCollection() as $creditMemo) { + $creditmemoRepository->delete($creditMemo); +} +$orderRepository->delete($order); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php new file mode 100644 index 0000000000000..bdf576cfb5182 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php @@ -0,0 +1,81 @@ +requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php new file mode 100644 index 0000000000000..0966f21645e3b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php @@ -0,0 +1,12 @@ +requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php index 173cdf8a64703..2531f9c60f070 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php @@ -78,6 +78,10 @@ public static function loadGetSynonymsForPhraseDataProvider() 'phrase' => 'schlicht', 'expectedResult' => [['schlicht', 'natürlich']] ], + 'withSlashInSearchPhrase' => [ + 'phrase' => 'orange hill/peak', + 'expectedResult' => [['orange', 'magento'], ['hill/peak']] + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php index 7013346fd76e2..6098883959dd3 100644 --- a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php @@ -13,7 +13,7 @@ use Magento\SendFriend\Helper\Data as SendFriendHelper; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Zend\Stdlib\Parameters; +use Laminas\Stdlib\Parameters; /** * Class checks send friend model behavior @@ -28,6 +28,9 @@ class SendFriendTest extends TestCase /** @var SendFriend */ private $sendFriend; + /** @var ResourceModel\SendFriend */ + private $sendFriendResource; + /** @var CookieManagerInterface */ private $cookieManager; @@ -43,6 +46,7 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->sendFriend = $this->objectManager->get(SendFriendFactory::class)->create(); + $this->sendFriendResource = $this->objectManager->get(ResourceModel\SendFriend::class); $this->cookieManager = $this->objectManager->get(CookieManagerInterface::class); $this->request = $this->objectManager->get(RequestInterface::class); } @@ -55,6 +59,7 @@ protected function setUp(): void * @param array $sender * @param array $recipients * @param string|bool $expectedResult + * * @return void */ public function testValidate(array $sender, array $recipients, $expectedResult): void @@ -185,22 +190,34 @@ public function testisExceedLimitByCookies(): void * @magentoDataFixture Magento/SendFriend/_files/sendfriend_log_record_half_hour_before.php * * @magentoDbIsolation disabled + * * @return void */ public function testisExceedLimitByIp(): void { - $this->markTestSkipped('Blocked by MC-31968'); + $remoteAddr = '127.0.0.1'; $parameters = $this->objectManager->create(Parameters::class); - $parameters->set('REMOTE_ADDR', '127.0.0.1'); + $parameters->set('REMOTE_ADDR', $remoteAddr); $this->request->setServer($parameters); $this->assertTrue($this->sendFriend->isExceedLimit()); + // Verify that ip is saved correctly as integer value + $this->assertEquals( + 1, + (int)$this->sendFriendResource->getSendCount( + null, + ip2long($remoteAddr), + time() - (60 * 60 * 24 * 365), + 1 + ) + ); } /** - * Check result + * Check test result * * @param array|bool $expectedResult * @param array|bool $result + * * @return void */ private function checkResult($expectedResult, $result): void @@ -217,6 +234,7 @@ private function checkResult($expectedResult, $result): void * * @param array $sender * @param array $recipients + * * @return void */ private function prepareData(array $sender, array $recipients): void diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php index ba61592903b4d..5a1348d9da49b 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php @@ -7,17 +7,100 @@ namespace Magento\Store\Controller\Store; +use Magento\Framework\Interception\InterceptorInterface; use Magento\Framework\Session\SidResolverInterface; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface; use Magento\TestFramework\TestCase\AbstractController; +use PHPUnit\Framework\MockObject\MockObject; /** * Test Redirect controller. * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea frontend */ class RedirectTest extends AbstractController { + /** + * @var RedirectDataPreprocessorInterface + */ + private $preprocessor; + /** + * @var MockObject + */ + private $preprocessorMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); + $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); + $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + if ($this->preprocessor) { + $this->_objectManager->addSharedInstance($this->preprocessor, $this->getClassName($this->preprocessor)); + } + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture web/url/use_store 0 + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + */ + public function testRedirectToSecondStoreOnAnotherUrl(): void + { + $data = ['key1' => 'value1', 'key2' => 1]; + $this->preprocessorMock->method('process') + ->willReturn($data); + $this->getRequest()->setParam(StoreResolver::PARAM_NAME, 'fixture_second_store'); + $this->getRequest()->setParam('___from_store', 'default'); + $this->dispatch('/stores/store/redirect'); + $header = $this->getResponse()->getHeader('Location'); + $this->assertNotEmpty($header); + $result = $header->getFieldValue(); + $this->assertStringStartsWith('http://second_store.test/', $result); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlParts = parse_url($result); + $this->assertStringEndsWith('stores/store/switch/', $urlParts['path']); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + parse_str($urlParts['query'], $params); + $this->assertTrue(!empty($params['time_stamp'])); + $this->assertTrue(!empty($params['signature'])); + $this->assertTrue(!empty($params['data'])); + $serializer = $this->_objectManager->get(RedirectDataSerializerInterface::class); + $this->assertEquals($data, $serializer->unserialize($params['data'])); + } + + /** + * Return class name of the given object + * + * @param mixed $instance + */ + private function getClassName($instance): string + { + if ($instance instanceof InterceptorInterface) { + $actionClass = get_parent_class($instance); + } else { + $actionClass = get_class($instance); + } + return $actionClass; + } + /** * Check that there's no SID in redirect URL. * @@ -35,6 +118,6 @@ public function testNoSid(): void $result = (string)$this->getResponse()->getHeader('location'); $this->assertNotEmpty($result); - $this->assertStringNotContainsString(SidResolverInterface::SESSION_ID_QUERY_PARAM .'=', $result); + $this->assertStringNotContainsString(SidResolverInterface::SESSION_ID_QUERY_PARAM . '=', $result); } } diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index bc8ca2ba07a80..e4d78de54d308 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -5,11 +5,143 @@ */ namespace Magento\Store\Controller\Store; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\Encryption\UrlCoder; +use Magento\Framework\Interception\InterceptorInterface; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\TestFramework\TestCase\AbstractController; +use PHPUnit\Framework\MockObject\MockObject; + /** * Test for store switch controller. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @magentoAppArea frontend */ -class SwitchActionTest extends \Magento\TestFramework\TestCase\AbstractController +class SwitchActionTest extends AbstractController { + /** + * @var RedirectDataPreprocessorInterface + */ + private $preprocessor; + /** + * @var MockObject + */ + private $preprocessorMock; + /** + * @var RedirectDataPostprocessorInterface + */ + private $postprocessor; + /** + * @var MockObject + */ + private $postprocessorMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); + $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); + $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); + + $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); + $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); + $this->_objectManager->addSharedInstance($this->postprocessorMock, $this->getClassName($this->postprocessor)); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + if ($this->preprocessor) { + $this->_objectManager->addSharedInstance($this->preprocessor, $this->getClassName($this->preprocessor)); + } + if ($this->postprocessor) { + $this->_objectManager->addSharedInstance($this->postprocessor, $this->getClassName($this->postprocessor)); + } + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture web/url/use_store 0 + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + */ + public function testSwitch() + { + $data = ['key1' => 'value1', 'key2' => 1]; + $this->preprocessorMock->method('process') + ->willReturn($data); + $this->postprocessorMock->expects($this->once()) + ->method('process') + ->with( + $this->callback( + function (ContextInterface $context) { + return $context->getFromStore()->getCode() === 'fixture_second_store' + && $context->getTargetStore()->getCode() === 'default' + && $context->getRedirectUrl() === 'http://localhost/index.php/'; + } + ), + $data + ); + $redirectDataGenerator = $this->_objectManager->get(RedirectDataGenerator::class); + $contextFactory = $this->_objectManager->get(ContextInterfaceFactory::class); + $storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $urlEncoder = $this->_objectManager->get(UrlCoder::class); + $fromStore = $storeManager->getStore('fixture_second_store'); + $targetStore = $storeManager->getStore('default'); + $redirectData = $redirectDataGenerator->generate( + $contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => '/', + ] + ) + ); + $this->getRequest()->setParams( + [ + '___from_store' => $fromStore->getCode(), + StoreResolverInterface::PARAM_NAME => $targetStore->getCode(), + ActionInterface::PARAM_NAME_URL_ENCODED => $urlEncoder->encode('/'), + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), + ] + ); + $this->dispatch('stores/store/switch'); + $this->assertRedirect($this->equalTo('http://localhost/index.php/')); + } + + /** + * Return class name of the given object + * + * @param mixed $instance + */ + private function getClassName($instance): string + { + if ($instance instanceof InterceptorInterface) { + $actionClass = get_parent_class($instance); + } else { + $actionClass = get_class($instance); + } + return $actionClass; + } + /** * Ensure that proper default store code is calculated. * @@ -41,10 +173,10 @@ public function testExecuteWithCustomDefaultStore() * @param string $from * @param string $to */ - protected function changeStoreCode($from, $to) + private function changeStoreCode($from, $to) { - /** @var \Magento\Store\Model\Store $store */ - $store = $this->_objectManager->create(\Magento\Store\Model\Store::class); + /** @var Store $store */ + $store = $this->_objectManager->create(Store::class); $store->load($from, 'code'); $store->setCode($to); $store->save(); diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php index a56c13ca92f2f..57d8e72712a61 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php @@ -71,6 +71,52 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_text_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 1'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php index 9860e5a78c436..a254d4953f2d3 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php @@ -71,6 +71,52 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_visual_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'option 1', 'simple1001' => 'option 1'], + 'expectation' => ['label' => 'option 1', 'count' => 0], + 'filter_value' => 'option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'option 1', 'simple1001' => 'option 2'], + 'expectation' => ['label' => 'option 1', 'count' => 0], + 'filter_value' => 'option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'option 1', 'simple1001' => 'option 2'], + 'expectation' => ['label' => 'option 2', 'count' => 0], + 'filter_value' => 'option 2', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php index 83867453a98ea..f38a22e5249e7 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php @@ -72,6 +72,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_text_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php index 47c7b09f2eb85..a82e637bc77fc 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php @@ -72,6 +72,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_visual_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php b/dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php new file mode 100644 index 0000000000000..779c9c955a62e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php @@ -0,0 +1,65 @@ +objectManager = Bootstrap::getObjectManager(); + $this->dateFactory = $this->objectManager->get(DateFactory::class); + $this->localeResolver = $this->objectManager->get(ResolverInterface::class); + } + + /** + * @dataProvider localeDataProvider + * + * @param string $locale + * @param string $dateFormat + * @return void + */ + public function testDateFormat(string $locale, string $dateFormat): void + { + $this->localeResolver->setLocale($locale); + $date = $this->dateFactory->create(); + $date->prepare(); + $this->assertEquals($dateFormat, $date->getData('config')['options']['dateFormat']); + } + + /** + * @return array + */ + public function localeDataProvider(): array + { + return [ + ['en_GB', 'dd/MM/y'], ['en_US', 'M/d/yy'], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php index 6b5829ee8bf28..2cda6710ce22b 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php @@ -3,40 +3,53 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main; +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Framework\Escaper; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Widget\Model\Widget\Instance; +use PHPUnit\Framework\TestCase; + /** * @magentoAppArea adminhtml */ -class LayoutTest extends \PHPUnit\Framework\TestCase +class LayoutTest extends TestCase { /** - * @var \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Layout */ - protected $_block; + private $block; + /** + * @inheritDoc + */ protected function setUp(): void { - parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); - $this->_block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock( - \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout::class, - '', - [ - 'data' => [ - 'widget_instance' => \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Widget\Model\Widget\Instance::class - ), + $this->block = $this->objectManager->get(LayoutInterface::class) + ->createBlock( + Layout::class, + '', + [ + 'data' => [ + 'widget_instance' => $this->objectManager->create(Instance::class), + ], ] - ] - ); - $this->_block->setLayout( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ) - ); + ); + $this->block->setLayout($this->objectManager->get(LayoutInterface::class)); } /** @@ -44,16 +57,12 @@ protected function setUp(): void */ public function testGetLayoutsChooser() { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\App\State::class - )->setAreaCode( - \Magento\Framework\App\Area::AREA_FRONTEND - ); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\DesignInterface::class - )->setDefaultDesignTheme(); + $this->objectManager->get(State::class) + ->setAreaCode(Area::AREA_FRONTEND); + $this->objectManager->get(DesignInterface::class) + ->setDefaultDesignTheme(); - $actualHtml = $this->_block->getLayoutsChooser(); + $actualHtml = $this->block->getLayoutsChooser(); $this->assertStringStartsWith('', $actualHtml); $this->assertStringContainsString('id="layout_handle"', $actualHtml); @@ -61,4 +70,28 @@ public function testGetLayoutsChooser() $this->assertGreaterThan(1, $optionCount, 'HTML select tag must provide options to choose from.'); $this->assertEquals($optionCount, substr_count($actualHtml, '')); } + + /** + * Check that escapeUrl called from template + * + * @return void + */ + public function testToHtml(): void + { + $escaperMock = $this->createMock(Escaper::class); + $this->objectManager->addSharedInstance($escaperMock, Escaper::class); + + $escaperMock->expects($this->atLeast(6)) + ->method('escapeUrl'); + + $this->block->toHtml(); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->objectManager->removeSharedInstance(Escaper::class); + } } diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js index 0071d5af7df4e..e4a2b95a4c975 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js @@ -14,6 +14,7 @@ define([ var formEl, jQueryAjax, order, + confirmSpy = jasmine.createSpy('confirm'), tmpl = '
' + '
' + '
' + @@ -129,7 +130,7 @@ define([ mocks = { 'jquery': $, 'Magento_Catalog/catalog/product/composite/configure': jasmine.createSpy(), - 'Magento_Ui/js/modal/confirm': jasmine.createSpy(), + 'Magento_Ui/js/modal/confirm': confirmSpy, 'Magento_Ui/js/modal/alert': jasmine.createSpy(), 'Magento_Ui/js/lib/view/utils/async': jasmine.createSpy() }; @@ -159,6 +160,22 @@ define([ jQueryAjax = undefined; }); + describe('Testing the process customer group change', function () { + it('and confirm method is called', function () { + init(); + spyOn(window, '$$').and.returnValue(['testing']); + order.processCustomerGroupChange( + 1, + 'testMsg', + 'customerGroupMsg', + 'errorMsg', + 1, + 'change' + ); + expect(confirmSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('submit()', function () { function testSubmit(currentPaymentMethod, paymentMethod, ajaxParams) { $.ajax = jasmine.createSpy('$.ajax'); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js index 46916054a29be..ba5ad61cfe310 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js @@ -358,6 +358,27 @@ define([ }); }); + describe('onFail handler', function () { + it('it logs responseText and status', function () { + var fakeEvent = { + target: document.createElement('input') + }, + data = { + jqXHR: { + responseText: 'Failed', + status: '500' + } + }; + + spyOn(console, 'error'); + + component.onFail(fakeEvent, data); + expect(console.error).toHaveBeenCalledWith(data.jqXHR.responseText); + expect(console.error).toHaveBeenCalledWith(data.jqXHR.status); + expect(console.error).toHaveBeenCalledTimes(2); + }); + }); + describe('aggregateError method', function () { it('should append onto aggregatedErrors array when called', function () { spyOn(component.aggregatedErrors, 'push'); diff --git a/dev/tools/grunt/configs/autoprefixer.json b/dev/tools/grunt/configs/autoprefixer.json index 28cd9c8a1255f..ea6301c2fd7ae 100644 --- a/dev/tools/grunt/configs/autoprefixer.json +++ b/dev/tools/grunt/configs/autoprefixer.json @@ -1,14 +1,11 @@ { - "options": { - "browsers": [ - "last 2 versions", - "ie 11" - ] - }, - "setup": { - "src": "<%= path.css.setup %>/setup.css" - }, - "updater": { - "src": "<%= path.css.updater %>/updater.css" - } -} \ No newline at end of file + "options": { + "browsers": ["last 2 versions", "ie 11"] + }, + "setup": { + "src": "<%= path.css.setup %>/setup.css" + }, + "updater": { + "src": "<%= path.css.updater %>/updater.css" + } +} diff --git a/dev/tools/grunt/configs/concat.json b/dev/tools/grunt/configs/concat.json index b02024c38559f..de3e5a73cc266 100644 --- a/dev/tools/grunt/configs/concat.json +++ b/dev/tools/grunt/configs/concat.json @@ -1,10 +1,10 @@ { - "options": { - "stripBanners": true, - "banner": "/**\n * Copyright © <%= grunt.template.today(\"yyyy\") %> Magento. All rights reserved.\n * See COPYING.txt for license details.\n */\n" - }, - "setup": { - "src": "<%= path.css.setup %>/setup.css", - "dest": "<%= path.css.setup %>/setup.css" - } -} \ No newline at end of file + "options": { + "stripBanners": true, + "banner": "/**\n * Copyright © <%= grunt.template.today(\"yyyy\") %> Magento. All rights reserved.\n * See COPYING.txt for license details.\n */\n" + }, + "setup": { + "src": "<%= path.css.setup %>/setup.css", + "dest": "<%= path.css.setup %>/setup.css" + } +} diff --git a/dev/tools/grunt/configs/cssmin.json b/dev/tools/grunt/configs/cssmin.json index 032657cc3ed81..7b56ec43eccdd 100644 --- a/dev/tools/grunt/configs/cssmin.json +++ b/dev/tools/grunt/configs/cssmin.json @@ -1,16 +1,16 @@ { - "options": { - "report": "gzip", - "keepSpecialComments": 0 - }, - "setup": { - "files": { - "<%= path.css.setup %>/setup.css": "<%= path.css.setup %>/setup.css" - } - }, - "updater": { - "files": { - "<%= path.css.updater %>/updater.css": "<%= path.css.updater %>/updater.css" - } + "options": { + "report": "gzip", + "keepSpecialComments": 0 + }, + "setup": { + "files": { + "<%= path.css.setup %>/setup.css": "<%= path.css.setup %>/setup.css" } -} \ No newline at end of file + }, + "updater": { + "files": { + "<%= path.css.updater %>/updater.css": "<%= path.css.updater %>/updater.css" + } + } +} diff --git a/dev/tools/grunt/configs/eslint.json b/dev/tools/grunt/configs/eslint.json index 78719ce9548f6..ec0d821a1c0d8 100644 --- a/dev/tools/grunt/configs/eslint.json +++ b/dev/tools/grunt/configs/eslint.json @@ -1,18 +1,18 @@ { - "file": { - "options": { - "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", - "reset": true, - "useEslintrc": false - } - }, - "test": { - "options": { - "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", - "reset": true, - "outputFile": "dev/tests/static/eslint-error-report.xml", - "format": "junit", - "quiet": true - } + "file": { + "options": { + "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", + "reset": true, + "useEslintrc": false } + }, + "test": { + "options": { + "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", + "reset": true, + "outputFile": "dev/tests/static/eslint-error-report.xml", + "format": "junit", + "quiet": true + } + } } diff --git a/dev/tools/grunt/configs/jscs.json b/dev/tools/grunt/configs/jscs.json index dd9254abbdc68..e4002f035f824 100644 --- a/dev/tools/grunt/configs/jscs.json +++ b/dev/tools/grunt/configs/jscs.json @@ -1,16 +1,16 @@ { - "file": { - "options": { - "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc" - }, - "src": "" + "file": { + "options": { + "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc" }, - "test": { - "options": { - "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc", - "reporterOutput": "dev/tests/static/jscs-error-report.xml", - "reporter": "junit" - }, - "src": "" - } + "src": "" + }, + "test": { + "options": { + "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc", + "reporterOutput": "dev/tests/static/jscs-error-report.xml", + "reporter": "junit" + }, + "src": "" + } } diff --git a/dev/tools/grunt/configs/mage-minify.json b/dev/tools/grunt/configs/mage-minify.json index 1ddfa3910a3a8..c6ecfc5579701 100644 --- a/dev/tools/grunt/configs/mage-minify.json +++ b/dev/tools/grunt/configs/mage-minify.json @@ -1,21 +1,21 @@ { - "legacy": { - "options": { - "compressor": "yui-js" - }, - "files": { - "<%= path.uglify.legacy %>": [ - "lib/web/prototype/prototype.js", - "lib/web/prototype/window.js", - "lib/web/scriptaculous/builder.js", - "lib/web/scriptaculous/effects.js", - "lib/web/lib/ccard.js", - "lib/web/prototype/validation.js", - "lib/web/varien/js.js", - "lib/web/mage/adminhtml/varienLoader.js", - "lib/web/mage/adminhtml/tools.js", - "dev/tools/grunt/assets/legacy-build/shim.js" - ] - } + "legacy": { + "options": { + "compressor": "yui-js" + }, + "files": { + "<%= path.uglify.legacy %>": [ + "lib/web/prototype/prototype.js", + "lib/web/prototype/window.js", + "lib/web/scriptaculous/builder.js", + "lib/web/scriptaculous/effects.js", + "lib/web/lib/ccard.js", + "lib/web/prototype/validation.js", + "lib/web/varien/js.js", + "lib/web/mage/adminhtml/varienLoader.js", + "lib/web/mage/adminhtml/tools.js", + "dev/tools/grunt/assets/legacy-build/shim.js" + ] } + } } diff --git a/dev/tools/grunt/configs/styledocco.json b/dev/tools/grunt/configs/styledocco.json index 8a75d43ac35e4..1527b26114b97 100644 --- a/dev/tools/grunt/configs/styledocco.json +++ b/dev/tools/grunt/configs/styledocco.json @@ -1,14 +1,12 @@ { - "documentation": { - "options": { - "name": "Magento UI Library", - "verbose": true, - "include": [ - "<%= path.doc %>/docs.css" - ] - }, - "files": { - "<%= path.doc %>": "<%= path.doc %>/source" - } + "documentation": { + "options": { + "name": "Magento UI Library", + "verbose": true, + "include": ["<%= path.doc %>/docs.css"] + }, + "files": { + "<%= path.doc %>": "<%= path.doc %>/source" } -} \ No newline at end of file + } +} diff --git a/grunt-config.json.sample b/grunt-config.json.sample index 7ef28a856f925..5704e5f012e17 100644 --- a/grunt-config.json.sample +++ b/grunt-config.json.sample @@ -1,3 +1,3 @@ { - "themes": "dev/tools/grunt/configs/local-themes" + "themes": "dev/tools/grunt/configs/local-themes" } diff --git a/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php b/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php index 12af882a46760..531fa6763fc6e 100644 --- a/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php +++ b/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php @@ -6,10 +6,10 @@ namespace Magento\Framework\Api\Code\Generator; -use Magento\Framework\ObjectManager\Code\Generator\Factory; +use Magento\Framework\Code\Generator\CodeGeneratorInterface; use Magento\Framework\Code\Generator\DefinedClasses; use Magento\Framework\Code\Generator\Io; -use Magento\Framework\Code\Generator\CodeGeneratorInterface; +use Magento\Framework\ObjectManager\Code\Generator\Factory; class ExtensionAttributesInterfaceFactoryGenerator extends Factory { @@ -18,11 +18,6 @@ class ExtensionAttributesInterfaceFactoryGenerator extends Factory */ const ENTITY_TYPE = 'extensionInterfaceFactory'; - /** - * @var string - */ - private static $suffix = 'InterfaceFactory'; - /** * Initialize dependencies. * @@ -50,21 +45,10 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ - protected function _validateData() + protected function getResultClassSuffix() { - $result = true; - $sourceClassName = $this->getSourceClassName(); - $resultClassName = $this->_getResultClassName(); - - if ($resultClassName !== $sourceClassName . self::$suffix) { - $this->_addError( - 'Invalid Factory class name [' . $resultClassName . ']. Use ' . $sourceClassName . self::$suffix - ); - $result = false; - } - - return $result; + return 'InterfaceFactory'; } } diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index 8f8dfd3baf1b6..363c9740b38aa 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -56,17 +56,19 @@ public function __construct( } /** - * Clean cache on save object + * Clean cache when object is saved * * @param AbstractResource $subject - * @param \Closure $proceed + * @param AbstractResource $result * @param AbstractModel $object * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundSave(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource - { - $result = $proceed($object); + public function afterSave( + AbstractResource $subject, + AbstractResource $result, + AbstractModel $object + ): AbstractResource { $tags = $this->tagResolver->getTags($object); $this->cleanCacheByTags($tags); @@ -74,18 +76,20 @@ public function aroundSave(AbstractResource $subject, \Closure $proceed, Abstrac } /** - * Clean cache on delete object + * Clean cache when object is deleted * * @param AbstractResource $subject - * @param \Closure $proceed + * @param AbstractResource $result * @param AbstractModel $object * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundDelete(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource - { + public function afterDelete( + AbstractResource $subject, + AbstractResource $result, + AbstractModel $object + ): AbstractResource { $tags = $this->tagResolver->getTags($object); - $result = $proceed($object); $this->cleanCacheByTags($tags); return $result; @@ -102,11 +106,12 @@ private function cleanCacheByTags(array $tags): void if (!$tags) { return; } + $uniqueTags = null; foreach ($this->cacheList as $cacheType) { if ($this->cacheState->isEnabled($cacheType)) { $this->cachePool->get($cacheType)->clean( \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, - \array_unique($tags) + $uniqueTags = $uniqueTags ?? \array_unique($tags) ); } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php index 6e550cd4fbde4..564a72c1ee510 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php @@ -61,7 +61,7 @@ protected function setUp(): void /** * @return void */ - public function testAroundSave(): void + public function testAfterSave(): void { $resource = $this->getMockBuilder(AbstractResource::class) ->disableOriginalConstructor() @@ -71,11 +71,9 @@ public function testAroundSave(): void ->getMockForAbstractClass(); $this->tagResolver->expects($this->atLeastOnce())->method('getTags')->with($model)->willReturn([]); - $result = $this->plugin->aroundSave( + $result = $this->plugin->afterSave( + $resource, $resource, - function () use ($resource) { - return $resource; - }, $model ); @@ -85,7 +83,7 @@ function () use ($resource) { /** * @return void */ - public function testAroundDelete(): void + public function testAfterDelete(): void { $resource = $this->getMockBuilder(AbstractResource::class) ->disableOriginalConstructor() @@ -95,11 +93,9 @@ public function testAroundDelete(): void ->getMockForAbstractClass(); $this->tagResolver->expects($this->atLeastOnce())->method('getTags')->with($model)->willReturn([]); - $result = $this->plugin->aroundDelete( + $result = $this->plugin->afterDelete( + $resource, $resource, - function () use ($resource) { - return $resource; - }, $model ); diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 2c3fbad4b9aaf..36993f1620e36 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -371,11 +371,11 @@ public function getMainConfigFiles($asDataSet = true) } $globPaths = [BP . '/app/etc/config.xml', BP . '/app/etc/*/config.xml']; $configXmlPaths = array_merge($globPaths, $configXmlPaths); - $files = [[]]; + $files = []; foreach ($configXmlPaths as $xmlPath) { $files[] = glob($xmlPath, GLOB_NOSORT); } - self::$_cache[$cacheKey] = array_merge(...$files); + self::$_cache[$cacheKey] = array_merge([], ...$files); } if ($asDataSet) { return self::composeDataSets(self::$_cache[$cacheKey]); diff --git a/lib/internal/Magento/Framework/Config/CompositeFileIterator.php b/lib/internal/Magento/Framework/Config/CompositeFileIterator.php new file mode 100644 index 0000000000000..904afa4f00c49 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/CompositeFileIterator.php @@ -0,0 +1,102 @@ +existingIterator = $existingIterator; + } + + /** + * @inheritDoc + */ + public function rewind() + { + $this->existingIterator->rewind(); + parent::rewind(); + } + + /** + * @inheritDoc + */ + public function current() + { + if ($this->existingIterator->valid()) { + return $this->existingIterator->current(); + } + + return parent::current(); + } + + /** + * @inheritDoc + */ + public function key() + { + if ($this->existingIterator->valid()) { + return $this->existingIterator->key(); + } + + return parent::key(); + } + + /** + * @inheritDoc + */ + public function next() + { + if ($this->existingIterator->valid()) { + $this->existingIterator->next(); + } else { + parent::next(); + } + } + + /** + * @inheritDoc + */ + public function valid() + { + return $this->existingIterator->valid() || parent::valid(); + } + + /** + * @inheritDoc + */ + public function toArray() + { + return array_merge($this->existingIterator->toArray(), parent::toArray()); + } + + /** + * @inheritDoc + */ + public function count() + { + return $this->existingIterator->count() + parent::count(); + } +} diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php new file mode 100644 index 0000000000000..eece996f91d5d --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php @@ -0,0 +1,73 @@ +readFactoryMock = $this->createMock(ReadFactory::class); + $this->readFactoryMock->method('create') + ->willReturnCallback( + function (string $file): Read { + $readMock = $this->createMock(Read::class); + $readMock->method('readAll')->willReturn('Content of ' .$file); + + return $readMock; + } + ); + } + + /** + * Test the composite. + */ + public function testComposition(): void + { + $existingFiles = [ + '/etc/magento/somefile.ext', + '/etc/magento/somefile2.ext', + '/etc/magento/somefile3.ext' + ]; + $newFiles = [ + '/etc/magento/some-other-file.ext', + '/etc/magento/some-other-file2.ext' + ]; + + $existing = new FileIterator($this->readFactoryMock, $existingFiles); + $composite = new CompositeFileIterator($this->readFactoryMock, $newFiles, $existing); + $found = []; + foreach ($composite as $file => $content) { + $this->assertNotEmpty($content); + $found[] = $file; + } + $this->assertEquals(array_merge($existingFiles, $newFiles), $found); + $this->assertEquals(count($existingFiles) + count($newFiles), $composite->count()); + $this->assertEquals(array_merge($existingFiles, $newFiles), array_keys($composite->toArray())); + } +} diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 3d0521fa5ac61..c5e17a97c9f01 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -48,30 +48,31 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface { // @codingStandardsIgnoreEnd - const TIMESTAMP_FORMAT = 'Y-m-d H:i:s'; - const DATETIME_FORMAT = 'Y-m-d H:i:s'; - const DATE_FORMAT = 'Y-m-d'; + public const TIMESTAMP_FORMAT = 'Y-m-d H:i:s'; + public const DATETIME_FORMAT = 'Y-m-d H:i:s'; + public const DATE_FORMAT = 'Y-m-d'; - const DDL_DESCRIBE = 1; - const DDL_CREATE = 2; - const DDL_INDEX = 3; - const DDL_FOREIGN_KEY = 4; - const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; - const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; + public const DDL_DESCRIBE = 1; + public const DDL_CREATE = 2; + public const DDL_INDEX = 3; + public const DDL_FOREIGN_KEY = 4; + public const DDL_EXISTS = 5; + public const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; + public const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; - const LENGTH_TABLE_NAME = 64; - const LENGTH_INDEX_NAME = 64; - const LENGTH_FOREIGN_NAME = 64; + public const LENGTH_TABLE_NAME = 64; + public const LENGTH_INDEX_NAME = 64; + public const LENGTH_FOREIGN_NAME = 64; /** * MEMORY engine type for MySQL tables */ - const ENGINE_MEMORY = 'MEMORY'; + public const ENGINE_MEMORY = 'MEMORY'; /** * Maximum number of connection retries */ - const MAX_CONNECTION_RETRIES = 10; + public const MAX_CONNECTION_RETRIES = 10; /** * Default class name for a DB statement. @@ -1631,7 +1632,13 @@ public function resetDdlCache($tableName = null, $schemaName = null) } else { $cacheKey = $this->_getTableName($tableName, $schemaName); - $ddlTypes = [self::DDL_DESCRIBE, self::DDL_CREATE, self::DDL_INDEX, self::DDL_FOREIGN_KEY]; + $ddlTypes = [ + self::DDL_DESCRIBE, + self::DDL_CREATE, + self::DDL_INDEX, + self::DDL_FOREIGN_KEY, + self::DDL_EXISTS + ]; foreach ($ddlTypes as $ddlType) { unset($this->_ddlCache[$ddlType][$cacheKey]); } @@ -2658,7 +2665,30 @@ public function truncateTable($tableName, $schemaName = null) */ public function isTableExists($tableName, $schemaName = null) { - return $this->showTableStatus($tableName, $schemaName) !== false; + $cacheKey = $this->_getTableName($tableName, $schemaName); + + $ddl = $this->loadDdlCache($cacheKey, self::DDL_EXISTS); + if ($ddl !== false) { + return true; + } + + $fromDbName = 'DATABASE()'; + if ($schemaName !== null) { + $fromDbName = $this->quote($schemaName); + } + + $sql = sprintf( + 'SELECT COUNT(1) AS tbl_exists FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = %s AND TABLE_SCHEMA = %s', + $this->quote($tableName), + $fromDbName + ); + $ddl = $this->rawFetchRow($sql, 'tbl_exists'); + if ($ddl) { + $this->saveDdlCache($cacheKey, self::DDL_EXISTS, $ddl); + return true; + } + + return false; } /** diff --git a/lib/internal/Magento/Framework/File/Name.php b/lib/internal/Magento/Framework/File/Name.php new file mode 100644 index 0000000000000..359719293f735 --- /dev/null +++ b/lib/internal/Magento/Framework/File/Name.php @@ -0,0 +1,64 @@ +getPathInfo($destinationFile); + if ($this->fileExist($destinationFile)) { + $index = 1; + $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; + while ($this->fileExist($fileInfo['dirname'] . '/' . $baseName)) { + $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; + $index++; + } + $destFileName = $baseName; + } else { + return $fileInfo['basename']; + } + + return $destFileName; + } + + /** + * Get the path information from a given file + * + * @param string $destinationFile + * @return string|string[] + */ + private function getPathInfo(string $destinationFile) + { + return pathinfo($destinationFile); + } + + /** + * Check to see if a given file exists + * + * @param string $destinationFile + * @return bool + */ + private function fileExist(string $destinationFile) + { + return file_exists($destinationFile); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 484eed347be0f..1d60b7ce879bf 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -8,6 +8,8 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Phrase; /** * Write Interface implementation @@ -25,14 +27,14 @@ class Write extends Read implements WriteInterface * Constructor * * @param \Magento\Framework\Filesystem\File\WriteFactory $fileFactory - * @param \Magento\Framework\Filesystem\DriverInterface $driver + * @param DriverInterface $driver * @param string $path * @param int $createPermissions * @param PathValidatorInterface|null $pathValidator */ public function __construct( \Magento\Framework\Filesystem\File\WriteFactory $fileFactory, - \Magento\Framework\Filesystem\DriverInterface $driver, + DriverInterface $driver, $path, $createPermissions = null, ?PathValidatorInterface $pathValidator = null @@ -48,13 +50,13 @@ public function __construct( * * @param string $path * @return void - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException|ValidatorException */ protected function assertWritable($path) { if ($this->isWritable($path) === false) { $path = $this->getAbsolutePath($path); - throw new FileSystemException(new \Magento\Framework\Phrase('The path "%1" is not writable.', [$path])); + throw new FileSystemException(new Phrase('The path "%1" is not writable.', [$path])); } } @@ -63,7 +65,7 @@ protected function assertWritable($path) * * @param string $path * @return void - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException */ protected function assertIsFile($path) { @@ -71,7 +73,7 @@ protected function assertIsFile($path) clearstatcache(true, $absolutePath); if (!$this->driver->isFile($absolutePath)) { throw new FileSystemException( - new \Magento\Framework\Phrase('The "%1" file doesn\'t exist.', [$absolutePath]) + new Phrase('The "%1" file doesn\'t exist.', [$absolutePath]) ); } } @@ -149,7 +151,7 @@ public function copyFile($path, $destination, WriteInterface $targetDirectory = * @param string $destination * @param WriteInterface $targetDirectory [optional] * @return bool - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException * @throws ValidatorException */ public function createSymlink($path, $destination, WriteInterface $targetDirectory = null) @@ -178,10 +180,18 @@ public function delete($path = null) { $exceptionMessages = []; $this->validatePath($path); + if (!$this->isExist($path)) { return true; } + $absolutePath = $this->driver->getAbsolutePath($this->path, $path); + $basePath = $this->driver->getRealPathSafety($this->driver->getAbsolutePath($this->path, '')); + + if ($path !== null && $path !== '' && $this->driver->getRealPathSafety($absolutePath) === $basePath) { + throw new FileSystemException(new Phrase('The path "%1" is not writable.', [$path])); + } + if ($this->driver->isFile($absolutePath)) { $this->driver->deleteFile($absolutePath); } else { @@ -198,12 +208,13 @@ public function delete($path = null) if (!empty($exceptionMessages)) { throw new FileSystemException( - new \Magento\Framework\Phrase( + new Phrase( \implode(' ', $exceptionMessages) ) ); } } + return true; } @@ -231,7 +242,7 @@ private function deleteFilesRecursively(string $path) } if (!empty($exceptionMessages)) { throw new FileSystemException( - new \Magento\Framework\Phrase( + new Phrase( \implode(' ', $exceptionMessages) ) ); @@ -297,7 +308,7 @@ public function touch($path, $modificationTime = null) * * @param string|null $path * @return bool - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException * @throws ValidatorException */ public function isWritable($path = null) @@ -313,7 +324,7 @@ public function isWritable($path = null) * @param string $path * @param string $mode * @return \Magento\Framework\Filesystem\File\WriteInterface - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException * @throws ValidatorException */ public function openFile($path, $mode = 'w') @@ -334,7 +345,7 @@ public function openFile($path, $mode = 'w') * @param string $content * @param string|null $mode * @return int The number of bytes that were written. - * @throws FileSystemException + * @throws FileSystemException|ValidatorException */ public function writeFile($path, $content, $mode = 'w+') { @@ -344,7 +355,7 @@ public function writeFile($path, $content, $mode = 'w+') /** * Get driver * - * @return \Magento\Framework\Filesystem\DriverInterface + * @return DriverInterface */ public function getDriver() { diff --git a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php index faddd54e5f180..9eb409ab4a593 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php @@ -7,6 +7,7 @@ namespace Magento\Framework\GraphQl\Query; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\GraphQl\Config\Element\Enum; use Magento\Framework\GraphQl\ConfigInterface; use Magento\Framework\GraphQl\Schema\Type\Enum\DataMapperInterface; @@ -43,23 +44,27 @@ public function __construct(ConfigInterface $typeConfig, DataMapperInterface $en * @param string $enumName * @param string $fieldValue * @return string - * @throws \Magento\Framework\Exception\RuntimeException + * @throws RuntimeException */ public function getEnumValueFromField(string $enumName, string $fieldValue) : string { - $priceViewEnum = $this->typeConfig->getConfigElement($enumName); - if ($priceViewEnum instanceof Enum) { - foreach ($priceViewEnum->getValues() as $enumItem) { - $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); - if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] == $fieldValue) { - return $enumItem->getValue(); - } - } - } else { - throw new \Magento\Framework\Exception\RuntimeException( + /** @var Enum $enumObject */ + $enumObject = $this->typeConfig->getConfigElement($enumName); + + if (!($enumObject instanceof Enum)) { + throw new RuntimeException( new Phrase('Enum type "%1" not defined', [$enumName]) ); } + + $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); + + foreach ($enumObject->getValues() as $enumItem) { + if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] == $fieldValue) { + return $enumItem->getValue(); + } + } + return ''; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php new file mode 100644 index 0000000000000..7e5d6ab2d6565 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php @@ -0,0 +1,154 @@ +objectManager = new ObjectManager($this); + + $this->map = [ + self::ENUM_NAME => [ + 'subscribed' => '1', + 'not_active' => '2', + 'unsubscribed' => '3', + 'unconfirmed' => '4', + ] + ]; + + $this->values = [ + 'NOT_ACTIVE' => new EnumValue('not_active', 'NOT_ACTIVE'), + 'SUBSCRIBED' => new EnumValue('subscribed', 'SUBSCRIBED'), + 'UNSUBSCRIBED' => new EnumValue('unsubscribed', 'UNSUBSCRIBED'), + 'UNCONFIRMED' => new EnumValue('unconfirmed', 'UNCONFIRMED'), + ]; + + $this->enumMock = $this->getMockBuilder(Enum::class) + ->setConstructorArgs( + [ + self::ENUM_NAME, + $this->values, + 'Subscription statuses', + ] + ) + ->getMock(); + + $this->enumDataMapperMock = $this->createMock(DataMapperInterface::class); + $this->configDataMock = $this->createMock(DataInterface::class); + $this->configElementFactoryMock = $this->createMock(ConfigElementFactoryInterface::class); + $this->queryFieldsMock = $this->createMock(QueryFields::class); + $this->typeConfigMock = $this->createMock(ConfigInterface::class); + + $this->enumLookup = $this->objectManager->getObject( + EnumLookup::class, + [ + 'typeConfig' => $this->typeConfigMock, + 'enumDataMapper' => $this->enumDataMapperMock, + ] + ); + } + + public function testGetEnumValueFromField() + { + $enumName = self::ENUM_NAME; + $fieldValue = '1'; + + $this->enumDataMapperMock + ->expects($this->once()) + ->method('getMappedEnums') + ->willReturn($this->map[$enumName]); + + $this->typeConfigMock + ->expects($this->once()) + ->method('getConfigElement') + ->willReturn($this->enumMock); + + $this->enumMock + ->expects($this->once()) + ->method('getValues') + ->willReturn($this->values); + + $this->assertEquals( + 'SUBSCRIBED', + $this->enumLookup->getEnumValueFromField($enumName, $fieldValue) + ); + } +} diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php index dfe4b759e85be..c505c82789f81 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php @@ -120,7 +120,7 @@ function (string $ip) { public function getRemoteAddress(bool $ipToLong = false) { if ($this->remoteAddress !== null) { - return $this->remoteAddress; + return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } $remoteAddress = $this->readAddress(); @@ -135,11 +135,11 @@ public function getRemoteAddress(bool $ipToLong = false) $this->remoteAddress = false; return false; - } else { - $this->remoteAddress = $remoteAddress; - - return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } + + $this->remoteAddress = $remoteAddress; + + return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } /** diff --git a/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php b/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php index 25f665ed70e84..20aafb797ce0e 100644 --- a/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php +++ b/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php @@ -9,13 +9,10 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test for - * * @see RemoteAddress */ class RemoteAddressTest extends TestCase @@ -23,24 +20,17 @@ class RemoteAddressTest extends TestCase /** * @var MockObject|HttpRequest */ - protected $_request; - - /** - * @var ObjectManager - */ - protected $_objectManager; + private $requestMock; /** * @inheritdoc */ protected function setUp(): void { - $this->_request = $this->getMockBuilder(HttpRequest::class) + $this->requestMock = $this->getMockBuilder(HttpRequest::class) ->disableOriginalConstructor() - ->setMethods(['getServer']) + ->onlyMethods(['getServer']) ->getMock(); - - $this->_objectManager = new ObjectManager($this); } /** @@ -49,6 +39,7 @@ protected function setUp(): void * @param string|bool $expected * @param bool $ipToLong * @param string[]|null $trustedProxies + * * @return void * @dataProvider getRemoteAddressProvider */ @@ -59,18 +50,16 @@ public function testGetRemoteAddress( bool $ipToLong, array $trustedProxies = null ): void { - $remoteAddress = $this->_objectManager->getObject( - RemoteAddress::class, - [ - 'httpRequest' => $this->_request, - 'alternativeHeaders' => $alternativeHeaders, - 'trustedProxies' => $trustedProxies, - ] + $remoteAddress = new RemoteAddress( + $this->requestMock, + $alternativeHeaders, + $trustedProxies ); - $this->_request->expects($this->any()) - ->method('getServer') + $this->requestMock->method('getServer') ->willReturnMap($serverValueMap); + // Check twice to verify if internal variable is cached correctly + $this->assertEquals($expected, $remoteAddress->getRemoteAddress($ipToLong)); $this->assertEquals($expected, $remoteAddress->getRemoteAddress($ipToLong)); } diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 46db17034ac3e..7e36cdb334eb2 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -3,12 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Image\Adapter; use Magento\Framework\Exception\LocalizedException; /** - * Image adapter from ImageMagick + * Image adapter from ImageMagick. */ class ImageMagick extends AbstractAdapter { @@ -35,6 +36,18 @@ class ImageMagick extends AbstractAdapter 'sharpen' => ['radius' => 4, 'deviation' => 1], ]; + /** + * @var \Imagick + */ + protected $_imageHandler; + + /** + * Colorspace of the image + * + * @var int + */ + private $colorspace = -1; + /** * Set/get background color. Check Imagick::COLOR_* constants * @@ -94,6 +107,8 @@ public function open($filename) ); } + $this->getColorspace(); + $this->maybeConvertColorspace(); $this->backgroundColor(); $this->getMimeType(); } @@ -158,8 +173,8 @@ protected function _applyOptions() /** * Render image and return its binary contents * - * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage * @return string + * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage */ public function getImage() { @@ -288,7 +303,7 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $this->_checkCanProcess(); $opacity = $this->getWatermarkImageOpacity() ? $this->getWatermarkImageOpacity() : $opacity; - $opacity = (double)number_format($opacity / 100, 1); + $opacity = (double) number_format($opacity / 100, 1); $watermark = new \Imagick($imagePath); @@ -419,8 +434,8 @@ public function getColorAt($x, $y) /** * Check whether the adapter can work with the image * - * @throws \LogicException * @return true + * @throws \LogicException */ protected function _checkCanProcess() { @@ -586,4 +601,31 @@ private function addSingleWatermark($positionX, int $positionY, \Imagick $waterm $compositeChannels ); } + + /** + * Get and store the image colorspace. + * + * @return int + */ + private function getColorspace(): int + { + if ($this->colorspace === -1) { + $this->colorspace = $this->_imageHandler->getImageColorspace(); + } + + return $this->colorspace; + } + + /** + * Convert colorspace to SRGB if current colorspace is COLORSPACE_CMYK or COLORSPACE_UNDEFINED. + * + * @return void + */ + private function maybeConvertColorspace(): void + { + if ($this->colorspace === \Imagick::COLORSPACE_CMYK || $this->colorspace === \Imagick::COLORSPACE_UNDEFINED) { + $this->_imageHandler->transformImageColorspace(\Imagick::COLORSPACE_SRGB); + $this->colorspace = $this->_imageHandler->getImageColorspace(); + } + } } diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php index 2a27d25dac82e..f21101f099200 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -76,7 +76,7 @@ public function testWatermark($imagePath, $expectedMessage) /** * @return array */ - public function watermarkDataProvider() + public function watermarkDataProvider(): array { return [ ['', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], diff --git a/lib/internal/Magento/Framework/Indexer/CacheContext.php b/lib/internal/Magento/Framework/Indexer/CacheContext.php index 4a6964477ebd5..b29aafa054997 100644 --- a/lib/internal/Magento/Framework/Indexer/CacheContext.php +++ b/lib/internal/Magento/Framework/Indexer/CacheContext.php @@ -73,6 +73,6 @@ public function getIdentities() $identities[] = $cacheTag . '_' . $id; } } - return array_merge($identities, array_unique($this->tags)); + return array_unique(array_merge($identities, array_unique($this->tags))); } } diff --git a/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php b/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php new file mode 100644 index 0000000000000..a13d5d54aafe7 --- /dev/null +++ b/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php @@ -0,0 +1,58 @@ +object = new CacheContext(); + } + + /** + * @param array $tagsData + * @param array $expected + * @dataProvider getTagsDataProvider + */ + public function testUniqueTags($tagsData, $expected) + { + foreach ($tagsData as $tagSet) { + foreach ($tagSet as $cacheTag => $ids) { + $this->object->registerEntities($cacheTag, $ids); + } + } + + $this->assertEquals($this->object->getIdentities(), $expected); + } + + /** + * @return array + */ + public function getTagsDataProvider() + { + return [ + 'same entities and ids' => [ + [['cat_p' => [1]], ['cat_p' => [1]]], + ['cat_p_1'] + ], + 'same entities with overlapping ids' => [ + [['cat_p' => [1, 2, 3]], ['cat_p' => [3]]], + ['cat_p_1', 'cat_p_2', 'cat_p_3'] + ] + ]; + } +} diff --git a/lib/internal/Magento/Framework/Logger/Monolog.php b/lib/internal/Magento/Framework/Logger/Monolog.php index f7647605cb7ac..05ef4cf262d77 100644 --- a/lib/internal/Magento/Framework/Logger/Monolog.php +++ b/lib/internal/Magento/Framework/Logger/Monolog.php @@ -11,7 +11,7 @@ class Monolog extends Logger { /** - * {@inheritdoc} + * @inheritdoc */ public function __construct($name, array $handlers = [], array $processors = []) { @@ -29,7 +29,7 @@ public function __construct($name, array $handlers = [], array $processors = []) * @param integer $level The logging level * @param string $message The log message * @param array $context The log context - * @return Boolean Whether the record has been processed + * @return bool Whether the record has been processed */ public function addRecord($level, $message, array $context = []) { diff --git a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php index 9f33b5b39d2ad..10b2eaf347dd9 100644 --- a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php @@ -18,7 +18,7 @@ interface PublisherInterface * Publishes a message to a specific queue or exchange. * * @param string $topicName - * @param array|object $data + * @param mixed $data * @return null|mixed * @throws \InvalidArgumentException If message is not formed properly * @since 103.0.0 diff --git a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php index 6b7fcd131ba8b..f702b71378bb9 100644 --- a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php +++ b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php @@ -74,7 +74,7 @@ public function getAttributes($entityType) $attributes[] = $provider->getAttributes($entityType); } - $this->registry[$entityType] = \array_merge(...$attributes); + $this->registry[$entityType] = \array_merge([], ...$attributes); } return $this->registry[$entityType]; diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index 7e484407d7a54..f30d38b6e112e 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php +++ b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php @@ -210,6 +210,6 @@ private function expandSequence($list, $name, $accumulated = []) $allResults[] = $this->expandSequence($list, $relatedName, $accumulated); } $allResults[] = $result; - return array_unique(array_merge(...$allResults)); + return array_unique(array_merge([], ...$allResults)); } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php index 6186bffd4ca68..9e23d0ca86ca9 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php @@ -7,9 +7,6 @@ class Factory extends \Magento\Framework\Code\Generator\EntityAbstract { - /** - * Entity type - */ const ENTITY_TYPE = 'factory'; /** @@ -90,7 +87,7 @@ protected function _getClassMethods() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _validateData() { @@ -100,13 +97,24 @@ protected function _validateData() $sourceClassName = $this->getSourceClassName(); $resultClassName = $this->_getResultClassName(); - if ($resultClassName !== $sourceClassName . 'Factory') { + if ($resultClassName !== $sourceClassName . $this->getResultClassSuffix()) { $this->_addError( - 'Invalid Factory class name [' . $resultClassName . ']. Use ' . $sourceClassName . 'Factory' + 'Invalid Factory class name [' . $resultClassName . ']. Use ' . + $sourceClassName . $this->getResultClassSuffix() ); $result = false; } } return $result; } + + /** + * Suffix for generated class + * + * @return string + */ + protected function getResultClassSuffix() + { + return 'Factory'; + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php index b662a2a34c813..b57f4665aff21 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php @@ -239,7 +239,7 @@ protected function resolveArgumentsInRuntime($requestedType, array $parameters, $resolvedArguments[] = $this->getResolvedArgument((string)$requestedType, $parameter, $arguments); } - return empty($resolvedArguments) ? [] : array_merge(...$resolvedArguments); + return array_merge([], ...$resolvedArguments); } /** diff --git a/lib/internal/Magento/Framework/Session/Config.php b/lib/internal/Magento/Framework/Session/Config.php index 296b7944ea4f6..1791ab09156fd 100644 --- a/lib/internal/Magento/Framework/Session/Config.php +++ b/lib/internal/Magento/Framework/Session/Config.php @@ -28,6 +28,18 @@ class Config implements ConfigInterface /** Configuration path for session cache limiter */ const PARAM_SESSION_CACHE_LIMITER = 'session/cache_limiter'; + /** Configuration path for session garbage collection probability */ + private const PARAM_SESSION_GC_PROBABILITY = 'session/gc_probability'; + + /** Configuration path for session garbage collection divisor */ + private const PARAM_SESSION_GC_DIVISOR = 'session/gc_divisor'; + + /** + * Configuration path for session garbage collection max lifetime. + * The number of seconds after which data will be seen as 'garbage'. + */ + private const PARAM_SESSION_GC_MAXLIFETIME = 'session/gc_maxlifetime'; + /** Configuration path for cookie domain */ const XML_PATH_COOKIE_DOMAIN = 'web/cookie/cookie_domain'; @@ -102,6 +114,7 @@ class Config implements ConfigInterface * @param string $scopeType * @param string $lifetimePath * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function __construct( \Magento\Framework\ValidatorFactory $validatorFactory, @@ -149,6 +162,30 @@ public function __construct( $this->setOption('session.cache_limiter', $cacheLimiter); } + /** + * Session garbage collection probability + */ + $gcProbability = $deploymentConfig->get(self::PARAM_SESSION_GC_PROBABILITY); + if ($gcProbability) { + $this->setOption('session.gc_probability', $gcProbability); + } + + /** + * Session garbage collection divisor + */ + $gcDivisor = $deploymentConfig->get(self::PARAM_SESSION_GC_DIVISOR); + if ($gcDivisor) { + $this->setOption('session.gc_divisor', $gcDivisor); + } + + /** + * Session garbage collection max lifetime + */ + $gcMaxlifetime = $deploymentConfig->get(self::PARAM_SESSION_GC_MAXLIFETIME); + if ($gcMaxlifetime) { + $this->setOption('session.gc_maxlifetime', $gcMaxlifetime); + } + /** * Cookie settings: lifetime, path, domain, httpOnly. These govern settings for the session cookie. */ diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Intl/DateFormatterFactory.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Intl/DateFormatterFactory.php new file mode 100644 index 0000000000000..42a381535b8b9 --- /dev/null +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Intl/DateFormatterFactory.php @@ -0,0 +1,104 @@ + [ + \IntlDateFormatter::SHORT => 'd/MM/y', + ] + ]; + + /** + * Create Intl Date formatter + * + * The Intl Date formatter gives date formats by ICU standard. + * http://userguide.icu-project.org/formatparse/datetime + * + * @param string $locale + * @param int $dateStyle + * @param int $timeStyle + * @param string|null $timeZone + * @param bool $useFourDigitsForYear + * @return \IntlDateFormatter + */ + public function create( + string $locale, + int $dateStyle, + int $timeStyle, + ?string $timeZone = null, + bool $useFourDigitsForYear = true + ): \IntlDateFormatter { + $formatter = new \IntlDateFormatter( + $locale, + $dateStyle, + $timeStyle, + $timeZone + ); + /** + * Process custom date formats + */ + $customDateFormat = $this->getCustomDateFormat($locale, $dateStyle, $timeStyle); + if ($customDateFormat !== null) { + $formatter->setPattern($customDateFormat); + } elseif ($dateStyle === \IntlDateFormatter::SHORT && $useFourDigitsForYear) { + /** + * Gives 4 places for year value in short style + */ + $longYearPattern = $this->setFourYearPlaces((string)$formatter->getPattern()); + $formatter->setPattern($longYearPattern); + } + + return $formatter; + } + + /** + * Get custom date format if it exists + * + * @param string $locale + * @param int $dateStyle + * @param int $timeStyle + * @return string + */ + private function getCustomDateFormat(string $locale, int $dateStyle, int $timeStyle): ?string + { + $customDateFormat = null; + if ($dateStyle !== \IntlDateFormatter::NONE && isset(self::CUSTOM_DATE_FORMATS[$locale][$dateStyle])) { + $customDateFormat = self::CUSTOM_DATE_FORMATS[$locale][$dateStyle]; + if ($timeStyle !== \IntlDateFormatter::NONE) { + $timeFormat = (new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, $timeStyle)) + ->getPattern(); + $customDateFormat .= ' ' . $timeFormat; + } + } + + return $customDateFormat; + } + + /** + * Set 4 places for year value in format string + * + * @param string $format + * @return string + */ + private function setFourYearPlaces(string $format): string + { + return preg_replace( + '/(?_scopeResolver = $scopeResolver; $this->_localeResolver = $localeResolver; @@ -87,6 +90,7 @@ public function __construct( $this->_defaultTimezonePath = $defaultTimezonePath; $this->_scopeConfig = $scopeConfig; $this->_scopeType = $scopeType; + $this->dateFormatterFactory = $dateFormatterFactory; } /** @@ -122,11 +126,15 @@ public function getConfigTimezone($scopeType = null, $scopeCode = null) */ public function getDateFormat($type = \IntlDateFormatter::SHORT) { - return (new \IntlDateFormatter( - $this->_localeResolver->getLocale(), - $type, - \IntlDateFormatter::NONE - ))->getPattern(); + $formatter = $this->dateFormatterFactory->create( + (string)$this->_localeResolver->getLocale(), + (int)$type, + \IntlDateFormatter::NONE, + null, + false + ); + + return $formatter->getPattern(); } /** @@ -134,11 +142,13 @@ public function getDateFormat($type = \IntlDateFormatter::SHORT) */ public function getDateFormatWithLongYear() { - return preg_replace( - '/(?getDateFormat() + $formatter = $this->dateFormatterFactory->create( + (string)$this->_localeResolver->getLocale(), + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE ); + + return $formatter->getPattern(); } /** @@ -146,11 +156,13 @@ public function getDateFormatWithLongYear() */ public function getTimeFormat($type = \IntlDateFormatter::SHORT) { - return (new \IntlDateFormatter( - $this->_localeResolver->getLocale(), + $formatter = $this->dateFormatterFactory->create( + (string)$this->_localeResolver->getLocale(), \IntlDateFormatter::NONE, - $type - ))->getPattern(); + (int)$type + ); + + return $formatter->getPattern(); } /** @@ -166,10 +178,8 @@ public function getDateTimeFormat($type) */ public function date($date = null, $locale = null, $useTimezone = true, $includeTime = true) { - $locale = $locale ?: $this->_localeResolver->getLocale(); - $timezone = $useTimezone - ? $this->getConfigTimezone() - : date_default_timezone_get(); + $locale = (string)($locale ?: $this->_localeResolver->getLocale()); + $timezone = (string)($useTimezone ? $this->getConfigTimezone() : date_default_timezone_get()); switch (true) { case (empty($date)): @@ -179,9 +189,14 @@ public function date($date = null, $locale = null, $useTimezone = true, $include case ($date instanceof \DateTimeImmutable): return new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone()); case (!is_numeric($date)): - $date = $this->appendTimeIfNeeded($date, $includeTime, $timezone, $locale); - $date = $this->parseLocaleDate($date, $locale, $timezone, $includeTime) - ?: (new \DateTime($date))->getTimestamp(); + $date = $this->appendTimeIfNeeded((string)$date, (bool)$includeTime, $timezone, $locale); + $formatter = $this->dateFormatterFactory->create( + $locale, + \IntlDateFormatter::SHORT, + $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE, + $timezone + ); + $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); break; } @@ -279,7 +294,6 @@ public function formatDateTime( if (!($date instanceof \DateTimeInterface)) { $date = new \DateTime($date); } - if ($timezone === null) { if ($date->getTimezone() == null || $date->getTimezone()->getName() == 'UTC' || $date->getTimezone()->getName() == '+00:00' @@ -290,14 +304,20 @@ public function formatDateTime( } } - $formatter = new \IntlDateFormatter( - $locale ?: $this->_localeResolver->getLocale(), - $dateType, - $timeType, - $timezone, + $formatter = $this->dateFormatterFactory->create( + (string)($locale ?: $this->_localeResolver->getLocale()), + (int)($dateType ?? \IntlDateFormatter::SHORT), + (int)($timeType ?? \IntlDateFormatter::SHORT), null, - $pattern + false ); + if ($timezone) { + $formatter->setTimeZone($timezone); + } + if ($pattern) { + $formatter->setPattern($pattern); + } + return $formatter->format($date); } @@ -338,11 +358,17 @@ public function convertConfigTimeToUtc($date, $format = 'Y-m-d H:i:s') * @return string * @throws LocalizedException */ - private function appendTimeIfNeeded($date, $includeTime, $timezone, $locale) + private function appendTimeIfNeeded(string $date, bool $includeTime, string $timezone, string $locale) { if ($includeTime && !preg_match('/\d{1}:\d{2}/', $date)) { - $convertedDate = $this->parseLocaleDate($date, $locale, $timezone, false); - if (!$convertedDate) { + $formatter = $this->dateFormatterFactory->create( + $locale, + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE, + $timezone + ); + $timestamp = $formatter->parse($date); + if (!$timestamp) { throw new LocalizedException( new Phrase( 'Could not append time to DateTime' @@ -350,68 +376,15 @@ private function appendTimeIfNeeded($date, $includeTime, $timezone, $locale) ); } - $formatterWithHour = $this->getDateFormatter( + $formatterWithHour = $this->dateFormatterFactory->create( $locale, - $timezone, - \IntlDateFormatter::MEDIUM, - \IntlDateFormatter::SHORT + \IntlDateFormatter::SHORT, + \IntlDateFormatter::SHORT, + $timezone ); - $date = $formatterWithHour->format($convertedDate); - } - return $date; - } - - /** - * Parse date by locale format through IntlDateFormatter - * - * @param string $date - * @param string $locale - * @param string $timeZone - * @param bool $includeTime - * @return int|null Timestamp of date - */ - private function parseLocaleDate(string $date, string $locale, string $timeZone, bool $includeTime): ?int - { - $allowedStyles = [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT]; - $timeStyle = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; - - /** - * Try to parse date with different styles - */ - foreach ($allowedStyles as $style) { - $formatter = $this->getDateFormatter($locale, $timeZone, $style, $timeStyle); - $timeStamp = $formatter->parse($date); - if ($timeStamp) { - return $timeStamp; - } + $date = $formatterWithHour->format($timestamp); } - return null; - } - - /** - * Get date formatter for locale - * - * @param string $locale - * @param string $timeZone - * @param int $style - * @param int $timeStyle - * @return \IntlDateFormatter - */ - private function getDateFormatter(string $locale, string $timeZone, int $style, int $timeStyle): \IntlDateFormatter - { - $cacheKey = "{$locale}_{$timeZone}_{$style}_{$timeStyle}"; - if (isset($this->dateFormatterCache[$cacheKey])) { - return $this->dateFormatterCache[$cacheKey]; - } - - $this->dateFormatterCache[$cacheKey] = new \IntlDateFormatter( - $locale, - $style, - $timeStyle, - new \DateTimeZone($timeZone) - ); - - return $this->dateFormatterCache[$cacheKey]; + return $date; } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php index 2e8110316ec29..38a62f006adbc 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; use Magento\Framework\Stdlib\DateTime\Timezone; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; @@ -90,22 +91,23 @@ protected function tearDown(): void * @param string $date * @param string $locale * @param bool $includeTime - * @param int $expectedTimestamp + * @param int|string $expectedTime + * @param string|null $timeZone * @dataProvider dateIncludeTimeDataProvider */ - public function testDateIncludeTime($date, $locale, $includeTime, $expectedTimestamp) + public function testDateIncludeTime($date, $locale, $includeTime, $expectedTime, $timeZone = 'America/Chicago') { - $this->scopeConfig->method('getValue')->willReturn('America/Chicago'); - /** @var Timezone $timezone */ - $timezone = $this->objectManager->getObject(Timezone::class, ['scopeConfig' => $this->scopeConfig]); + if ($timeZone !== null) { + $this->scopeConfig->method('getValue')->willReturn($timeZone); + } /** @var \DateTime $dateTime */ - $dateTime = $timezone->date($date, $locale, true, $includeTime); - if (is_numeric($expectedTimestamp)) { - $this->assertEquals($expectedTimestamp, $dateTime->getTimestamp()); + $dateTime = $this->getTimezone()->date($date, $locale, $timeZone !== null, $includeTime); + if (is_numeric($expectedTime)) { + $this->assertEquals($expectedTime, $dateTime->getTimestamp()); } else { $format = $includeTime ? DateTime::DATETIME_PHP_FORMAT : DateTime::DATE_PHP_FORMAT; - $this->assertEquals($expectedTimestamp, date($format, $dateTime->getTimestamp())); + $this->assertEquals($expectedTime, date($format, $dateTime->getTimestamp())); } } @@ -158,16 +160,30 @@ public function dateIncludeTimeDataProvider(): array 1635570000 // expected timestamp ], 'Parse Saudi Arabia date without time' => [ - '31‏/8‏/2020 02020', + '4/09/2020', 'ar_SA', false, - '2020-08-31' + '2020-09-04' + ], + 'Parse Saudi Arabia date with time' => [ + '4/09/2020 10:10 مساء', + 'ar_SA', + true, + '2020-09-04 22:10:00', + null + ], + 'Parse Saudi Arabia date with zero time' => [ + '4/09/2020', + 'ar_SA', + true, + '2020-09-04 00:00:00', + null ], 'Parse date in short style with long year 1999' => [ - '9/11/1999', + '8/11/1999', 'en_US', false, - '1999-09-11' + '1999-08-11' ], 'Parse date in short style with long year 2099' => [ '9/2/2099', @@ -175,6 +191,59 @@ public function dateIncludeTimeDataProvider(): array false, '2099-09-02' ], + 'Parse date in short style with short year 1999' => [ + '8/11/99', + 'en_US', + false, + '1999-08-11' + ], + ]; + } + + /** + * @param string $locale + * @param int $style + * @param string $expectedFormat + * @dataProvider getDatetimeFormatDataProvider + */ + public function testGetDatetimeFormat(string $locale, int $style, string $expectedFormat): void + { + /** @var Timezone $timezone */ + $this->localeResolver->method('getLocale')->willReturn($locale); + $this->assertEquals($expectedFormat, $this->getTimezone()->getDateTimeFormat($style)); + } + + /** + * @return array + */ + public function getDatetimeFormatDataProvider(): array + { + return [ + ['en_US', \IntlDateFormatter::SHORT, 'M/d/yy h:mm a'], + ['ar_SA', \IntlDateFormatter::SHORT, 'd/MM/y h:mm a'] + ]; + } + + /** + * @param string $locale + * @param int $style + * @param string $expectedFormat + * @dataProvider getDateFormatWithLongYearDataProvider + */ + public function testGetDateFormatWithLongYear(string $locale, string $expectedFormat): void + { + /** @var Timezone $timezone */ + $this->localeResolver->method('getLocale')->willReturn($locale); + $this->assertEquals($expectedFormat, $this->getTimezone()->getDateFormatWithLongYear()); + } + + /** + * @return array + */ + public function getDateFormatWithLongYearDataProvider(): array + { + return [ + ['en_US', 'M/d/y'], ]; } @@ -320,7 +389,8 @@ private function getTimezone() $this->createMock(DateTime::class), $this->scopeConfig, $this->scopeType, - $this->defaultTimezonePath + $this->defaultTimezonePath, + new DateFormatterFactory() ); } diff --git a/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php new file mode 100644 index 0000000000000..43dea2356e630 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php @@ -0,0 +1,67 @@ +name = new Name(); + $this->existingFilePath = __DIR__ . '/../_files/source.txt'; + $this->multipleExistingFilePath = __DIR__ . '/../_files/name.txt'; + $this->nonExistingFilePath = __DIR__ . '/../_files/file.txt'; + } + + /** + * @test + */ + public function testGetNewFileNameWhenOneFileExists() + { + $this->assertEquals('source_1.txt', $this->name->getNewFileName($this->existingFilePath)); + } + + /** + * @test + */ + public function testGetNewFileNameWhenTwoFileExists() + { + $this->assertEquals('name_2.txt', $this->name->getNewFileName($this->multipleExistingFilePath)); + } + + /** + * @test + */ + public function testGetNewFileNameWhenFileDoesNotExist() + { + $this->assertEquals('file.txt', $this->name->getNewFileName($this->nonExistingFilePath)); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/_files/name.txt b/lib/internal/Magento/Framework/Test/Unit/_files/name.txt new file mode 100644 index 0000000000000..f121bdbff4df6 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/_files/name.txt @@ -0,0 +1 @@ +name diff --git a/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt b/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt new file mode 100644 index 0000000000000..94972ff069874 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt @@ -0,0 +1 @@ +name_1 diff --git a/lib/internal/Magento/Framework/View/Element/Template.php b/lib/internal/Magento/Framework/View/Element/Template.php index 53355203213fa..e9f164ca2fd14 100644 --- a/lib/internal/Magento/Framework/View/Element/Template.php +++ b/lib/internal/Magento/Framework/View/Element/Template.php @@ -19,7 +19,7 @@ * custom view models in block arguments in layout handle file. * * Example: - * + * * * My\Module\ViewModel\Custom * diff --git a/lib/internal/Magento/Framework/View/Layout.php b/lib/internal/Magento/Framework/View/Layout.php index ce8b086dc7b84..eeba7485e0469 100644 --- a/lib/internal/Magento/Framework/View/Layout.php +++ b/lib/internal/Magento/Framework/View/Layout.php @@ -35,6 +35,11 @@ class Layout extends \Magento\Framework\Simplexml\Config implements \Magento\Fra */ const LAYOUT_NODE = ''; + /** + * Default cache life time + */ + private const DEFAULT_CACHE_LIFETIME = 31536000; + /** * Layout Update module * @@ -172,6 +177,10 @@ class Layout extends \Magento\Framework\Simplexml\Config implements \Magento\Fra * @var SerializerInterface */ private $serializer; + /** + * @var int + */ + private $cacheLifetime; /** * @param Layout\ProcessorFactory $processorFactory @@ -188,6 +197,7 @@ class Layout extends \Magento\Framework\Simplexml\Config implements \Magento\Fra * @param \Psr\Log\LoggerInterface $logger * @param bool $cacheable * @param SerializerInterface|null $serializer + * @param int|null $cacheLifetime */ public function __construct( Layout\ProcessorFactory $processorFactory, @@ -203,7 +213,8 @@ public function __construct( AppState $appState, Logger $logger, $cacheable = true, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ?int $cacheLifetime = null ) { $this->_elementClass = \Magento\Framework\View\Layout\Element::class; $this->_renderingOutput = new \Magento\Framework\DataObject(); @@ -222,6 +233,7 @@ public function __construct( $this->generatorContextFactory = $generatorContextFactory; $this->appState = $appState; $this->logger = $logger; + $this->cacheLifetime = $cacheLifetime ?? self::DEFAULT_CACHE_LIFETIME; } /** @@ -338,7 +350,8 @@ public function generateElements() 'pageConfigStructure' => $this->getReaderContext()->getPageConfigStructure()->__toArray(), 'scheduledStructure' => $this->getReaderContext()->getScheduledStructure()->__toArray(), ]; - $this->cache->save($this->serializer->serialize($data), $cacheId, $this->getUpdate()->getHandles()); + $handles = $this->getUpdate()->getHandles(); + $this->cache->save($this->serializer->serialize($data), $cacheId, $handles, $this->cacheLifetime); } $generatorContext = $this->generatorContextFactory->create( diff --git a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php index fe79976039a9c..239d4167043c4 100644 --- a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php +++ b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php @@ -47,6 +47,11 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface */ const PAGE_LAYOUT_CACHE_SUFFIX = 'page_layout_merged'; + /** + * Default cache life time + */ + private const DEFAULT_CACHE_LIFETIME = 31536000; + /** * @var \Magento\Framework\View\Design\ThemeInterface */ @@ -169,6 +174,10 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface * @var ReadFactory */ private $readFactory; + /** + * @var int + */ + private $cacheLifetime; /** * Init merge model @@ -182,10 +191,11 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface * @param \Magento\Framework\View\Model\Layout\Update\Validator $validator * @param \Psr\Log\LoggerInterface $logger * @param ReadFactory $readFactory - * @param \Magento\Framework\View\Design\ThemeInterface $theme Non-injectable theme instance + * @param \Magento\Framework\View\Design\ThemeInterface|null $theme Non-injectable theme instance * @param string $cacheSuffix - * @param LayoutCacheKeyInterface $layoutCacheKey + * @param LayoutCacheKeyInterface|null $layoutCacheKey * @param SerializerInterface|null $serializer + * @param int|null $cacheLifetime * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -201,7 +211,8 @@ public function __construct( \Magento\Framework\View\Design\ThemeInterface $theme = null, $cacheSuffix = '', LayoutCacheKeyInterface $layoutCacheKey = null, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ?int $cacheLifetime = null ) { $this->theme = $theme ?: $design->getDesignTheme(); $this->scope = $scopeResolver->getScope(); @@ -216,6 +227,7 @@ public function __construct( $this->layoutCacheKey = $layoutCacheKey ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); + $this->cacheLifetime = $cacheLifetime ?? self::DEFAULT_CACHE_LIFETIME; } /** @@ -749,7 +761,7 @@ protected function _loadCache($cacheId) */ protected function _saveCache($data, $cacheId, array $cacheTags = []) { - $this->cache->save($data, $cacheId, $cacheTags, null); + $this->cache->save($data, $cacheId, $cacheTags, $this->cacheLifetime); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php b/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php index 31606b55f6519..8e4a907011a69 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php @@ -944,7 +944,7 @@ public function testGenerateElementsWithoutCache(): void $this->cacheMock->expects($this->once()) ->method('save') - ->with(json_encode($data), 'structure_' . $layoutCacheId, $handles) + ->with(json_encode($data), 'structure_' . $layoutCacheId, $handles, 31536000) ->willReturn(true); $generatorContextMock = $this->getMockBuilder(Context::class) diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php index a34e9a840ea75..cdd3d2c4dc65c 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php @@ -14,6 +14,7 @@ use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Url\ScopeInterface; +use Magento\Framework\View\Design\ThemeInterface; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; use Magento\Framework\View\Model\Layout\Merge; use Magento\Framework\View\Model\Layout\Update\Validator; @@ -67,6 +68,10 @@ class MergeTest extends TestCase * @var LayoutCacheKeyInterface|MockObject */ protected $layoutCacheKeyMock; + /** + * @var ThemeInterface|MockObject + */ + private $theme; protected function setUp(): void { @@ -88,6 +93,8 @@ protected function setUp(): void ->method('getCacheKeys') ->willReturn([]); + $this->theme = $this->createMock(ThemeInterface::class); + $this->model = $this->objectManagerHelper->getObject( Merge::class, [ @@ -98,6 +105,7 @@ protected function setUp(): void 'appState' => $this->appState, 'layoutCacheKey' => $this->layoutCacheKeyMock, 'serializer' => $this->serializer, + 'theme' => $this->theme, ] ); } @@ -133,7 +141,12 @@ public function testValidateMergedLayoutThrowsException() public function testSaveToCache() { $this->scope->expects($this->once())->method('getId')->willReturn(1); - $this->cache->expects($this->once())->method('save'); + $this->theme->method('getArea')->willReturn('frontend'); + $this->theme->method('getId')->willReturn(1); + $cacheKey = 'LAYOUT_frontend_STORE1_1d41d8cd98f00b204e9800998ecf8427e_page_layout_merged'; + $this->cache->expects($this->once()) + ->method('save') + ->with(null, $cacheKey, [], 31536000); $this->model->load(); } diff --git a/lib/web/css/source/components/_modals.less b/lib/web/css/source/components/_modals.less index 58c9c0674b6ad..8513db545f1ec 100644 --- a/lib/web/css/source/components/_modals.less +++ b/lib/web/css/source/components/_modals.less @@ -103,10 +103,6 @@ &.confirm { .modal-inner-wrap { .lib-css(max-width, @modal-popup-confirm__width); - - .modal-content { - padding-right: 7rem; - } } } diff --git a/lib/web/css/source/lib/_forms.less b/lib/web/css/source/lib/_forms.less index a22ec5f52db6d..9e16962c45584 100644 --- a/lib/web/css/source/lib/_forms.less +++ b/lib/web/css/source/lib/_forms.less @@ -271,7 +271,7 @@ input[type="tel"], input[type="search"], input[type="number"], - input[type="datetime"], + input[type*="date"], input[type="email"] { .lib-form-element-input(@_type: input-text); } diff --git a/lib/web/css/source/lib/_typography.less b/lib/web/css/source/lib/_typography.less index db4eaaf584f4a..67a4df192c1f6 100644 --- a/lib/web/css/source/lib/_typography.less +++ b/lib/web/css/source/lib/_typography.less @@ -13,7 +13,8 @@ @font-format: false, @font-weight: normal, @font-style: normal, - @font-display: auto + @font-display: auto, + @font-type: false ) when (@font-format = false) { @font-face { font-family: @family-name; @@ -25,17 +26,23 @@ } } +// When need specify font format also should define font type include +// The available types for @font-type are 'woff', 'woff2', 'truetype', 'opentype', 'embedded-opentype', and 'svg' +// Enclose it in single quotes +// _____________________________________________ + .lib-font-face( @family-name, @font-path, @font-format: false, @font-weight: normal, @font-style: normal, - @font-display: auto -) when not (@font-format = false) { + @font-display: auto, + @font-type: false +) when not (@font-format = false) and not (@font-type = false) { @font-face { font-family: @family-name; - src: url('@{font-path}.@{font-format}') format(@font-format); + src: url('@{font-path}.@{font-format}') format(@font-type); font-weight: @font-weight; font-style: @font-style; font-display: @font-display; @@ -569,3 +576,4 @@ .lib-typography-code(); .lib-typography-blockquote(); } + diff --git a/lib/web/css/source/lib/variables/_typography.less b/lib/web/css/source/lib/variables/_typography.less index 205b1fa9c7a3b..e357b6969dbfd 100644 --- a/lib/web/css/source/lib/variables/_typography.less +++ b/lib/web/css/source/lib/variables/_typography.less @@ -39,11 +39,15 @@ @font-size__xs: floor(.75 * @font-size__base); // 11px // Weights +@font-weight__hairline: 100; +@font-weight__extralight: 200; @font-weight__light: 300; @font-weight__regular: 400; @font-weight__heavier: 500; @font-weight__semibold: 600; @font-weight__bold: 700; +@font-weight__extrabold: 800; +@font-weight__heavy: 900; // Styles @font-style__base: normal; diff --git a/package.json.sample b/package.json.sample index 93fe72afbd24a..1d07bcff26d8d 100644 --- a/package.json.sample +++ b/package.json.sample @@ -1,42 +1,42 @@ { - "name": "magento2", - "author": "Magento Commerce Inc.", - "description": "Magento2 node modules dependencies for local development", - "license": "(OSL-3.0 OR AFL-3.0)", - "repository": { - "type": "git", - "url": "https://github.com/magento/magento2.git" - }, - "homepage": "http://magento.com/", - "devDependencies": { - "glob": "~7.1.1", - "grunt": "~1.0.1", - "grunt-autoprefixer": "~3.0.4", - "grunt-banner": "~0.6.0", - "grunt-continue": "~0.1.0", - "grunt-contrib-clean": "~1.1.0", - "grunt-contrib-connect": "~1.0.2", - "grunt-contrib-cssmin": "~2.2.1", - "grunt-contrib-imagemin": "~2.0.1", - "grunt-contrib-jasmine": "~1.2.0", - "grunt-contrib-less": "~1.4.1", - "grunt-contrib-watch": "~1.0.0", - "grunt-eslint": "~20.1.0", - "grunt-exec": "~3.0.0", - "grunt-jscs": "~3.0.1", - "grunt-replace": "~1.0.1", - "grunt-styledocco": "~0.3.0", - "grunt-template-jasmine-requirejs": "~0.2.3", - "grunt-text-replace": "~0.4.0", - "imagemin-svgo": "~5.2.1", - "load-grunt-config": "~0.19.2", - "morgan": "~1.9.0", - "node-minify": "~2.3.1", - "path": "~0.12.7", - "serve-static": "~1.13.1", - "squirejs": "~0.2.1", - "strip-json-comments": "~2.0.1", - "time-grunt": "~1.4.0", - "underscore": "~1.8.0" - } + "name": "magento2", + "author": "Magento Commerce Inc.", + "description": "Magento2 node modules dependencies for local development", + "license": "(OSL-3.0 OR AFL-3.0)", + "repository": { + "type": "git", + "url": "https://github.com/magento/magento2.git" + }, + "homepage": "http://magento.com/", + "devDependencies": { + "glob": "~7.1.1", + "grunt": "~1.0.1", + "grunt-autoprefixer": "~3.0.4", + "grunt-banner": "~0.6.0", + "grunt-continue": "~0.1.0", + "grunt-contrib-clean": "~1.1.0", + "grunt-contrib-connect": "~1.0.2", + "grunt-contrib-cssmin": "~2.2.1", + "grunt-contrib-imagemin": "~2.0.1", + "grunt-contrib-jasmine": "~1.2.0", + "grunt-contrib-less": "~1.4.1", + "grunt-contrib-watch": "~1.0.0", + "grunt-eslint": "~20.1.0", + "grunt-exec": "~3.0.0", + "grunt-jscs": "~3.0.1", + "grunt-replace": "~1.0.1", + "grunt-styledocco": "~0.3.0", + "grunt-template-jasmine-requirejs": "~0.2.3", + "grunt-text-replace": "~0.4.0", + "imagemin-svgo": "~5.2.1", + "load-grunt-config": "~0.19.2", + "morgan": "~1.9.0", + "node-minify": "~2.3.1", + "path": "~0.12.7", + "serve-static": "~1.13.1", + "squirejs": "~0.2.1", + "strip-json-comments": "~2.0.1", + "time-grunt": "~1.4.0", + "underscore": "~1.8.0" + } } diff --git a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php index 60cfcbb67c217..43f85d092b0a7 100644 --- a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php +++ b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php @@ -565,6 +565,7 @@ private function createBlock( ) { $indentLength = 0; $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); + $lineIndentation = ''; if (null !== $type) { $type = sprintf('[%s] ', $type); $indentLength = strlen($type); @@ -605,7 +606,7 @@ private function getBlockLines( int $prefixLength, int $indentLength ) { - $lines = [[]]; + $lines = []; foreach ($messages as $key => $message) { $message = OutputFormatter::escape($message); $wordwrap = wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true); @@ -614,7 +615,7 @@ private function getBlockLines( $lines[][] = ''; } } - $lines = array_merge(...$lines); + $lines = array_merge([], ...$lines); return $lines; } diff --git a/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php index acb55e29afddd..7355ac30ac59d 100644 --- a/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php +++ b/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php @@ -175,7 +175,7 @@ protected function _fetchMissingExtensionAttributesClasses($reflectionClass, $fi */ public function collectEntities(array $files) { - $output = [[]]; + $output = []; foreach ($files as $file) { $classes = $this->getDeclaredClasses($file); foreach ($classes as $className) { @@ -184,7 +184,7 @@ public function collectEntities(array $files) $output[] = $this->_fetchMissingExtensionAttributesClasses($reflectionClass, $file); } } - return array_unique(array_merge(...$output)); + return array_unique(array_merge([], ...$output)); } /** diff --git a/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php b/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php index cf38fd70884f3..ec62ab8b84482 100644 --- a/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php +++ b/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Setup\Module\I18n\Parser\Adapter; +use Exception; use Magento\Email\Model\Template\Filter; /** @@ -16,17 +19,30 @@ class Html extends AbstractAdapter * Covers * * + * @deprecated Not used anymore because of newly introduced constant + * @see self::HTML_REGEX_LIST */ const HTML_FILTER = "/i18n:\s?'(?[^'\\\\]*(?:\\\\.[^'\\\\]*)*)'/i"; + private const HTML_REGEX_LIST = [ + // + // + "/i18n:\s?'(?[^'\\\\]*(?:\\\\.[^'\\\\]*)*)'/i", + // + // + "/translate( args|)=\"'(?[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)'\"/i" + ]; + /** * @inheritdoc */ protected function _parse() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $data = file_get_contents($this->_file); if ($data === false) { - throw new \Exception('Failed to load file from disk.'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception('Failed to load file from disk.'); } $results = []; @@ -37,15 +53,19 @@ protected function _parse() if (preg_match(Filter::TRANS_DIRECTIVE_REGEX, $results[$i][2], $directive) !== 1) { continue; } + $quote = $directive[1]; $this->_addPhrase($quote . $directive[2] . $quote); } } - preg_match_all(self::HTML_FILTER, $data, $results, PREG_SET_ORDER); - for ($i = 0, $count = count($results); $i < $count; $i++) { - if (!empty($results[$i]['value'])) { - $this->_addPhrase($results[$i]['value']); + foreach (self::HTML_REGEX_LIST as $regex) { + preg_match_all($regex, $data, $results, PREG_SET_ORDER); + + for ($i = 0, $count = count($results); $i < $count; $i++) { + if (!empty($results[$i]['value'])) { + $this->_addPhrase($results[$i]['value']); + } } } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php index 15c442e9bac98..d7a2f0b4a9397 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php @@ -7,33 +7,25 @@ namespace Magento\Setup\Test\Unit\Module\I18n\Parser\Adapter; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Setup\Module\I18n\Parser\Adapter\Html; use PHPUnit\Framework\TestCase; class HtmlTest extends TestCase { /** - * @var string - */ - protected $_testFile; - - /** - * @var int + * @var Html */ - protected $_stringsCount; + private $model; /** - * @var Html + * @var string */ - protected $_adapter; + private $testFile; protected function setUp(): void { - $this->_testFile = str_replace('\\', '/', realpath(dirname(__FILE__))) . '/_files/email.html'; - $this->_stringsCount = count(file($this->_testFile)); - - $this->_adapter = (new ObjectManager($this))->getObject(Html::class); + $this->testFile = str_replace('\\', '/', realpath(__DIR__)) . '/_files/email.html'; + $this->model = new Html(); } public function testParse() @@ -41,68 +33,80 @@ public function testParse() $expectedResult = [ [ 'phrase' => 'Phrase 1', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '\'', ], [ 'phrase' => 'Phrase 2 with %a_lot of extra info for the brilliant %customer_name.', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '"', ], [ 'phrase' => 'This is test data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data at right side of attr', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is \\\' test \\\' data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is \\" test \\" data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data with a quote after', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data with space after ', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => '\\\'', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => '\\\\\\\\ ', - 'file' => $this->_testFile, + 'file' => $this->testFile, + 'line' => '', + 'quote' => '', + ], + [ + 'phrase' => 'This is test content in translate tag', + 'file' => $this->testFile, + 'line' => '', + 'quote' => '', + ], + [ + 'phrase' => 'This is test content in translate attribute', + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], ]; - $this->_adapter->parse($this->_testFile); + $this->model->parse($this->testFile); - $this->assertEquals($expectedResult, $this->_adapter->getPhrases()); + $this->assertEquals($expectedResult, $this->model->getPhrases()); } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html index 90579b48a07b5..f5603768ef306 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html +++ b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html @@ -29,5 +29,7 @@ + +