diff --git a/.gitignore b/.gitignore index 68d38d9ca7817..8ec1104f25535 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ atlassian* /pub/media/import/* !/pub/media/import/.htaccess /pub/media/logo/* +/pub/media/custom_options/* +!/pub/media/custom_options/.htaccess /pub/media/theme/* /pub/media/theme_customization/* !/pub/media/theme_customization/.htaccess diff --git a/README.md b/README.md index e73da84d66f46..9e3cf448f99fb 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@

Welcome

Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting-edge, feature-rich eCommerce solution that gets results. -## Magento system requirements -[Magento system requirements](https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements2.html). +## Magento System Requirements +[Magento System Requirements](https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements2.html). ## Install Magento -* [Installation guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). +* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). -

Contributing to the Magento 2 code base

+

Contributing to the Magento 2 Code Base

Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions. To learn about how to make a contribution, click [here][1]. @@ -39,11 +39,11 @@ Magento is thankful for any contribution that can improve our code base, documen -### Labels applied by the Magento team +### Labels Applied by the Magento Team We apply labels to public Pull Requests and Issues to help other participants retrieve additional information about current progress, component assignments, Magento release lines, and much more. Please review the [Code Contributions guide](https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing.html#labels) for detailed information on labels used in Magento 2 repositories. -## Reporting security issues +## Reporting Security Issues To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account [there](https://bugcrowd.com/magento) to submit and follow-up your issue. Learn more about reporting security issues [here](https://magento.com/security/reporting-magento-security-issue). diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php index 86e691daa4213..af1ef4400e442 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -14,7 +14,7 @@ use Magento\Framework\MessageQueue\MessageLockException; use Magento\Framework\MessageQueue\ConnectionLostException; use Magento\Framework\Exception\NotFoundException; -use Magento\Framework\MessageQueue\CallbackInvoker; +use Magento\Framework\MessageQueue\CallbackInvokerInterface; use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; use Magento\Framework\MessageQueue\EnvelopeInterface; use Magento\Framework\MessageQueue\QueueInterface; @@ -30,7 +30,7 @@ class MassConsumer implements ConsumerInterface { /** - * @var \Magento\Framework\MessageQueue\CallbackInvoker + * @var CallbackInvokerInterface */ private $invoker; @@ -67,7 +67,7 @@ class MassConsumer implements ConsumerInterface /** * Initialize dependencies. * - * @param CallbackInvoker $invoker + * @param CallbackInvokerInterface $invoker * @param ResourceConnection $resource * @param MessageController $messageController * @param ConsumerConfigurationInterface $configuration @@ -76,7 +76,7 @@ class MassConsumer implements ConsumerInterface * @param Registry $registry */ public function __construct( - CallbackInvoker $invoker, + CallbackInvokerInterface $invoker, ResourceConnection $resource, MessageController $messageController, ConsumerConfigurationInterface $configuration, diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php index eae92e1663fc8..89d468159c6e9 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -20,6 +20,7 @@ use Psr\Log\LoggerInterface; use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Encryption\Encryptor; /** * Class MassSchedule used for adding multiple entities as Operations to Bulk Management with the status tracking @@ -63,6 +64,11 @@ class MassSchedule */ private $userContext; + /** + * @var Encryptor + */ + private $encryptor; + /** * Initialize dependencies. * @@ -73,6 +79,7 @@ class MassSchedule * @param LoggerInterface $logger * @param OperationRepository $operationRepository * @param UserContextInterface $userContext + * @param Encryptor|null $encryptor */ public function __construct( IdentityGeneratorInterface $identityService, @@ -81,7 +88,8 @@ public function __construct( BulkManagementInterface $bulkManagement, LoggerInterface $logger, OperationRepository $operationRepository, - UserContextInterface $userContext = null + UserContextInterface $userContext = null, + Encryptor $encryptor = null ) { $this->identityService = $identityService; $this->itemStatusInterfaceFactory = $itemStatusInterfaceFactory; @@ -90,6 +98,7 @@ public function __construct( $this->logger = $logger; $this->operationRepository = $operationRepository; $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); + $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(Encryptor::class); } /** @@ -130,9 +139,13 @@ public function publishMass($topicName, array $entitiesArray, $groupId = null, $ $requestItem = $this->itemStatusInterfaceFactory->create(); try { - $operations[] = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $operation = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $operations[] = $operation; $requestItem->setId($key); $requestItem->setStatus(ItemStatusInterface::STATUS_ACCEPTED); + $requestItem->setDataHash( + $this->encryptor->hash($operation->getSerializedData(), Encryptor::HASH_VERSION_SHA256) + ); $requestItems[] = $requestItem; } catch (\Exception $exception) { $this->logger->error($exception); diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php index d518af4e04f55..10be4cd5febf6 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php @@ -194,7 +194,7 @@ public function setDataFromOrder( /** * Set sign hash into the request object. * - * All needed fields should be placed in the object fist. + * All needed fields should be placed in the object first. * * @return $this */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 9890a10a4ceb0..891b2a3ada724 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -282,25 +282,23 @@ public function getGridIdsJson() if (!$this->getUseSelectAll()) { return ''; } - /** @var \Magento\Framework\Data\Collection $allIdsCollection */ - $allIdsCollection = clone $this->getParentBlock()->getCollection(); - if ($this->getMassactionIdField()) { - $massActionIdField = $this->getMassactionIdField(); + /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ + $collection = clone $this->getParentBlock()->getCollection(); + + if ($collection instanceof AbstractDb) { + $idsSelect = clone $collection->getSelect(); + $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); + $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $idsSelect->columns($this->getMassactionIdField(), 'main_table'); + $idList = $collection->getConnection()->fetchCol($idsSelect); } else { - $massActionIdField = $this->getParentBlock()->getMassactionIdField(); + $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); } - if ($allIdsCollection instanceof AbstractDb) { - $allIdsCollection->getSelect()->limit(); - $allIdsCollection->clear(); - } - - $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); - if (!empty($gridIds)) { - return join(",", $gridIds); - } - return ''; + return implode(',', $idList); } /** diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php index e8143b5f6b43a..e62b73f39241d 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php @@ -269,62 +269,6 @@ public function testGetGridIdsJsonWithoutUseSelectAll() $this->assertEmpty($this->_block->getGridIdsJson()); } - /** - * @param array $items - * @param string $result - * - * @dataProvider dataProviderGetGridIdsJsonWithUseSelectAll - */ - public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) - { - $this->_block->setUseSelectAll(true); - - if ($this->_block->getMassactionIdField()) { - $massActionIdField = $this->_block->getMassactionIdField(); - } else { - $massActionIdField = $this->_block->getParentBlock()->getMassactionIdField(); - } - - $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->_gridMock->expects($this->once()) - ->method('getCollection') - ->willReturn($collectionMock); - $collectionMock->expects($this->once()) - ->method('setPageSize') - ->with(0) - ->willReturnSelf(); - $collectionMock->expects($this->once()) - ->method('getColumnValues') - ->with($massActionIdField) - ->willReturn($items); - - $this->assertEquals($result, $this->_block->getGridIdsJson()); - } - - /** - * @return array - */ - public function dataProviderGetGridIdsJsonWithUseSelectAll() - { - return [ - [ - [], - '', - ], - [ - [1], - '1', - ], - [ - [1, 2, 3], - '1,2,3', - ], - ]; - } - /** * @param string $itemId * @param array|\Magento\Framework\DataObject $item diff --git a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml index 69d545f12d075..0a1dcb0b626e6 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml @@ -8,7 +8,7 @@ ?> getChildHtml()):?> -
getUiId('content-header') ?>> +
getUiId('content-header') ?>> getChildHtml() ?>
diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml index b3a6e5d795cea..ebc4ac1fb056a 100644 --- a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml @@ -19,8 +19,8 @@ + - diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js index e56cc6f32d804..f8d2f8bc11116 100644 --- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js +++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js @@ -374,8 +374,17 @@ define([ function applyTierPrice(oneItemPrice, qty, optionConfig) { var tiers = optionConfig.tierPrice, magicKey = _.keys(oneItemPrice)[0], + tiersFirstKey = _.keys(optionConfig)[0], lowest = false; + if (!tiers) {//tiers is undefined when options has only one option + tiers = optionConfig[tiersFirstKey].tierPrice; + } + + tiers.sort(function (a, b) {//sorting based on "price_qty" + return a['price_qty'] - b['price_qty']; + }); + _.each(tiers, function (tier, index) { if (tier['price_qty'] > qty) { return; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 0730e7a7c5dc1..342bbc388f872 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -6,8 +6,10 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Class Save @@ -16,75 +18,68 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute implements HttpPostActionInterface { /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var \Magento\Framework\Bulk\BulkManagementInterface */ - protected $_productFlatIndexerProcessor; + private $bulkManagement; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory */ - protected $_productPriceIndexerProcessor; + private $operationFactory; /** - * Catalog product - * - * @var \Magento\Catalog\Helper\Product + * @var \Magento\Framework\DataObject\IdentityGeneratorInterface */ - protected $_catalogProduct; + private $identityService; /** - * @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory + * @var \Magento\Framework\Serialize\SerializerInterface */ - protected $stockItemFactory; + private $serializer; /** - * Stock Indexer - * - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + * @var \Magento\Authorization\Model\UserContextInterface */ - protected $_stockIndexerProcessor; + private $userContext; /** - * @var \Magento\Framework\Api\DataObjectHelper + * @var int */ - protected $dataObjectHelper; + private $bulkSize; /** * @param Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper - * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor - * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor - * @param \Magento\Catalog\Helper\Product $catalogProduct - * @param \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory + * @param \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param int $bulkSize */ public function __construct( Action\Context $context, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, - \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, - \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, - \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, - \Magento\Catalog\Helper\Product $catalogProduct, - \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement, + \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory, + \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService, + \Magento\Framework\Serialize\SerializerInterface $serializer, + \Magento\Authorization\Model\UserContextInterface $userContext, + int $bulkSize = 100 ) { - $this->_productFlatIndexerProcessor = $productFlatIndexerProcessor; - $this->_productPriceIndexerProcessor = $productPriceIndexerProcessor; - $this->_stockIndexerProcessor = $stockIndexerProcessor; - $this->_catalogProduct = $catalogProduct; - $this->stockItemFactory = $stockItemFactory; parent::__construct($context, $attributeHelper); - $this->dataObjectHelper = $dataObjectHelper; + $this->bulkManagement = $bulkManagement; + $this->operationFactory = $operartionFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; + $this->userContext = $userContext; + $this->bulkSize = $bulkSize; } /** * Update product attributes * - * @return \Magento\Backend\Model\View\Result\Redirect - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -93,128 +88,184 @@ public function execute() } /* Collect Data */ - $inventoryData = $this->getRequest()->getParam('inventory', []); $attributesData = $this->getRequest()->getParam('attributes', []); $websiteRemoveData = $this->getRequest()->getParam('remove_website_ids', []); $websiteAddData = $this->getRequest()->getParam('add_website_ids', []); - /* Prepare inventory data item options (use config settings) */ - $options = $this->_objectManager->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) - ->getConfigItemOptions(); - foreach ($options as $option) { - if (isset($inventoryData[$option]) && !isset($inventoryData['use_config_' . $option])) { - $inventoryData['use_config_' . $option] = 0; - } - } + $storeId = $this->attributeHelper->getSelectedStoreId(); + $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); + $productIds = $this->attributeHelper->getProductIds(); + + $attributesData = $this->sanitizeProductAttributes($attributesData); try { - $storeId = $this->attributeHelper->getSelectedStoreId(); - if ($attributesData) { - $dateFormat = $this->_objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) - ->getDateFormat(\IntlDateFormatter::SHORT); - - foreach ($attributesData as $attributeCode => $value) { - $attribute = $this->_objectManager->get(\Magento\Eav\Model\Config::class) - ->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); - if (!$attribute->getAttributeId()) { - unset($attributesData[$attributeCode]); - continue; - } - if ($attribute->getBackendType() == 'datetime') { - if (!empty($value)) { - $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); - $filterInternal = new \Zend_Filter_NormalizedToLocalized( - ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] - ); - $value = $filterInternal->filter($filterInput->filter($value)); - } else { - $value = null; - } - $attributesData[$attributeCode] = $value; - } elseif ($attribute->getFrontendInput() == 'multiselect') { - // Check if 'Change' checkbox has been checked by admin for this attribute - $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); - if (!$isChanged) { - unset($attributesData[$attributeCode]); - continue; - } - if (is_array($value)) { - $value = implode(',', $value); - } - $attributesData[$attributeCode] = $value; - } - } + $this->publish($attributesData, $websiteRemoveData, $websiteAddData, $storeId, $websiteId, $productIds); + $this->messageManager->addSuccessMessage(__('Message is added to queue')); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while updating the product(s) attributes.') + ); + } - $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class) - ->updateAttributes($this->attributeHelper->getProductIds(), $attributesData, $storeId); - } + return $this->resultRedirectFactory->create()->setPath('catalog/product/', ['store' => $storeId]); + } - if ($inventoryData) { - // TODO why use ObjectManager? - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry */ - $stockRegistry = $this->_objectManager - ->create(\Magento\CatalogInventory\Api\StockRegistryInterface::class); - /** @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository */ - $stockItemRepository = $this->_objectManager - ->create(\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class); - foreach ($this->attributeHelper->getProductIds() as $productId) { - $stockItemDo = $stockRegistry->getStockItem( - $productId, - $this->attributeHelper->getStoreWebsiteId($storeId) - ); - if (!$stockItemDo->getProductId()) { - $inventoryData['product_id'] = $productId; - } - - $stockItemId = $stockItemDo->getId(); - $this->dataObjectHelper->populateWithArray( - $stockItemDo, - $inventoryData, - \Magento\CatalogInventory\Api\Data\StockItemInterface::class + /** + * Sanitize product attributes + * + * @param array $attributesData + * + * @return array + */ + private function sanitizeProductAttributes($attributesData) + { + $dateFormat = $this->_objectManager->get(TimezoneInterface::class)->getDateFormat(\IntlDateFormatter::SHORT); + $config = $this->_objectManager->get(\Magento\Eav\Model\Config::class); + + foreach ($attributesData as $attributeCode => $value) { + $attribute = $config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + if (!$attribute->getAttributeId()) { + unset($attributesData[$attributeCode]); + continue; + } + if ($attribute->getBackendType() === 'datetime') { + if (!empty($value)) { + $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); + $filterInternal = new \Zend_Filter_NormalizedToLocalized( + ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] ); - $stockItemDo->setItemId($stockItemId); - $stockItemRepository->save($stockItemDo); + $value = $filterInternal->filter($filterInput->filter($value)); + } else { + $value = null; } - $this->_stockIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); - } - - if ($websiteAddData || $websiteRemoveData) { - /* @var $actionModel \Magento\Catalog\Model\Product\Action */ - $actionModel = $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class); - $productIds = $this->attributeHelper->getProductIds(); - - if ($websiteRemoveData) { - $actionModel->updateWebsites($productIds, $websiteRemoveData, 'remove'); + $attributesData[$attributeCode] = $value; + } elseif ($attribute->getFrontendInput() === 'multiselect') { + // Check if 'Change' checkbox has been checked by admin for this attribute + $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); + if (!$isChanged) { + unset($attributesData[$attributeCode]); + continue; } - if ($websiteAddData) { - $actionModel->updateWebsites($productIds, $websiteAddData, 'add'); + if (is_array($value)) { + $value = implode(',', $value); } - - $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); + $attributesData[$attributeCode] = $value; } + } + return $attributesData; + } - $this->messageManager->addSuccessMessage( - __('A total of %1 record(s) were updated.', count($this->attributeHelper->getProductIds())) - ); - - $this->_productFlatIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); + /** + * Schedule new bulk + * + * @param array $attributesData + * @param array $websiteRemoveData + * @param array $websiteAddData + * @param int $storeId + * @param int $websiteId + * @param array $productIds + * @throws \Magento\Framework\Exception\LocalizedException + * + * @return void + */ + private function publish( + $attributesData, + $websiteRemoveData, + $websiteAddData, + $storeId, + $websiteId, + $productIds + ):void { + $productIdsChunks = array_chunk($productIds, $this->bulkSize); + $bulkUuid = $this->identityService->generateId(); + $bulkDescription = __('Update attributes for ' . count($productIds) . ' selected products'); + $operations = []; + foreach ($productIdsChunks as $productIdsChunk) { + if ($websiteRemoveData || $websiteAddData) { + $dataToUpdate = [ + 'website_assign' => $websiteAddData, + 'website_detach' => $websiteRemoveData + ]; + $operations[] = $this->makeOperation( + 'Update website assign', + 'product_action_attribute.website.update', + $dataToUpdate, + $storeId, + $websiteId, + $productIdsChunk, + $bulkUuid + ); + } - if ($this->_catalogProduct->isDataForPriceIndexerWasChanged($attributesData) - || !empty($websiteRemoveData) - || !empty($websiteAddData) - ) { - $this->_productPriceIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); + if ($attributesData) { + $operations[] = $this->makeOperation( + 'Update product attributes', + 'product_action_attribute.update', + $attributesData, + $storeId, + $websiteId, + $productIdsChunk, + $bulkUuid + ); } - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addExceptionMessage( - $e, - __('Something went wrong while updating the product(s) attributes.') + } + + if (!empty($operations)) { + $result = $this->bulkManagement->scheduleBulk( + $bulkUuid, + $operations, + $bulkDescription, + $this->userContext->getUserId() ); + if (!$result) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Something went wrong while processing the request.') + ); + } } + } + + /** + * Make asynchronous operation + * + * @param string $meta + * @param string $queue + * @param array $dataToUpdate + * @param int $storeId + * @param int $websiteId + * @param array $productIds + * @param int $bulkUuid + * + * @return OperationInterface + */ + private function makeOperation( + $meta, + $queue, + $dataToUpdate, + $storeId, + $websiteId, + $productIds, + $bulkUuid + ): OperationInterface { + $dataToEncode = [ + 'meta_information' => $meta, + 'product_ids' => $productIds, + 'store_id' => $storeId, + 'website_id' => $websiteId, + 'attributes' => $dataToUpdate + ]; + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => $queue, + 'serialized_data' => $this->serializer->serialize($dataToEncode), + 'status' => \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN, + ] + ]; - return $this->resultRedirectFactory->create() - ->setPath('catalog/product/', ['store' => $this->attributeHelper->getSelectedStoreId()]); + return $this->operationFactory->create($data); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index e84d9ff12906e..825d0ee032d6c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -159,6 +159,7 @@ public function execute() if ($redirectBack === 'duplicate') { $product->unsetData('quantity_and_stock_status'); $newProduct = $this->productCopier->copy($product); + $this->checkUniqueAttributes($product); $this->messageManager->addSuccessMessage(__('You duplicated the product.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -343,4 +344,25 @@ private function persistMediaData(ProductInterface $product, array $data) return $data; } + + /** + * Check unique attributes and add error to message manager + * + * @param \Magento\Catalog\Model\Product $product + */ + private function checkUniqueAttributes(\Magento\Catalog\Model\Product $product) + { + $uniqueLabels = []; + foreach ($product->getAttributes() as $attribute) { + if ($attribute->getIsUnique() && $attribute->getIsUserDefined() + && $product->getData($attribute->getAttributeCode()) !== null + ) { + $uniqueLabels[] = $attribute->getDefaultFrontendLabel(); + } + } + if ($uniqueLabels) { + $uniqueLabels = implode('", "', $uniqueLabels); + $this->messageManager->addErrorMessage(__('The value of attribute(s) "%1" must be unique', $uniqueLabels)); + } + } } diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php new file mode 100644 index 0000000000000..dc24a3090481e --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php @@ -0,0 +1,163 @@ +catalogProduct = $catalogProduct; + $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; + $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->productAction = $action; + $this->logger = $logger; + $this->serializer = $serializer; + $this->operationManagement = $operationManagement; + $this->entityManager = $entityManager; + } + + /** + * Process + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation + * @throws \Exception + * + * @return void + */ + public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->execute($data); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException + || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException + || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException + ) { + $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } else { + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __( + 'Sorry, something went wrong during product attributes update. Please see log for details.' + ); + } + } catch (NoSuchEntityException $e) { + $this->logger->critical($e->getMessage()); + $status = ($e instanceof TemporaryStateExceptionInterface) + ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (LocalizedException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during product attributes update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } + + /** + * Execute + * + * @param array $data + * + * @return void + */ + private function execute($data): void + { + $this->productAction->updateAttributes($data['product_ids'], $data['attributes'], $data['store_id']); + if ($this->catalogProduct->isDataForPriceIndexerWasChanged($data['attributes'])) { + $this->productPriceIndexerProcessor->reindexList($data['product_ids']); + } + + $this->productFlatIndexerProcessor->reindexList($data['product_ids']); + } +} diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php new file mode 100644 index 0000000000000..32ba39d9afd98 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php @@ -0,0 +1,168 @@ +productFlatIndexerProcessor = $productFlatIndexerProcessor; + $this->productAction = $action; + $this->logger = $logger; + $this->serializer = $serializer; + $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->entityManager = $entityManager; + } + + /** + * Process + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation + * @throws \Exception + * + * @return void + */ + public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->execute($data); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException + || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException + || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException + ) { + $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __($e->getMessage()); + } else { + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __( + 'Sorry, something went wrong during product attributes update. Please see log for details.' + ); + } + } catch (NoSuchEntityException $e) { + $this->logger->critical($e->getMessage()); + $status = ($e instanceof TemporaryStateExceptionInterface) + ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (LocalizedException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during product attributes update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } + + /** + * Update website in products + * + * @param array $productIds + * @param array $websiteRemoveData + * @param array $websiteAddData + * + * @return void + */ + private function updateWebsiteInProducts($productIds, $websiteRemoveData, $websiteAddData): void + { + if ($websiteRemoveData) { + $this->productAction->updateWebsites($productIds, $websiteRemoveData, 'remove'); + } + if ($websiteAddData) { + $this->productAction->updateWebsites($productIds, $websiteAddData, 'add'); + } + } + + /** + * Execute + * + * @param array $data + * + * @return void + */ + private function execute($data): void + { + $this->updateWebsiteInProducts( + $data['product_ids'], + $data['attributes']['website_detach'], + $data['attributes']['website_assign'] + ); + $this->productPriceIndexerProcessor->reindexList($data['product_ids']); + $this->productFlatIndexerProcessor->reindexList($data['product_ids']); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Action.php b/app/code/Magento/Catalog/Model/Product/Action.php index f78048424b42c..3863cf2457247 100644 --- a/app/code/Magento/Catalog/Model/Product/Action.php +++ b/app/code/Magento/Catalog/Model/Product/Action.php @@ -168,5 +168,7 @@ public function updateWebsites($productIds, $websiteIds, $type) if (!$categoryIndexer->isScheduled()) { $categoryIndexer->reindexList(array_unique($productIds)); } + + $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); } } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index d786795bc3090..90d732c9654e1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -302,4 +302,18 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 20ea93cd272f1..da570f9ed99b0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -20,7 +20,7 @@ - + @@ -34,6 +34,25 @@ + + + + + + + + + + + + + + + + + + + @@ -77,6 +96,11 @@ + + + + + @@ -389,6 +413,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml new file mode 100644 index 0000000000000..7917fe68aaebc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml new file mode 100644 index 0000000000000..963c9d9f1c911 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..1bb7c179dfca8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..6cb156723b286 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..3c62ef89e584b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..85d3927a6d6d0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml new file mode 100644 index 0000000000000..f5fabae5fc4ce --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml index 4c6b0749a0f9e..31783526932b6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml @@ -12,4 +12,9 @@ 0 + + 55.55 + 0 + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 0b3a31194ea36..383797933074e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -60,6 +60,48 @@ EavStockItem CustomAttributeCategoryIds + + SimpleProductForTest1 + simple + 4 + SimpleProductAfterImport1 + 250.00 + 4 + 1 + 100 + simple-product-for-test-1 + 1 + EavStockItem + CustomAttributeCategoryIds + + + SimpleProductForTest2 + simple + 4 + SimpleProductAfterImport2 + 300.00 + 4 + 1 + 100 + simple-product-for-test-2 + 1 + EavStockItem + CustomAttributeCategoryIds + + + SimpleProductForTest3 + simple + 4 + SimpleProductAfterImport3 + 350.00 + 4 + 1 + 100 + simple-product-for-test-3 + 1 + EavStockItem + CustomAttributeCategoryIds + SimpleProduct simple @@ -386,6 +428,11 @@ ProductOptionDropDownWithLongValuesTitle + + + ProductOptionField + ProductOptionArea + api-virtual-product virtual @@ -906,4 +953,20 @@ EavStockItem CustomAttributeCategoryIds + + sku_simple_product_ + simple + 4 + 4 + SimpleProduct + 560 + simple-product- + 1 + 25 + 1 + 1 + 1 + 2 + EavStockItem + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml index fee86ca1caa29..ea4f4bf53eb71 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml index e218f5ae74fc0..2de7bf19fd378 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml @@ -15,7 +15,7 @@ - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml index 4dcda8dcd41ae..c58479a7b73e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -11,6 +11,6 @@
- +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 8504683648bce..8393cee57996f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,6 +13,7 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml index 45e0b03e8d995..ea10e12fb73f5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml @@ -9,6 +9,11 @@
+ + + + +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml index 09b49011938e8..a3f543e9cf32a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -16,6 +16,9 @@ + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml new file mode 100644 index 0000000000000..37ec4e0d32528 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml @@ -0,0 +1,41 @@ + + + + + + + + <description value="Login as admin and create duplicate category"/> + <testCaseId value="MC-14702"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category Page and select Add category --> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCategoryPage"/> + + <!-- Fill the Category form with same name and urlKey as initially created category(SimpleSubCategory) --> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillCategoryForm"> + <argument name="categoryName" value="$$category.name$$"/> + <argument name="categoryUrlKey" value="$$category.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Assert error message --> + <see selector="{{AdminCategoryMessagesSection.errorMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml new file mode 100644 index 0000000000000..575bb56912b25 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml @@ -0,0 +1,61 @@ +<?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="AdminCreateDuplicateProductTest"> + <annotations> + <stories value="Create Product"/> + <title value="Create Duplicate Product With Existed Subcategory Name And UrlKey"/> + <description value="Login as admin and create duplicate Product"/> + <testCaseId value="MC-14714"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SubCategory" stepKey="category"/> + <createData entity="Two_nested_categories" stepKey="subCategory"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new simple product page --> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="goToCreateProductPage"/> + + <!-- Fill the main fields in the form --> + <actionGroup ref="FillMainProductFormByString" stepKey="fillMainProductForm"> + <argument name="productName" value="$$subCategory.name$$"/> + <argument name="productSku" value="{{defaultSimpleProduct.sku}}"/> + <argument name="productPrice" value="{{defaultSimpleProduct.price}}"/> + <argument name="productQuantity" value="{{defaultSimpleProduct.quantity}}"/> + <argument name="productStatus" value="{{defaultSimpleProduct.status}}"/> + <argument name="productWeight" value="{{defaultSimpleProduct.weight}}"/> + </actionGroup> + + <!-- Select the category that we created in the before block --> + <actionGroup ref="SetCategoryByName" stepKey="setCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + + <!-- Set the url key to match the subcategory created in the before block --> + <actionGroup ref="SetProductUrlKeyByString" stepKey="fillUrlKey"> + <argument name="urlKey" value="$$subCategory.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Save the product and expect to see an error message --> + <actionGroup ref="SaveProductFormNoSuccessCheck" stepKey="tryToSaveProduct"/> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 70edb0ce3ea7d..17769c79677f7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -139,7 +139,7 @@ </assertEquals> <!--Verify we see customizable options are Required --> - <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFistCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index d7607b4b269e8..4d581bae700d7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -54,7 +54,13 @@ <fillField stepKey="fillPrice" selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="90.99"/> <click stepKey="clickOnSaveButton" selector="{{AdminEditProductAttributesSection.Save}}"/> <waitForPageLoad stepKey="waitForUpdatedProductToSave" /> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!--Verify product name, sku and updated price--> <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct1.sku$$)}}"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index c0eebd1512d6d..8a44c8093ca5e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -58,7 +58,13 @@ <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$$createProductOne.price$$0" stepKey="fillAttributeNameField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index 845c47c0e4c20..bee13bec370da 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -52,7 +52,13 @@ <click selector="{{AdminEditProductAttributesSection.ChangeAttributeDescriptionToggle}}" stepKey="toggleToChangeDescription"/> <fillField selector="{{AdminEditProductAttributesSection.AttributeDescription}}" userInput="Updated $$createProductOne.custom_attributes[description]$$" stepKey="fillAttributeDescriptionField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index c9a37ec40e8fa..318ab6555235e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -135,7 +135,7 @@ </assertEquals> <!--Verify customer see customizable options are Required --> - <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(simpleProductCustomizableOption.title)}}" stepKey="verifyFistCustomOptionIsRequired"/> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(simpleProductCustomizableOption.title)}}" stepKey="verifyFirstCustomOptionIsRequired"/> <!--Verify customer see customizable option titles and prices --> <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(simpleProductCustomizableOption.title)}}" stepKey="simpleOptionId"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml index d67d5b36109e6..34d85e7b46850 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -229,7 +229,7 @@ <actualResult type="variable">productPriceAmount</actualResult> </assertEquals> <!--Verify we customer see customizable options are Required --> - <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFistCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php deleted file mode 100644 index de44af7f58afc..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Action\Attribute; - -/** - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save */ - protected $object; - - /** @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute|\PHPUnit_Framework_MockObject_MockObject */ - protected $attributeHelper; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $dataObjectHelperMock; - - /** @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockIndexerProcessor; - - /** @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; - - /** @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; - - /** @var \Magento\Framework\App\Response\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; - - /** @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManager; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $url; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirect; - - /** @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject */ - protected $actionFlag; - - /** @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $view; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManager; - - /** @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $session; - - /** @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $authorization; - - /** @var \Magento\Backend\Model\Auth|\PHPUnit_Framework_MockObject_MockObject */ - protected $auth; - - /** @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $helper; - - /** @var \Magento\Backend\Model\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $backendUrl; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $formKeyValidator; - - /** @var \Magento\Framework\Locale\ResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $localeResolver; - - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; - - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemService; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $stockItem; - - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockConfig; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemRepository; - - /** - * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectFactory; - - protected function setUp() - { - $this->attributeHelper = $this->createPartialMock( - \Magento\Catalog\Helper\Product\Edit\Action\Attribute::class, - ['getProductIds', 'getSelectedStoreId', 'getStoreWebsiteId'] - ); - - $this->dataObjectHelperMock = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->stockIndexerProcessor = $this->createPartialMock( - \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class, - ['reindexList'] - ); - - $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultRedirectFactory = $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->resultRedirectFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($resultRedirect); - - $this->prepareContext(); - - $this->object = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( - \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::class, - [ - 'context' => $this->context, - 'attributeHelper' => $this->attributeHelper, - 'stockIndexerProcessor' => $this->stockIndexerProcessor, - 'dataObjectHelper' => $this->dataObjectHelperMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function prepareContext() - { - $this->stockItemRepository = $this->getMockBuilder( - \Magento\CatalogInventory\Api\StockItemRepositoryInterface::class - )->disableOriginalConstructor()->getMock(); - - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); - $this->objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $this->actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); - $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); - $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); - $this->session = $this->createMock(\Magento\Backend\Model\Session::class); - $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class); - $this->auth = $this->createMock(\Magento\Backend\Model\Auth::class); - $this->helper = $this->createMock(\Magento\Backend\Helper\Data::class); - $this->backendUrl = $this->createMock(\Magento\Backend\Model\UrlInterface::class); - $this->formKeyValidator = $this->createMock(\Magento\Framework\Data\Form\FormKey\Validator::class); - $this->localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); - - $this->context = $this->context = $this->createPartialMock(\Magento\Backend\App\Action\Context::class, [ - 'getRequest', - 'getResponse', - 'getObjectManager', - 'getEventManager', - 'getUrl', - 'getRedirect', - 'getActionFlag', - 'getView', - 'getMessageManager', - 'getSession', - 'getAuthorization', - 'getAuth', - 'getHelper', - 'getBackendUrl', - 'getFormKeyValidator', - 'getLocaleResolver', - 'getResultRedirectFactory' - ]); - $this->context->expects($this->any())->method('getRequest')->willReturn($this->request); - $this->context->expects($this->any())->method('getResponse')->willReturn($this->response); - $this->context->expects($this->any())->method('getObjectManager')->willReturn($this->objectManager); - $this->context->expects($this->any())->method('getEventManager')->willReturn($this->eventManager); - $this->context->expects($this->any())->method('getUrl')->willReturn($this->url); - $this->context->expects($this->any())->method('getRedirect')->willReturn($this->redirect); - $this->context->expects($this->any())->method('getActionFlag')->willReturn($this->actionFlag); - $this->context->expects($this->any())->method('getView')->willReturn($this->view); - $this->context->expects($this->any())->method('getMessageManager')->willReturn($this->messageManager); - $this->context->expects($this->any())->method('getSession')->willReturn($this->session); - $this->context->expects($this->any())->method('getAuthorization')->willReturn($this->authorization); - $this->context->expects($this->any())->method('getAuth')->willReturn($this->auth); - $this->context->expects($this->any())->method('getHelper')->willReturn($this->helper); - $this->context->expects($this->any())->method('getBackendUrl')->willReturn($this->backendUrl); - $this->context->expects($this->any())->method('getFormKeyValidator')->willReturn($this->formKeyValidator); - $this->context->expects($this->any())->method('getLocaleResolver')->willReturn($this->localeResolver); - $this->context->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($this->resultRedirectFactory); - - $this->product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, - ['isProductsHasSku', '__wakeup'] - ); - - $this->stockItemService = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getStockItem', 'saveStockItem']) - ->getMockForAbstractClass(); - $this->stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) - ->setMethods(['getId', 'getProductId']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->stockConfig = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->objectManager->expects($this->any())->method('create')->will($this->returnValueMap([ - [\Magento\Catalog\Model\Product::class, [], $this->product], - [\Magento\CatalogInventory\Api\StockRegistryInterface::class, [], $this->stockItemService], - [\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class, [], $this->stockItemRepository], - ])); - - $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ - [\Magento\CatalogInventory\Api\StockConfigurationInterface::class, $this->stockConfig], - ])); - } - - public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() - { - $this->attributeHelper->expects($this->any())->method('getProductIds')->will($this->returnValue([5])); - $this->attributeHelper->expects($this->any())->method('getSelectedStoreId')->will($this->returnValue([1])); - $this->attributeHelper->expects($this->any())->method('getStoreWebsiteId')->will($this->returnValue(1)); - $this->stockConfig->expects($this->any())->method('getConfigItemOptions')->will($this->returnValue([])); - $this->dataObjectHelperMock->expects($this->any()) - ->method('populateWithArray') - ->with($this->stockItem, $this->anything(), \Magento\CatalogInventory\Api\Data\StockItemInterface::class) - ->willReturnSelf(); - $this->product->expects($this->any())->method('isProductsHasSku')->with([5])->will($this->returnValue(true)); - $this->stockItemService->expects($this->any())->method('getStockItem')->with(5, 1) - ->will($this->returnValue($this->stockItem)); - $this->stockIndexerProcessor->expects($this->any())->method('reindexList')->with([5]); - - $this->request->expects($this->any())->method('getParam')->will($this->returnValueMap([ - ['inventory', [], [7]], - ])); - - $this->messageManager->expects($this->never())->method('addErrorMessage'); - $this->messageManager->expects($this->never())->method('addExceptionMessage'); - - $this->object->execute(); - } -} diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 44d051933909b..5c3ee3da8ca81 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -7,6 +7,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-asynchronous-operations": "*", "magento/module-backend": "*", "magento/module-catalog-inventory": "*", "magento/module-catalog-rule": "*", diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 7a05601fcd666..1d563244f1432 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -59,7 +59,7 @@ <field id="grid_per_page_values" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Default Value</label> @@ -69,7 +69,7 @@ <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Default Value</label> diff --git a/app/code/Magento/Catalog/etc/communication.xml b/app/code/Magento/Catalog/etc/communication.xml new file mode 100644 index 0000000000000..1a957f6ac9fe5 --- /dev/null +++ b/app/code/Magento/Catalog/etc/communication.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="product_action_attribute.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="product_action_attribute.update" type="Magento\Catalog\Model\Attribute\Backend\Consumer" method="process" /> + </topic> + <topic name="product_action_attribute.website.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="product_action_attribute.website.update" type="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign" method="process" /> + </topic> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 7d2c3699ee2c2..49447447622f9 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -72,6 +72,7 @@ <preference for="Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface" type="Magento\Catalog\Model\Indexer\Product\Price\InvalidateIndex" /> <preference for="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactoryInterface" type="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactory" /> <preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" /> + <preference for="Magento\Catalog\Api\Data\MassActionInterface" type="\Magento\Catalog\Model\MassAction" /> <type name="Magento\Customer\Model\ResourceModel\Visitor"> <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> diff --git a/app/code/Magento/Catalog/etc/queue.xml b/app/code/Magento/Catalog/etc/queue.xml new file mode 100644 index 0000000000000..137f34a5c1e25 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="product_action_attribute.update" exchange="magento-db" type="db"> + <queue name="product_action_attribute.update" consumer="product_action_attribute.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process"/> + </broker> + <broker topic="product_action_attribute.website.update" exchange="magento-db" type="db"> + <queue name="product_action_attribute.website.update" consumer="product_action_attribute.website.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process"/> + </broker> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_consumer.xml b/app/code/Magento/Catalog/etc/queue_consumer.xml new file mode 100644 index 0000000000000..d9e66ae69c10c --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="product_action_attribute.update" queue="product_action_attribute.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process" /> + <consumer name="product_action_attribute.website.update" queue="product_action_attribute.website.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process" /> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_publisher.xml b/app/code/Magento/Catalog/etc/queue_publisher.xml new file mode 100644 index 0000000000000..1606ea42ec0b3 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_publisher.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> + <publisher topic="product_action_attribute.update"> + <connection name="db" exchange="magento-db" /> + </publisher> + <publisher topic="product_action_attribute.website.update"> + <connection name="db" exchange="magento-db" /> + </publisher> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_topology.xml b/app/code/Magento/Catalog/etc/queue_topology.xml new file mode 100644 index 0000000000000..bdac891afbdb8 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_topology.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="updateBinding" topic="product_action_attribute.update" destinationType="queue" destination="product_action_attribute.update"/> + <binding id="updateBindingWebsite" topic="product_action_attribute.website.update" destinationType="queue" destination="product_action_attribute.website.update"/> + </exchange> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml index c930d2195a01b..1c4a37fedebe3 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml @@ -23,8 +23,8 @@ <tbody> <?php foreach ($_additional as $_data): ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__($_data['label'])) ?></th> - <td class="col data" data-th="<?= $block->escapeHtml(__($_data['label'])) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> + <th class="col label" scope="row"><?= $block->escapeHtml($_data['label']) ?></th> + <td class="col data" data-th="<?= $block->escapeHtml($_data['label']) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> </tr> <?php endforeach; ?> </tbody> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml new file mode 100644 index 0000000000000..b9eea2b114634 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml @@ -0,0 +1,65 @@ +<?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"> + + <!-- Export products using filtering by attribute --> + <actionGroup name="exportProductsFilterByAttribute"> + <arguments> + <argument name="attribute" type="string"/> + <argument name="attributeData" type="string"/> + </arguments> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> + <scrollTo selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="scrollToAttribute" /> + <checkOption selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="selectAttribute"/> + <fillField selector="{{AdminExportAttributeSection.fillFilter('attribute')}}" userInput="{{attributeData}}" stepKey="setDataInField"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue" /> + <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Export products without filtering --> + <actionGroup name="exportAllProducts"> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible" time="5"/> + <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue"/> + <wait stepKey="waitForScroll" time="5"/> + <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> + <wait stepKey="waitForClick" time="5"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Download first file in the grid --> + <actionGroup name="downloadFileByRowIndex"> + <arguments> + <argument name="rowIndex" type="string"/> + </arguments> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormReload"/> + <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> + <click stepKey="clickOnDownload" selector="{{AdminExportAttributeSection.download(rowIndex)}}" after="clickSelectBtn"/> + </actionGroup> + + <!-- Delete exported file --> + <actionGroup name="deleteExportedFile"> + <arguments> + <argument name="rowIndex" type="string"/> + </arguments> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormReload"/> + <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> + <click stepKey="clickOnDelete" selector="{{AdminExportAttributeSection.delete(rowIndex)}}" after="clickSelectBtn"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForExportDataDeleted" /> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml new file mode 100644 index 0000000000000..1f5ae6b6905bc --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml @@ -0,0 +1,126 @@ +<?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="AdminExportBundleProductTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Bundle Product"/> + <description value="Admin should be able to export Bundle Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14008"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Create bundle product with dynamic price with two simple products --> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForDynamic"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForDynamic"/> + <createData entity="ApiBundleProduct" stepKey="createDynamicBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="firstSimpleProductForDynamic"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="secondSimpleProductForDynamic"/> + </createData> + + <!-- Create bundle product with fixed price with two simple products --> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixed"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixed"/> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="firstSimpleProductForFixed"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="secondSimpleProductForFixed"/> + </createData> + + <!-- Create bundle product with custom textarea attribute with two simple products --> + <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProductWithAttribute"> + <requiredEntity createDataKey="addToDefaultAttributeSet"/> + </createData> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixedWithAttribute"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixedWithAttribute"/> + <createData entity="DropDownBundleOption" stepKey="createBundleOptionWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProductWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + <requiredEntity createDataKey="createBundleOptionWithAttribute"/> + <requiredEntity createDataKey="firstSimpleProductForFixedWithAttribute"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProductWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + <requiredEntity createDataKey="createBundleOptionWithAttribute"/> + <requiredEntity createDataKey="secondSimpleProductForFixedWithAttribute"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete products creations --> + <deleteData createDataKey="createDynamicBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="firstSimpleProductForDynamic" stepKey="deleteFirstSimpleProductForDynamic"/> + <deleteData createDataKey="secondSimpleProductForDynamic" stepKey="deleteSecondSimpleProductForDynamic"/> + <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteFixedBundleProduct"/> + <deleteData createDataKey="firstSimpleProductForFixed" stepKey="deleteFirstSimpleProductForFixed"/> + <deleteData createDataKey="secondSimpleProductForFixed" stepKey="deleteSecondSimpleProductForFixed"/> + <deleteData createDataKey="createFixedBundleProductWithAttribute" stepKey="deleteFixedBundleProductWithAttribute"/> + <deleteData createDataKey="firstSimpleProductForFixedWithAttribute" stepKey="deleteFirstSimpleProductForFixedWithAttribute"/> + <deleteData createDataKey="secondSimpleProductForFixedWithAttribute" stepKey="deleteSecondSimpleProductForFixedWithAttribute"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml new file mode 100644 index 0000000000000..a587d71ba0e68 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -0,0 +1,91 @@ +<?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="AdminExportGroupedProductWithSpecialPriceTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export grouped product with special price"/> + <description value="Admin should be able to export grouped product with special price"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14009"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create first simple product and add special price --> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="specialProductPrice2" stepKey="specialPriceToFirstProduct"> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + + <!-- Create second simple product and add special price--> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="specialProductPrice2" stepKey="specialPriceToSecondProduct"> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create group product with created below simple products --> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </updateData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Deleted created products --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml new file mode 100644 index 0000000000000..6f64da4693692 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -0,0 +1,118 @@ +<?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="AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple and Configurable products with custom options"/> + <description value="Admin should be able to export Simple and Configurable products with custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14005"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create configurable product with two attributes --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Add custom options to configurable product --> + <updateData createDataKey="createConfigProduct" entity="productWithOptions" stepKey="updateProductWithOptions"/> + + <!-- Create two simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add created below children products to configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Fill entity attributes data --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml new file mode 100644 index 0000000000000..993f1c9cd9da2 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -0,0 +1,134 @@ +<?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="AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple product and Configurable products with assigned images"/> + <description value="Admin should be able to export Simple and Configurable products with assigned images"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14004"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create configurable product with two attributes --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create first simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + + <!-- Add image to first simple product --> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigChildFirstProductImage"> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + + <!-- Create second simple product which will be the part of configurable product --> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add image to second simple product --> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createConfigSecondChildProductImage"> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Add two options to configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add created below children products to configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Add image to configurable product --> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigProductImage"> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Fill entity attributes data --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml new file mode 100644 index 0000000000000..491d20604a08b --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml @@ -0,0 +1,116 @@ +<?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="AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> + <description value="Admin should be able to export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14006"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create configurable product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create two simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete simple product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml new file mode 100644 index 0000000000000..f671b54803e35 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -0,0 +1,68 @@ +<?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="AdminExportSimpleProductWithCustomAttributeTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple Product with custom attribute"/> + <description value="Admin should be able to export Simple Product with custom attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14007"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create simple product with custom attribute set --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="SimpleProductWithCustomAttributeSet" stepKey="createSimpleProductWithCustomAttributeSet"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createAttributeSet"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete product creations --> + <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php index f711268bc7930..0fa4b919c40fa 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php @@ -7,16 +7,24 @@ /** * Interface StockRegistryProviderInterface + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockRegistryProviderInterface { /** + * Get stock. + * * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockInterface */ public function getStock($scopeId); /** + * Get stock item. + * * @param int $productId * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockItemInterface @@ -24,6 +32,8 @@ public function getStock($scopeId); public function getStockItem($productId, $scopeId); /** + * Get stock status. + * * @param int $productId * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockStatusInterface diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php index 89fb54e7e496b..30f703b5b928f 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php @@ -9,22 +9,32 @@ /** * Interface StockStateProviderInterface + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStateProviderInterface { /** + * Verify stock. + * * @param StockItemInterface $stockItem * @return bool */ public function verifyStock(StockItemInterface $stockItem); /** + * Verify notification. + * * @param StockItemInterface $stockItem * @return bool */ public function verifyNotification(StockItemInterface $stockItem); /** + * Validate quote qty. + * * @param StockItemInterface $stockItem * @param int|float $itemQty * @param int|float $qtyToCheck @@ -44,8 +54,9 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $itemQty, $qtyT public function checkQty(StockItemInterface $stockItem, $qty); /** - * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions - * or original qty if such value does not exist + * Returns suggested qty or original qty if such value does not exist. + * + * Suggested qty satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions. * * @param StockItemInterface $stockItem * @param int|float $qty @@ -54,6 +65,8 @@ public function checkQty(StockItemInterface $stockItem, $qty); public function suggestQty(StockItemInterface $stockItem, $qty); /** + * Check qty increments. + * * @param StockItemInterface $stockItem * @param int|float $qty * @return \Magento\Framework\DataObject diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php new file mode 100644 index 0000000000000..334d2b22edbfa --- /dev/null +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Plugin; + +use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; +use Magento\CatalogInventory\Api\Data\StockItemInterface; + +/** + * MassUpdate product attribute. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MassUpdateProductAttribute +{ + /** + * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + */ + private $stockIndexerProcessor; + + /** + * @var \Magento\Framework\Api\DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var \Magento\CatalogInventory\Api\StockRegistryInterface + */ + private $stockRegistry; + + /** + * @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var \Magento\CatalogInventory\Api\StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute + */ + private $attributeHelper; + + /** + * @var \Magento\Framework\Message\ManagerInterface + */ + private $messageManager; + + /** + * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor + * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration + * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper + * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, + \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, + \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, + \Magento\Framework\Message\ManagerInterface $messageManager + ) { + $this->stockIndexerProcessor = $stockIndexerProcessor; + $this->dataObjectHelper = $dataObjectHelper; + $this->stockRegistry = $stockRegistry; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + $this->attributeHelper = $attributeHelper; + $this->messageManager = $messageManager; + } + + /** + * Around execute plugin + * + * @param Save $subject + * @param callable $proceed + * + * @return \Magento\Framework\Controller\ResultInterface + */ + public function aroundExecute(Save $subject, callable $proceed) + { + try { + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = $subject->getRequest(); + $inventoryData = $request->getParam('inventory', []); + $inventoryData = $this->addConfigSettings($inventoryData); + + $storeId = $this->attributeHelper->getSelectedStoreId(); + $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); + $productIds = $this->attributeHelper->getProductIds(); + + if (!empty($inventoryData)) { + $this->updateInventoryInProducts($productIds, $websiteId, $inventoryData); + } + + return $proceed(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + return $proceed(); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while updating the product(s) attributes.') + ); + return $proceed(); + } + } + + /** + * Add config settings + * + * @param array $inventoryData + * + * @return array + */ + private function addConfigSettings($inventoryData) + { + $options = $this->stockConfiguration->getConfigItemOptions(); + foreach ($options as $option) { + $useConfig = 'use_config_' . $option; + if (isset($inventoryData[$option]) && !isset($inventoryData[$useConfig])) { + $inventoryData[$useConfig] = 0; + } + } + return $inventoryData; + } + + /** + * Update inventory in products + * + * @param array $productIds + * @param int $websiteId + * @param array $inventoryData + * + * @return void + */ + private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void + { + foreach ($productIds as $productId) { + $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); + if (!$stockItemDo->getProductId()) { + $inventoryData['product_id'] = $productId; + } + $stockItemId = $stockItemDo->getId(); + $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); + $stockItemDo->setItemId($stockItemId); + $this->stockItemRepository->save($stockItemDo); + } + $this->stockIndexerProcessor->reindexList($productIds); + } +} diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 51a5b46b59d22..e7d79c593b8c7 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -135,4 +135,7 @@ <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> </type> + <type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save"> + <plugin name="massAction" type="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute" /> + </type> </config> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml new file mode 100644 index 0000000000000..33ffa4fe1b296 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml @@ -0,0 +1,59 @@ +<?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="AssertSearchTermSaveSuccessMessage"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="storeValue" type="string"/> + <argument name="redirectUrl" type="string"/> + <argument name="displayInSuggestedTerm" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.addNewSearchTermButton}}" stepKey="clickAddNewSearchTermButton"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermNewPageLoad"/> + <fillField selector="{{AdminCatalogSearchTermNewSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQueryTextBox"/> + <selectOption selector="{{AdminCatalogSearchTermNewSection.store}}" userInput="{{storeValue}}" stepKey="selectStoreValue"/> + <fillField selector="{{AdminCatalogSearchTermNewSection.redirectUrl}}" userInput="{{redirectUrl}}" stepKey="fillRedirectUrl"/> + <selectOption selector="{{AdminCatalogSearchTermNewSection.displayInSuggestedTerm}}" userInput="{{displayInSuggestedTerm}}" stepKey="selectDisplayInSuggestedTerm"/> + <click selector="{{AdminCatalogSearchTermNewSection.saveSearchButton}}" stepKey="clickSaveSearchButton"/> + <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="You saved the search term." stepKey="seeSaveSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertSearchTermSuccessDeleteMessage"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.nthRow('1')}}" stepKey="checkFirstRow"/> + <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> + <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> + <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> + <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="Total of 1 record(s) were deleted." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertSearchTermNotInGrid"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultToLoad"/> + <see selector="{{AdminCatalogSearchTermIndexSection.emptyRecords}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml index 6b913e5b458e6..4b52b2c669edf 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml @@ -11,9 +11,9 @@ <!-- Quick search the phrase and check if the result page contains correct information --> <actionGroup name="StorefrontCheckQuickSearchActionGroup"> <arguments> - <argument name="phrase"/> + <argument name="phrase" /> </arguments> - <submitForm selector="#search_mini_form" parameterArray="['q' => '{{phrase}}']" stepKey="fillQuickSearch" /> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{phrase}}]" stepKey="fillQuickSearch" /> <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> @@ -116,4 +116,9 @@ <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="clickSubmit"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <!-- Asserts that search results do not contain any results--> + <actionGroup name="StorefrontCheckSearchIsEmpty"> + <see stepKey="checkEmpty" selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="Your search returned no results"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml new file mode 100644 index 0000000000000..83e4ac50a74e6 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml @@ -0,0 +1,37 @@ +<?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"> + <!--Verify AssertSearchTermNotOnFrontend--> + <actionGroup name="AssertSearchTermNotOnFrontend"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="url_key" type="string"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url('url_key')}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="Your search returned no results." stepKey="seeAssertSearchTermNotOnFrontendNoticeMessage"/> + </actionGroup> + + <actionGroup name="AssertSearchTermOnFrontend"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="redirectUrl" type="string"/> + </arguments> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <waitForPageLoad stepKey="waitForFillField"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <seeInCurrentUrl url="{{redirectUrl}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml new file mode 100644 index 0000000000000..995b860d107ca --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SimpleTerm" type="searchTerm"> + <data key="search_query" unique="suffix">Query text</data> + <data key="store_id">Default Store View</data> + <data key="redirect" unique="suffix">http://example.com/</data> + <data key="display_in_terms">No</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml new file mode 100644 index 0000000000000..bbafff8ad7739 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchTermIndexPage" url="/search/term/index/" area="admin" module="Magento_CatalogSearch"> + <section name="AdminCatalogSearchTermIndexSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml new file mode 100644 index 0000000000000..de7491471741c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchTermNewPage" url="/search/term/new/" area="admin" module="Magento_CatalogSearch"> + <section name="AdminCatalogSearchTermNewSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml new file mode 100644 index 0000000000000..ac316d060f6e9 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml @@ -0,0 +1,23 @@ +<?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="AdminCatalogSearchTermIndexSection"> + <element name="addNewSearchTermButton" type="button" selector="//div[@class='page-actions-buttons']/button[@id='add']" timeout="30"/> + <element name="resetFilterButton" type="button" selector="//button[@class='action-default scalable action-reset action-tertiary']" timeout="30"/> + <element name="searchButton" type="button" selector="//button[@class='action-default scalable action-secondary']" timeout="30"/> + <element name="massActions" type="text" selector="//div[@class='admin__grid-massaction-form']//select[@id='search_term_grid_massaction-select']"/> + <element name="submit" type="button" selector="//button[@class='action-default scalable']/span" timeout="30"/> + <element name="searchQuery" type="text" selector="//tr[@class='data-grid-filters']//td/input[@name='search_query']"/> + <element name="nthRow" type="checkbox" selector="//tbody/tr['{{rowNum}}']//input[@name='search']" parameterized="true"/> + <element name="searchTermRowCheckboxBySearchQuery" type="checkbox" selector="//*[normalize-space()='{{var1}}']/preceding-sibling::td//input[@name='search']" parameterized="true" timeout="30"/> + <element name="okButton" type="button" selector="//button[@class='action-primary action-accept']/span" timeout="30"/> + <element name="emptyRecords" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml new file mode 100644 index 0000000000000..5d19198a1b94c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml @@ -0,0 +1,14 @@ +<?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="AdminCatalogSearchTermMessagesSection"> + <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div[@data-ui-id='messages-message-success']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml new file mode 100644 index 0000000000000..a7d577a7508c0 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml @@ -0,0 +1,18 @@ +<?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="AdminCatalogSearchTermNewSection"> + <element name="searchQuery" type="text" selector="//div[@class='admin__field-control control']/input[@id='query_text']"/> + <element name="store" type="text" selector="//select[@id='store_id']"/> + <element name="redirectUrl" type="text" selector="//div[@class='admin__field-control control']/input[@id='redirect']"/> + <element name="displayInSuggestedTerm" type="select" selector="//select[@name='display_in_terms']"/> + <element name="saveSearchButton" type="button" selector="//button[@id='save']/span[@class='ui-button-text']" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml new file mode 100644 index 0000000000000..2b425f34f8a5b --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml @@ -0,0 +1,59 @@ +<?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="AdminCreateSearchTermEntityTest"> + <annotations> + <stories value="Search terms"/> + <title value="Create search term test"/> + <description value="Admin should be able to create search term"/> + <testCaseId value="MC-13989"/> + <severity value="CRITICAL"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + </before> + <after> + <!-- Delete created search term --> + <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Delete created product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to the search terms page and create new search term --> + <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="createNewSearchTerm"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> + </actionGroup> + + <!-- Go to storefront --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Assert created search term on storefront --> + <actionGroup ref="AssertSearchTermOnFrontend" stepKey="assertCreatedSearchTermOnFrontend"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml new file mode 100644 index 0000000000000..c72ed424ef307 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml @@ -0,0 +1,59 @@ +<?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="AdminDeleteSearchTermTest"> + <annotations> + <stories value="Search terms"/> + <title value="Delete Search Term and Verify Storefront"/> + <description value="Test log in to SearchTerm and DeleteSearchTerm"/> + <testCaseId value="MC-13988"/> + <severity value="CRITICAL"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Add new search term--> + <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="addNewSearchTerm"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> + </actionGroup> + + <!--Search and delete search term and AssertSearchTermSuccessDeleteMessage--> + <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + </actionGroup> + + <!--Verify deleted search term in grid and AssertSearchTermNotInGrid--> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="verifyDeletedSearchTermNotInGrid"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + </actionGroup> + + <!--Go to storefront and Verify AssertSearchTermNotOnFrontend--> + <actionGroup ref="AssertSearchTermNotOnFrontend" stepKey="verifySearchTermNotOnFrontend"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + <argument name="url_key" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php new file mode 100644 index 0000000000000..f5851cf1e11b1 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\Products; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Save/Delete UrlRewrites by Product ID's and visibility + */ +class AdaptUrlRewritesToVisibilityAttribute +{ + /** + * @var CollectionFactory + */ + private $productCollectionFactory; + + /** + * @var ProductUrlRewriteGenerator + */ + private $urlRewriteGenerator; + + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var ProductUrlPathGenerator + */ + private $urlPathGenerator; + + /** + * @param CollectionFactory $collectionFactory + * @param ProductUrlRewriteGenerator $urlRewriteGenerator + * @param UrlPersistInterface $urlPersist + * @param ProductUrlPathGenerator|null $urlPathGenerator + */ + public function __construct( + CollectionFactory $collectionFactory, + ProductUrlRewriteGenerator $urlRewriteGenerator, + UrlPersistInterface $urlPersist, + ProductUrlPathGenerator $urlPathGenerator + ) { + $this->productCollectionFactory = $collectionFactory; + $this->urlRewriteGenerator = $urlRewriteGenerator; + $this->urlPersist = $urlPersist; + $this->urlPathGenerator = $urlPathGenerator; + } + + /** + * Process Url Rewrites according to the products visibility attribute + * + * @param array $productIds + * @param int $visibility + * @throws UrlAlreadyExistsException + */ + public function execute(array $productIds, int $visibility): void + { + $products = $this->getProductsByIds($productIds); + + /** @var Product $product */ + foreach ($products as $product) { + if ($visibility == Visibility::VISIBILITY_NOT_VISIBLE) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + ] + ); + } elseif ($visibility !== Visibility::VISIBILITY_NOT_VISIBLE) { + $product->setVisibility($visibility); + $productUrlPath = $this->urlPathGenerator->getUrlPath($product); + $productUrlRewrite = $this->urlRewriteGenerator->generate($product); + $product->unsUrlPath(); + $product->setUrlPath($productUrlPath); + + try { + $this->urlPersist->replace($productUrlRewrite); + } catch (UrlAlreadyExistsException $e) { + throw new UrlAlreadyExistsException( + __( + 'Can not change the visibility of the product with SKU equals "%1". ' + . 'URL key "%2" for specified store already exists.', + $product->getSku(), + $product->getUrlKey() + ), + $e, + $e->getCode(), + $e->getUrls() + ); + } + } + } + } + + /** + * Get Product Models by Id's + * + * @param array $productIds + * @return array + */ + private function getProductsByIds(array $productIds): array + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect(ProductInterface::VISIBILITY); + $productCollection->addAttributeToSelect('url_key'); + $productCollection->addFieldToFilter( + 'entity_id', + ['in' => array_unique($productIds)] + ); + + return $productCollection->getItems(); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php new file mode 100644 index 0000000000000..2337bb3646bd7 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogUrlRewrite\Model\Products\AdaptUrlRewritesToVisibilityAttribute; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; + +/** + * Consider URL rewrites on change product visibility via mass action + */ +class ProcessUrlRewriteOnChangeProductVisibilityObserver implements ObserverInterface +{ + /** + * @var AdaptUrlRewritesToVisibilityAttribute + */ + private $adaptUrlRewritesToVisibility; + + /** + * @param AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility + */ + public function __construct(AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility) + { + $this->adaptUrlRewritesToVisibility = $adaptUrlRewritesToVisibility; + } + + /** + * Generate urls for UrlRewrites and save it in storage + * + * @param Observer $observer + * @return void + * @throws UrlAlreadyExistsException + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + $attrData = $event->getAttributesData(); + $productIds = $event->getProductIds(); + $visibility = $attrData[ProductInterface::VISIBILITY] ?? 0; + + if (!$visibility || !$productIds) { + return; + } + + $this->adaptUrlRewritesToVisibility->execute($productIds, (int)$visibility); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml index 593df1c5bc6e1..30a4290d882fb 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -67,7 +67,13 @@ <waitForAjaxLoad stepKey="waitForLoadWebSiteTab"/> <click selector="{{AdminUpdateAttributesWebsiteSection.addProductToWebsite}}" stepKey="checkAddProductToWebsiteCheckbox"/> <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 1 record(s) were updated." stepKey="seeSaveSuccess"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!--Got to Store front product page and check url--> <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="navigateToSimpleProductPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/events.xml b/app/code/Magento/CatalogUrlRewrite/etc/events.xml index cc558fe81f16d..728442acf7a44 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/events.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/events.xml @@ -27,6 +27,9 @@ <event name="catalog_product_save_after"> <observer name="process_url_rewrite_saving" instance="Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver"/> </event> + <event name="catalog_product_attribute_update_before"> + <observer name="process_url_rewrite_on_change_product_visibility" instance="Magento\CatalogUrlRewrite\Observer\ProcessUrlRewriteOnChangeProductVisibilityObserver"/> + </event> <event name="catalog_category_save_before"> <observer name="category_url_path_autogeneration" instance="Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver"/> </event> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml new file mode 100644 index 0000000000000..8c5c6f41fffa7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml @@ -0,0 +1,25 @@ +<?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="AssertMiniShoppingCartSubTotalActionGroup"> + <arguments> + <argument name="dataQuote" type="entity" /> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad" time="120"/> + <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabMiniCartTotal" /> + <assertContains stepKey="assertMiniCartTotal"> + <actualResult type="variable">$grabMiniCartTotal</actualResult> + <expectedResult type="string">{{dataQuote.subtotal}}</expectedResult> + </assertContains> + <assertContains stepKey="assertMiniCartCurrency"> + <actualResult type="variable">$grabMiniCartTotal</actualResult> + <expectedResult type="string">{{dataQuote.currency}}</expectedResult> + </assertContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..ee8b761a452d4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml @@ -0,0 +1,28 @@ +<?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="StorefrontUpdateProductQtyMiniShoppingCartActionGroup"> + <arguments> + <argument name="product" type="entity" /> + <argument name="quote" type="entity" /> + </arguments> + + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="goToMiniShoppingCart"/> + + <!-- Clearing QTY field --> + <doubleClick selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" stepKey="doubleClickOnQtyInput" /> + <pressKey selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" parameterArray="[\WebDriverKeys::DELETE]" stepKey="clearQty"/> + <!-- Clearing QTY field --> + + <fillField selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" userInput="{{quote.qty}}" stepKey="changeQty"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdateBySku(product.sku)}}" stepKey="clickUpdateButton"/> + <waitForPageLoad stepKey="waitForProductQtyUpdate" /> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml index 530157851191f..e7a5992ad8943 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml @@ -15,4 +15,26 @@ <data key="total">495.00</data> <data key="shippingMethod">Flat Rate - Fixed</data> </entity> + <entity name="simpleOrderQty2" type="Quote"> + <data key="price">560.00</data> + <data key="qty">2</data> + <data key="subtotal">1,120.00</data> + <data key="shipping">10.00</data> + <data key="total">1,130.00</data> + <data key="shippingMethod">Flat Rate - Fixed</data> + <data key="currency">$</data> + </entity> + <entity name="quoteQty3Price123" type="Quote"> + <data key="price">123.00</data> + <data key="qty">3</data> + <data key="subtotal">369.00</data> + <data key="currency">$</data> + </entity> + <entity name="quoteQty11Subtotal1320" type="Quote"> + <data key="price">100.00</data> + <data key="customOptionsPrice">20</data> + <data key="qty">11</data> + <data key="subtotal">1,320.00</data> + <data key="currency">$</data> + </entity> </entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index bdb02835c6276..38c88bf4f80bb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -25,6 +25,8 @@ <element name="deleteMiniCartItem" type="button" selector=".action.delete" timeout="30"/> <element name="deleteMiniCartItemByName" type="button" selector="//ol[@id='mini-cart']//div[contains(., '{{var}}')]//a[contains(@class, 'delete')]" parameterized="true"/> <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> + <element name="itemQuantityBySku" type="input" selector="#minicart-content-wrapper input[data-cart-item-id='{{productSku}}']" parameterized="true"/> + <element name="itemQuantityUpdateBySku" type="button" selector="//div[@id='minicart-content-wrapper']//input[@data-cart-item-id='{{productSku}}']/../button[contains(@class, 'update-cart-item')]" parameterized="true"/> <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> <element name="itemDiscount" type="text" selector="//tr[@class='totals']//td[@class='amount']/span"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml new file mode 100644 index 0000000000000..423f4049f6722 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml @@ -0,0 +1,65 @@ +<?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="StorefrontUpdateShoppingCartSimpleProductQtyTest"> + <annotations> + <features value="Checkout"/> + <title value="Check updating shopping cart while updating items qty"/> + <description value="Check updating shopping cart while updating items qty"/> + <testCaseId value="MC-14731" /> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add the newly created product to the shopping cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad1"/> + + <!-- Change the product QTY --> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty3Price123.qty}}" stepKey="changeCartQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="openShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad2"/> + + <!-- The price and QTY values should be updated for the product --> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> + <assertEquals stepKey="assertProductQtyInCart"> + <actualResult type="variable">grabProductQtyInCart</actualResult> + <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> + </assertEquals> + + <!-- Subtotal should be updated --> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertCartSubtotal"/> + + <!-- Minicart product price and subtotal should be updated --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> + <assertEquals stepKey="assertProductQtyInMinicart"> + <actualResult type="variable">grabProductQtyInMinicart</actualResult> + <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> + </assertEquals> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml new file mode 100644 index 0000000000000..84080b04c80ee --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml @@ -0,0 +1,70 @@ +<?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="StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest"> + <annotations> + <features value="Checkout"/> + <title value="Check updating shopping cart while updating qty of items with custom options"/> + <description value="Check updating shopping cart while updating qty of items with custom options"/> + <testCaseId value="MC-14732" /> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add two custom options to the product: field and textarea --> + <updateData createDataKey="createProduct" entity="ProductWithTextFieldAndAreaOptions" stepKey="updateProductWithOption"/> + + <!-- Go to the product page, fill the custom options values and add the product to the shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> + <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + + <!-- Change the product QTY --> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty11Subtotal1320.qty}}" stepKey="changeCartQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="updateShoppingCart"/> + <waitForPageLoad stepKey="waitShoppingCartUpdated"/> + + <!-- The price and QTY values should be updated for the product --> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> + <assertEquals stepKey="assertProductQtyInCart"> + <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> + <actualResult type="variable">grabProductQtyInCart</actualResult> + </assertEquals> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> + + <!-- Minicart product price and subtotal should be updated --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> + <assertEquals stepKey="assertProductQtyInMinicart"> + <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> + <actualResult type="variable">grabProductQtyInMinicart</actualResult> + </assertEquals> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml new file mode 100644 index 0000000000000..7318f865a0dc1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -0,0 +1,47 @@ +<?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="UpdateProductFromMiniShoppingCartEntityTest"> + <annotations> + <stories value="Shopping Cart"/> + <title value="Check updating product from mini shopping cart"/> + <description value="Update Product Qty on Mini Shopping Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15068"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!--Create product according to dataset.--> + <createData entity="simpleProductWithoutCategory" stepKey="createProduct"/> + + <!--Add product to cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + </after> + + <actionGroup ref="StorefrontUpdateProductQtyMiniShoppingCartActionGroup" stepKey="updateProductQty"> + <argument name="product" value="$$createProduct$$" /> + <argument name="quote" value="simpleOrderQty2" /> + </actionGroup> + + <!-- Perform all assertions --> + <actionGroup ref="AssertMiniShoppingCartSubTotalActionGroup" stepKey="checkSummary"> + <argument name="dataQuote" value="simpleOrderQty2"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 71dfd12bb4779..4ebd594a28562 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -49,7 +49,4 @@ </argument> </arguments> </type> - <type name="Magento\Quote\Model\Quote"> - <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> - </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 00bcd2a27005a..8f35fe9f37abf 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -96,4 +96,7 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> + </type> </config> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml index 3f742de0177da..122160f1a10cd 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout_overview"> - <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::multishipping_agreements.phtml"/> + <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::additional_agreements.phtml"/> </referenceBlock> </body> </page> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index 3400770f5cee8..33227f0cdce3c 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index a448537d64e83..4b1a68624e547 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -5,17 +5,17 @@ */ --> <div data-role="checkout-agreements"> - <div class="checkout-agreements" data-bind="visible: isVisible"> + <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> <!-- ko foreach: agreements --> <!-- ko if: ($parent.isAgreementRequired($data)) --> - <div class="checkout-agreement required"> + <div class="checkout-agreement field choice required"> <input type="checkbox" class="required-entry" data-bind="attr: { 'id': $parent.getCheckboxId($parentContext, agreementId), 'name': 'agreement[' + agreementId + ']', 'value': agreementId }"/> - <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> + <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> <button type="button" class="action action-show" data-bind="click: function(data, event) { return $parent.showContent(data, event) }" diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index b2ef78bab9909..ca563bd9d8f61 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -270,7 +270,8 @@ public function getDirsCollection($path) $collection = $this->getCollection($path) ->setCollectDirs(true) ->setCollectFiles(false) - ->setCollectRecursively(false); + ->setCollectRecursively(false) + ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); $conditions = $this->getConditionsForExcludeDirs(); diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 05b7dfeeb3953..03edc69e6d625 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -16,7 +16,6 @@ <description value="Admin should be able to add image to WYSIWYG content of Block"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84376"/> - <group value="WYSIWYGDisabled" /> </annotations> <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml index b4bcdaadf9a09..e6ab1c130606b 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -17,7 +17,6 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94229"/> <group value="Cms"/> - <group value="WYSIWYGDisabled"/> </annotations> <before> <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 309f08a54aab6..7bec1e3601461 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -417,6 +417,10 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e ->method('setCollectRecursively') ->with(false) ->willReturnSelf(); + $storageCollectionMock->expects($this->once()) + ->method('setOrder') + ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) + ->willReturnSelf(); $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); 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 9f886f6f1345e..793fc7d26cb4a 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 @@ -146,7 +146,6 @@ <editor> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> - <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> </validation> <editorType>text</editorType> </editor> diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 2c4b8a8dc48d2..c63ccae871657 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\App\Config\Type; use Magento\Framework\App\Config\ConfigSourceInterface; @@ -13,11 +14,12 @@ use Magento\Config\App\Config\Type\System\Reader; use Magento\Framework\App\ScopeInterface; use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; -use Magento\Store\Model\ScopeInterface as StoreScope; use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Model\ScopeInterface as StoreScope; /** * System configuration type @@ -25,6 +27,7 @@ * @api * @since 100.1.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ class System implements ConfigTypeInterface { @@ -42,22 +45,6 @@ class System implements ConfigTypeInterface * @var string */ private static $lockName = 'SYSTEM_CONFIG'; - /** - * Timeout between retrieves to load the configuration from the cache. - * - * Value of the variable in microseconds. - * - * @var int - */ - private static $delayTimeout = 100000; - /** - * Lifetime of the lock for write in cache. - * - * Value of the variable in seconds. - * - * @var int - */ - private static $lockTimeout = 42; /** * @var array @@ -106,9 +93,9 @@ class System implements ConfigTypeInterface private $encryptor; /** - * @var LockManagerInterface + * @var LockGuardedCacheLoader */ - private $locker; + private $lockQuery; /** * @param ConfigSourceInterface $source @@ -122,6 +109,7 @@ class System implements ConfigTypeInterface * @param Reader|null $reader * @param Encryptor|null $encryptor * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -136,7 +124,8 @@ public function __construct( $configType = self::CONFIG_TYPE, Reader $reader = null, Encryptor $encryptor = null, - LockManagerInterface $locker = null + LockManagerInterface $locker = null, + LockGuardedCacheLoader $lockQuery = null ) { $this->postProcessor = $postProcessor; $this->cache = $cache; @@ -145,8 +134,8 @@ public function __construct( $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(Encryptor::class); - $this->locker = $locker - ?: ObjectManager::getInstance()->get(LockManagerInterface::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); } /** @@ -225,83 +214,56 @@ private function getWithParts($path) } /** - * Make lock on data load. - * - * @param callable $dataLoader - * @param bool $flush - * @return array - */ - private function lockedLoadData(callable $dataLoader, bool $flush = false): array - { - $cachedData = $dataLoader(); //optimistic read - - while ($cachedData === false && $this->locker->isLocked(self::$lockName)) { - usleep(self::$delayTimeout); - $cachedData = $dataLoader(); - } - - while ($cachedData === false) { - try { - if ($this->locker->lock(self::$lockName, self::$lockTimeout)) { - if (!$flush) { - $data = $this->readData(); - $this->cacheData($data); - $cachedData = $data; - } else { - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); - $cachedData = []; - } - } - } finally { - $this->locker->unlock(self::$lockName); - } - - if ($cachedData === false) { - usleep(self::$delayTimeout); - $cachedData = $dataLoader(); - } - } - - return $cachedData; - } - - /** - * Load configuration data for all scopes + * Load configuration data for all scopes. * * @return array */ private function loadAllData() { - return $this->lockedLoadData(function () { + $loadAction = function () { $cachedData = $this->cache->load($this->configType); $data = false; if ($cachedData !== false) { $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); } return $data; - }); + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Load configuration data for default scope + * Load configuration data for default scope. * * @param string $scopeType * @return array */ private function loadDefaultScopeData($scopeType) { - return $this->lockedLoadData(function () use ($scopeType) { + $loadAction = function () use ($scopeType) { $cachedData = $this->cache->load($this->configType . '_' . $scopeType); $scopeData = false; if ($cachedData !== false) { $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; } return $scopeData; - }); + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Load configuration data for a specified scope + * Load configuration data for a specified scope. * * @param string $scopeType * @param string $scopeId @@ -309,7 +271,7 @@ private function loadDefaultScopeData($scopeType) */ private function loadScopeData($scopeType, $scopeId) { - return $this->lockedLoadData(function () use ($scopeType, $scopeId) { + $loadAction = function () use ($scopeType, $scopeId) { $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); $scopeData = false; if ($cachedData === false) { @@ -329,7 +291,14 @@ private function loadScopeData($scopeType, $scopeId) } return $scopeData; - }); + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** @@ -371,7 +340,7 @@ private function cacheData(array $data) } /** - * Walk nested hash map by keys from $pathParts + * Walk nested hash map by keys from $pathParts. * * @param array $data to walk in * @param array $pathParts keys path @@ -408,7 +377,7 @@ private function readData(): array } /** - * Clean cache and global variables cache + * Clean cache and global variables cache. * * Next items cleared: * - Internal property intended to store already loaded configuration data @@ -420,11 +389,13 @@ private function readData(): array public function clean() { $this->data = []; - $this->lockedLoadData( - function () { - return false; - }, - true + $cleanAction = function () { + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction ); } } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index 6bf191c20a844..bd38d1451e1b6 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -114,6 +114,11 @@ class Config extends \Magento\Framework\DataObject */ private $scopeTypeNormalizer; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -126,6 +131,7 @@ class Config extends \Magento\Framework\DataObject * @param array $data * @param ScopeResolverPool|null $scopeResolverPool * @param ScopeTypeNormalizer|null $scopeTypeNormalizer + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -139,7 +145,8 @@ public function __construct( SettingChecker $settingChecker = null, array $data = [], ScopeResolverPool $scopeResolverPool = null, - ScopeTypeNormalizer $scopeTypeNormalizer = null + ScopeTypeNormalizer $scopeTypeNormalizer = null, + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { parent::__construct($data); $this->_eventManager = $eventManager; @@ -155,6 +162,8 @@ public function __construct( ?? ObjectManager::getInstance()->get(ScopeResolverPool::class); $this->scopeTypeNormalizer = $scopeTypeNormalizer ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); } /** @@ -224,6 +233,8 @@ public function save() throw $e; } + $this->pillPut->put(); + return $this; } diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 57c067d2cae27..3312fb630ccda 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-message-queue": "*", "magento/module-backend": "*", "magento/module-cron": "*", "magento/module-deploy": "*", diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 87a0e666d2d7b..920cac382fcbf 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -90,9 +90,18 @@ <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> - <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockQuery" xsi:type="object">systemConfigQueryLocker</argument> </arguments> </type> + + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockTimeout" xsi:type="number">42000</argument> + <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + <type name="Magento\Config\App\Config\Type\System\Reader"> <arguments> <argument name="source" xsi:type="object">systemConfigSourceAggregated</argument> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 95b86ec3a8587..43dae2d70d416 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -145,8 +145,8 @@ <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(secondAttributeCode)}}" stepKey="clickOnSecondAttributeCheckbox" after="clickOnFirstAttributeCheckbox"/> <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(attributeCode)}}" stepKey="grabFirstAttributeDefaultLabel" after="clickOnSecondAttributeCheckbox"/> <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(secondAttributeCode)}}" stepKey="grabSecondAttributeDefaultLabel" after="grabFirstAttributeDefaultLabel"/> - <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabFirstAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForFistAttribute" after="clickOnNextButton1"/> - <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabSecondAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForSecondAttribute" after="clickOnSelectAllForFistAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabFirstAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForFirstAttribute" after="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabSecondAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForSecondAttribute" after="clickOnSelectAllForFirstAttribute"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml index af12f49bf86ea..39aa516077c56 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml @@ -57,7 +57,13 @@ <click selector="{{AdminUpdateAttributesSection.toggleDescription}}" stepKey="clickToggleDescription"/> <fillField selector="{{AdminUpdateAttributesSection.description}}" userInput="MFTF automation!" stepKey="fillDescription"/> <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 3 record(s) were updated." stepKey="seeSaveSuccess"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Check storefront for description --> <amOnPage url="$$createProduct1.sku$$.html" stepKey="gotoProduct1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml index e79f7f75cce3f..93df31a7d89e5 100755 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -54,7 +54,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <skip> - <issueId value="MAGETWO-62808"/> + <issueId value="MSI-2110"/> </skip> </annotations> <before> @@ -83,4 +83,120 @@ <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> </actionGroup> </test> + <test name="AdminCreateSimpleProductSwitchToConfigurableTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from simple to configurable"/> + <description value="After selecting a simple product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <useCaseId value="MAGETWO-44165"/> + <testCaseId value="MAGETWO-29398"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MSI-2110"/> + </skip> + </annotations> + <before> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + </after> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="simple"/> + </actionGroup> + <!-- Create configurable product from simple product page--> + <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <!-- Verify product on store front --> + <comment userInput="Verify product on store front" stepKey="commentVerifyProductGrid"/> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> + <test name="AdminCreateDownloadableProductSwitchToConfigurableTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from downloadable to configurable"/> + <description value="After selecting a downloadable product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <useCaseId value="MAGETWO-44165"/> + <testCaseId value="MAGETWO-29398"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MSI-2110"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create configurable product from downloadable product page--> + <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <!-- Open Dropdown and select downloadable product option --> + <comment stepKey="beforeOpenProductFillForm" userInput="Selecting Product from the Add Product Dropdown"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="downloadable"/> + </actionGroup> + <scrollTo selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="scrollToDownloadableInfo" /> + <uncheckOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> + <!-- Fill form for Downloadable Product Type --> + <comment stepKey="beforeFillProductForm" userInput="Filling Product Form"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="SetProductUrlKey" stepKey="setProductUrl"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <!-- Check that product was added with implicit type change --> + <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <actionGroup ref="filterProductGridByName" stepKey="searchForProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefrontProductPage"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml index 981985b3f4ea8..1db9b3e5b79b2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml @@ -144,7 +144,7 @@ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> <!-- Quick search the storefront for the first attribute option --> - <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFistChildProduct"/> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> <!-- Quick search the storefront for the second attribute option --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml index e278018330aa6..934a410d58a8a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml @@ -126,7 +126,7 @@ <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> <!-- Quick search the storefront for the first attribute option --> - <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFistChildProduct"/> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> <!-- Quick search the storefront for the second attribute option --> diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 06134fcbf09d8..c3ffe988b00d7 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -248,4 +248,11 @@ <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> </type> + <type name="Magento\Eav\Model\Entity\Attribute\Group"> + <arguments> + <argument name="reservedSystemNames" xsi:type="array"> + <item name="configurable" xsi:type="string">configurable</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Customer/Block/Address/Grid.php b/app/code/Magento/Customer/Block/Address/Grid.php index de6767a0ef92a..963efc648d94b 100644 --- a/app/code/Magento/Customer/Block/Address/Grid.php +++ b/app/code/Magento/Customer/Block/Address/Grid.php @@ -1,9 +1,10 @@ <?php -declare(strict_types=1); /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Block\Address; use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; @@ -236,8 +237,12 @@ private function getAddressCollection(): \Magento\Customer\Model\ResourceModel\A } /** @var \Magento\Customer\Model\ResourceModel\Address\Collection $collection */ $collection = $this->addressCollectionFactory->create(); - $collection->setOrder('entity_id', 'desc') - ->setCustomerFilter([$this->getCustomer()->getId()]); + $collection->setOrder('entity_id', 'desc'); + $collection->addFieldToFilter( + 'entity_id', + ['nin' => [$this->getDefaultBilling(), $this->getDefaultShipping()]] + ); + $collection->setCustomerFilter([$this->getCustomer()->getId()]); $this->addressCollection = $collection; } return $this->addressCollection; diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml new file mode 100644 index 0000000000000..132b5ca81886f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.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="AssertCustomerAccountPageTitleActionGroup"> + <arguments> + <argument name="pageTitle" type="string" /> + </arguments> + <see selector="{{StorefrontCustomerAccountMainSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="assertPageTitle" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml index 90544dc2673a3..703b9f542f81a 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml @@ -17,5 +17,6 @@ <fillField userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <waitForPageLoad stepKey="waitForCustomerLoggedIn" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml new file mode 100644 index 0000000000000..5591bee529690 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="NavigateThroughCustomerTabsActionGroup"> + <arguments> + <argument name="navigationItemName" type="string" /> + </arguments> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(navigationItemName)}}" stepKey="clickOnDesiredNavItem" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml new file mode 100644 index 0000000000000..6ca0f612deeaa --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="OpenMyAccountPageActionGroup"> + <click selector="{{LoggedInCustomerHeaderLinksSection.customerDropdownMenu}}" stepKey="openCustomerDropdownMenu"/> + <click selector="{{LoggedInCustomerHeaderLinksSection.myAccount}}" stepKey="clickOnMyAccount"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml index 9b6155e982013..28305d37cf77b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -8,6 +8,12 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="NotLoggedInCustomerGroup" type="customerGroup"> + <data key="id">0</data> + <data key="code">NOT LOGGED IN</data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> <entity name="GeneralCustomerGroup" type="customerGroup"> <data key="code">General</data> <data key="tax_class_id">3</data> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml index 1fdb15f189ace..4cb7f5e3f628e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml @@ -15,5 +15,6 @@ <element name="selectFirstRow" type="button" selector="//button[@class='action-select']"/> <element name="deleteBtn" type="button" selector="//*[text()='Delete']"/> <element name="clearAllBtn" type="button" selector="//button[text()='Clear all']"/> + <element name="editButtonByCustomerGroupCode" type="button" selector="//tr[.//td[count(//th[./*[.='Group']]/preceding-sibling::th) + 1][./*[.='{{code}}']]]//a[contains(@href, '/edit/')]" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml new file mode 100644 index 0000000000000..907551e932fcf --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml @@ -0,0 +1,17 @@ +<?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="LoggedInCustomerHeaderLinksSection"> + <element name="customerDropdownMenu" type="button" selector=".customer-name"/> + <element name="myAccount" type="button" selector="//*[contains(@class, 'page-header')]//*[contains(@class, 'customer-menu')]//a[contains(., 'My Account')]"/> + <element name="myWishList" type="button" selector=".page-header .customer-menu .wishlist"/> + <element name="signOut" type="button" selector=".page-header .customer-menu .authorization-link"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml new file mode 100644 index 0000000000000..3a4329969ae2b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml @@ -0,0 +1,14 @@ +<?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="StorefrontCustomerAccountMainSection"> + <element name="pageTitle" type="text" selector="#maincontent .column.main [data-ui-id='page-title-wrapper']" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml index bed0c71ae7fad..407c6480e9dde 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml @@ -9,7 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerSidebarSection"> - <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{var1}}']" parameterized="true"/> + <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{tabName}}']" parameterized="true"/> <element name="sidebarCurrentTab" type="text" selector="//div[@id='block-collapsible-nav']//*[contains(text(), '{{var}}')]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml new file mode 100644 index 0000000000000..648c30b1ca0bb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.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="VerifyDisabledCustomerGroupFieldTest"> + <annotations> + <stories value="Check that field is disabled in system Customer Group"/> + <title value="Check that field is disabled in system Customer Group"/> + <description value="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"/> + <testCaseId value="MC-14206"/> + <severity value="CRITICAL"/> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + + <!-- Steps --> + <!-- 1. Login to backend as admin user --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <waitForPageLoad stepKey="waitForAdminPageLoad" /> + + <!-- 2. Navigate to Customers > Customer Groups --> + <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="amOnCustomerGroupPage" /> + <waitForPageLoad stepKey="waitForCustomerGroupsPageLoad" /> + + <!-- 3. Select system Customer Group specified in data set from grid --> + <click selector="{{AdminCustomerGroupMainSection.editButtonByCustomerGroupCode(NotLoggedInCustomerGroup.code)}}" stepKey="clickOnEditCustomerGroup" /> + + <!-- 4. Perform all assertions --> + <seeInField selector="{{AdminNewCustomerGroupSection.groupName}}" userInput="{{NotLoggedInCustomerGroup.code}}" stepKey="seeNotLoggedInTextInGroupName" /> + <assertElementContainsAttribute selector="{{AdminNewCustomerGroupSection.groupName}}" attribute="disabled" expectedValue="true" stepKey="checkIfGroupNameIsDisabled" /> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php index 31bcc37612302..47f96b132b3db 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php @@ -81,7 +81,7 @@ protected function setUp() public function testGetChildHtml() { $customerId = 1; - + $outputString = 'OutputString'; /** @var \Magento\Framework\View\Element\BlockInterface|\PHPUnit_Framework_MockObject_MockObject $block */ $block = $this->getMockBuilder(\Magento\Framework\View\Element\BlockInterface::class) ->setMethods(['setCollection']) @@ -93,7 +93,7 @@ public function testGetChildHtml() /** @var \PHPUnit_Framework_MockObject_MockObject */ $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) ->disableOriginalConstructor() - ->setMethods(['setOrder', 'setCustomerFilter', 'load']) + ->setMethods(['setOrder', 'setCustomerFilter', 'load','addFieldToFilter']) ->getMock(); $layout->expects($this->atLeastOnce())->method('getChildName')->with('NameInLayout', 'pager') @@ -108,12 +108,13 @@ public function testGetChildHtml() ->willReturnSelf(); $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) ->willReturnSelf(); + $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') ->willReturn($addressCollection); $block->expects($this->atLeastOnce())->method('setCollection')->with($addressCollection)->willReturnSelf(); $this->gridBlock->setNameInLayout('NameInLayout'); $this->gridBlock->setLayout($layout); - $this->assertEquals('OutputString', $this->gridBlock->getChildHtml('pager')); + $this->assertEquals($outputString, $this->gridBlock->getChildHtml('pager')); } /** @@ -137,7 +138,7 @@ public function testGetAdditionalAddresses() /** @var \PHPUnit_Framework_MockObject_MockObject */ $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) ->disableOriginalConstructor() - ->setMethods(['setOrder', 'setCustomerFilter', 'load', 'getIterator']) + ->setMethods(['setOrder', 'setCustomerFilter', 'load', 'getIterator','addFieldToFilter']) ->getMock(); $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) @@ -157,6 +158,7 @@ public function testGetAdditionalAddresses() ->willReturnSelf(); $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) ->willReturnSelf(); + $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); $addressCollection->expects($this->atLeastOnce())->method('getIterator') ->willReturn(new \ArrayIterator($collection)); $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') diff --git a/app/code/Magento/Deploy/Console/DeployStaticOptions.php b/app/code/Magento/Deploy/Console/DeployStaticOptions.php index 89cb3e4b30345..1c02d24f7e99c 100644 --- a/app/code/Magento/Deploy/Console/DeployStaticOptions.php +++ b/app/code/Magento/Deploy/Console/DeployStaticOptions.php @@ -6,6 +6,7 @@ namespace Magento\Deploy\Console; +use Magento\Deploy\Process\Queue; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -57,6 +58,11 @@ class DeployStaticOptions */ const JOBS_AMOUNT = 'jobs'; + /** + * Key for max execution time option + */ + const MAX_EXECUTION_TIME = 'max-execution-time'; + /** * Force run of static deploy */ @@ -150,6 +156,7 @@ public function getOptionsList() * Basic options * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ private function getBasicOptions() { @@ -216,6 +223,13 @@ private function getBasicOptions() 'Enable parallel processing using the specified number of jobs.', self::DEFAULT_JOBS_AMOUNT ), + new InputOption( + self::MAX_EXECUTION_TIME, + null, + InputOption::VALUE_OPTIONAL, + 'The maximum expected execution time of deployment static process (in seconds).', + Queue::DEFAULT_MAX_EXEC_TIME + ), new InputOption( self::SYMLINK_LOCALE, null, diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php index 66ec6e7418afd..854bf50e0af2f 100644 --- a/app/code/Magento/Deploy/Service/DeployStaticContent.php +++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php @@ -85,24 +85,26 @@ public function deploy(array $options) return; } - $queue = $this->queueFactory->create( - [ - 'logger' => $this->logger, - 'options' => $options, - 'maxProcesses' => $this->getProcessesAmount($options), - 'deployPackageService' => $this->objectManager->create( - \Magento\Deploy\Service\DeployPackage::class, - [ - 'logger' => $this->logger - ] - ) - ] - ); + $queueOptions = [ + 'logger' => $this->logger, + 'options' => $options, + 'maxProcesses' => $this->getProcessesAmount($options), + 'deployPackageService' => $this->objectManager->create( + \Magento\Deploy\Service\DeployPackage::class, + [ + 'logger' => $this->logger + ] + ) + ]; + + if (isset($options[Options::MAX_EXECUTION_TIME])) { + $queueOptions['maxExecTime'] = (int)$options[Options::MAX_EXECUTION_TIME]; + } $deployStrategy = $this->deployStrategyFactory->create( $options[Options::STRATEGY], [ - 'queue' => $queue + 'queue' => $this->queueFactory->create($queueOptions) ] ); @@ -133,6 +135,8 @@ public function deploy(array $options) } /** + * Returns amount of parallel processes, returns zero if option wasn't set. + * * @param array $options * @return int */ @@ -142,6 +146,8 @@ private function getProcessesAmount(array $options) } /** + * Checks if need to refresh only version. + * * @param array $options * @return bool */ diff --git a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php index 75edc8cb4f6ee..396381960e544 100644 --- a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Deploy\Test\Unit\Service; +use Magento\Deploy\Console\DeployStaticOptions; use Magento\Deploy\Package\Package; use Magento\Deploy\Process\Queue; use Magento\Deploy\Service\Bundle; @@ -221,4 +222,35 @@ public function deployDataProvider() ] ]; } + + public function testMaxExecutionTimeOptionPassed() + { + $options = [ + DeployStaticOptions::MAX_EXECUTION_TIME => 100, + DeployStaticOptions::REFRESH_CONTENT_VERSION_ONLY => false, + DeployStaticOptions::JOBS_AMOUNT => 3, + DeployStaticOptions::STRATEGY => 'compact', + DeployStaticOptions::NO_JAVASCRIPT => true, + DeployStaticOptions::NO_HTML_MINIFY => true, + ]; + + $queueMock = $this->createMock(Queue::class); + $strategyMock = $this->createMock(CompactDeploy::class); + $this->queueFactory->expects($this->once()) + ->method('create') + ->with([ + 'logger' => $this->logger, + 'maxExecTime' => 100, + 'maxProcesses' => 3, + 'options' => $options, + 'deployPackageService' => null + ]) + ->willReturn($queueMock); + $this->deployStrategyFactory->expects($this->once()) + ->method('create') + ->with('compact', ['queue' => $queueMock]) + ->willReturn($strategyMock); + + $this->service->deploy($options); + } } diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls index 40ef6975fad8b..8da1920f9a444 100644 --- a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -10,8 +10,10 @@ type Query { type Currency { base_currency_code: String base_currency_symbol: String - default_display_currecy_code: String - default_display_currecy_symbol: String + default_display_currecy_code: String @deprecated(reason: "Symbol was missed. Use `default_display_currency_code`.") + default_display_currency_code: String + default_display_currecy_symbol: String @deprecated(reason: "Symbol was missed. Use `default_display_currency_symbol`.") + default_display_currency_symbol: String available_currency_codes: [String] exchange_rates: [ExchangeRate] } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index d0a5e8de53ae9..1fd71e446e6bb 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -1683,14 +1683,16 @@ public function saveAttribute(DataObject $object, $attributeCode) $connection->beginTransaction(); try { - $select = $connection->select()->from($table, 'value_id')->where($where); - $origValueId = $connection->fetchOne($select); + $select = $connection->select()->from($table, ['value_id', 'value'])->where($where); + $origRow = $connection->fetchRow($select); + $origValueId = $origRow['value_id'] ?? false; + $origValue = $origRow['value'] ?? null; if ($origValueId === false && $newValue !== null) { $this->_insertAttribute($object, $attribute, $newValue); } elseif ($origValueId !== false && $newValue !== null) { $this->_updateAttribute($object, $attribute, $origValueId, $newValue); - } elseif ($origValueId !== false && $newValue === null) { + } elseif ($origValueId !== false && $newValue === null && $origValue !== null) { $connection->delete($table, $where); } $this->_processAttributeValues(); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php index 0b6ac2b998de7..2e55964560588 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeGroupExtensionInterface; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Exception\LocalizedException; /** + * Entity attribute group model + * * @api * @method int getSortOrder() * @method \Magento\Eav\Model\Entity\Attribute\Group setSortOrder(int $value) @@ -27,6 +32,11 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ private $translitFilter; + /** + * @var array + */ + private $reservedSystemNames = []; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -35,7 +45,8 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Filter\Translit $translitFilter * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data + * @param array $data (optional) + * @param array $reservedSystemNames (optional) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -45,7 +56,8 @@ public function __construct( \Magento\Framework\Filter\Translit $translitFilter, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + array $reservedSystemNames = [] ) { parent::__construct( $context, @@ -56,6 +68,7 @@ public function __construct( $resourceCollection, $data ); + $this->reservedSystemNames = $reservedSystemNames; $this->translitFilter = $translitFilter; } @@ -74,6 +87,7 @@ protected function _construct() * Checks if current attribute group exists * * @return bool + * @throws LocalizedException * @codeCoverageIgnore */ public function itemExists() @@ -85,6 +99,7 @@ public function itemExists() * Delete groups * * @return $this + * @throws LocalizedException * @codeCoverageIgnore */ public function deleteGroups() @@ -110,9 +125,10 @@ public function beforeSave() ), '-' ); - if (empty($attributeGroupCode)) { + $isReservedSystemName = in_array(strtolower($attributeGroupCode), $this->reservedSystemNames); + if (empty($attributeGroupCode) || $isReservedSystemName) { // in the following code md5 is not used for security purposes - $attributeGroupCode = md5($groupName); + $attributeGroupCode = md5(strtolower($groupName)); } $this->setAttributeGroupCode($attributeGroupCode); } @@ -121,7 +137,8 @@ public function beforeSave() } /** - * {@inheritdoc} + * @inheritdoc + * * @codeCoverageIgnoreStart */ public function getAttributeGroupId() @@ -130,7 +147,7 @@ public function getAttributeGroupId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeGroupName() { @@ -138,7 +155,7 @@ public function getAttributeGroupName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeSetId() { @@ -146,7 +163,7 @@ public function getAttributeSetId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupId($attributeGroupId) { @@ -154,7 +171,7 @@ public function setAttributeGroupId($attributeGroupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupName($attributeGroupName) { @@ -162,7 +179,7 @@ public function setAttributeGroupName($attributeGroupName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeSetId($attributeSetId) { @@ -170,9 +187,9 @@ public function setAttributeSetId($attributeSetId) } /** - * {@inheritdoc} + * @inheritdoc * - * @return \Magento\Eav\Api\Data\AttributeGroupExtensionInterface|null + * @return AttributeGroupExtensionInterface|null */ public function getExtensionAttributes() { @@ -180,14 +197,13 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * - * @param \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes + * @param AttributeGroupExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes( - \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes - ) { + public function setExtensionAttributes(AttributeGroupExtensionInterface $extensionAttributes) + { return $this->_setExtensionAttributes($extensionAttributes); } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php index d4c91e98d9608..1584b922abaa9 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php @@ -40,6 +40,7 @@ protected function setUp() 'resource' => $this->resourceMock, 'translitFilter' => $translitFilter, 'context' => $contextMock, + 'reservedSystemNames' => ['configurable'], ]; $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( @@ -67,6 +68,8 @@ public function attributeGroupCodeDataProvider() { return [ ['General Group', 'general-group'], + ['configurable', md5('configurable')], + ['configurAble', md5('configurable')], ['///', md5('///')], ]; } diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index a4c89dcfab2af..db6f9b0a64f9f 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -210,4 +210,3 @@ </arguments> </type> </config> - diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php index 0c03a9df18dc8..eeb48f805bccf 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php @@ -22,13 +22,15 @@ public function build( array $queryResult, DataProviderInterface $dataProvider ) { + $buckets = $queryResult['aggregations'][$bucket->getName()]['buckets'] ?? []; $values = []; - foreach ($queryResult['aggregations'][$bucket->getName()]['buckets'] as $resultBucket) { + foreach ($buckets as $resultBucket) { $values[$resultBucket['key']] = [ 'value' => $resultBucket['key'], 'count' => $resultBucket['doc_count'], ]; } + return $values; } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php index aaa9d8a88382f..afd383c13421f 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -5,6 +5,10 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface as TypeResolver; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Query\BoolExpression; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; @@ -26,20 +30,49 @@ class Match implements QueryInterface private $fieldMapper; /** + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @var PreprocessorInterface[] */ protected $preprocessorContainer; + /** + * @var AttributeProvider + */ + private $attributeProvider; + + /** + * @var TypeResolver + */ + private $fieldTypeResolver; + + /** + * @var ValueTransformerPool + */ + private $valueTransformerPool; + /** * @param FieldMapperInterface $fieldMapper * @param PreprocessorInterface[] $preprocessorContainer + * @param AttributeProvider|null $attributeProvider + * @param TypeResolver|null $fieldTypeResolver + * @param ValueTransformerPool|null $valueTransformerPool */ public function __construct( FieldMapperInterface $fieldMapper, - array $preprocessorContainer + array $preprocessorContainer, + AttributeProvider $attributeProvider = null, + TypeResolver $fieldTypeResolver = null, + ValueTransformerPool $valueTransformerPool = null ) { $this->fieldMapper = $fieldMapper; $this->preprocessorContainer = $preprocessorContainer; + $this->attributeProvider = $attributeProvider ?? ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldTypeResolver = $fieldTypeResolver ?? ObjectManager::getInstance() + ->get(TypeResolver::class); + $this->valueTransformerPool = $valueTransformerPool ?? ObjectManager::getInstance() + ->get(ValueTransformerPool::class); } /** @@ -72,10 +105,6 @@ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $ */ protected function prepareQuery($queryValue, $conditionType) { - $queryValue = $this->escape($queryValue); - foreach ($this->preprocessorContainer as $preprocessor) { - $queryValue = $preprocessor->process($queryValue); - } $condition = $conditionType === BoolExpression::QUERY_CONDITION_NOT ? self::QUERY_CONDITION_MUST_NOT : $conditionType; return [ @@ -104,10 +133,24 @@ protected function buildQueries(array $matches, array $queryValue) // Checking for quoted phrase \"phrase test\", trim escaped surrounding quotes if found $count = 0; - $value = preg_replace('#^\\\\"(.*)\\\\"$#m', '$1', $queryValue['value'], -1, $count); + $value = preg_replace('#^"(.*)"$#m', '$1', $queryValue['value'], -1, $count); $condition = ($count) ? 'match_phrase' : 'match'; + $transformedTypes = []; foreach ($matches as $match) { + $attributeAdapter = $this->attributeProvider->getByAttributeCode($match['field']); + $fieldType = $this->fieldTypeResolver->getFieldType($attributeAdapter); + $valueTransformer = $this->valueTransformerPool->get($fieldType ?? 'text'); + $valueTransformerHash = \spl_object_hash($valueTransformer); + if (!isset($transformedTypes[$valueTransformerHash])) { + $transformedTypes[$valueTransformerHash] = $valueTransformer->transform($value); + } + $transformedValue = $transformedTypes[$valueTransformerHash]; + if (null === $transformedValue) { + //Value is incompatible with this field type. + continue; + } + $resolvedField = $this->fieldMapper->getFieldName( $match['field'], ['type' => FieldMapperInterface::TYPE_QUERY] @@ -117,8 +160,8 @@ protected function buildQueries(array $matches, array $queryValue) 'body' => [ $condition => [ $resolvedField => [ - 'query' => $value, - 'boost' => isset($match['boost']) ? $match['boost'] : 1, + 'query' => $transformedValue, + 'boost' => $match['boost'] ?? 1, ], ], ], @@ -131,16 +174,13 @@ protected function buildQueries(array $matches, array $queryValue) /** * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. * - * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. - * https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @param string $value * @return string */ protected function escape($value) { - $value = preg_replace('/@+|[@+-]+$/', '', $value); - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; $replace = '\\\$1'; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php new file mode 100644 index 0000000000000..49eca6e9d82a6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\Model\Adapter\FieldType\Date; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for date type fields. + */ +class DateTransformer implements ValueTransformerInterface +{ + /** + * @var Date + */ + private $dateFieldType; + + /** + * @param Date $dateFieldType + */ + public function __construct(Date $dateFieldType) + { + $this->dateFieldType = $dateFieldType; + } + + /** + * @inheritdoc + */ + public function transform(string $value): ?string + { + try { + $formattedDate = $this->dateFieldType->formatDate(null, $value); + } catch (\Exception $e) { + $formattedDate = null; + } + + return $formattedDate; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php new file mode 100644 index 0000000000000..5e330076d3df7 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for float type fields. + */ +class FloatTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?float + { + return \is_numeric($value) ? (float) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php new file mode 100644 index 0000000000000..0846ff3a9bd86 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for integer type fields. + */ +class IntegerTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?int + { + return \is_numeric($value) ? (int) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php new file mode 100644 index 0000000000000..68bec2580f621 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; + +/** + * Value transformer for fields with text types. + */ +class TextTransformer implements ValueTransformerInterface +{ + /** + * @var PreprocessorInterface[] + */ + private $preprocessors; + + /** + * @param PreprocessorInterface[] $preprocessors + */ + public function __construct(array $preprocessors = []) + { + foreach ($preprocessors as $preprocessor) { + if (!$preprocessor instanceof PreprocessorInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($preprocessor)) + ); + } + } + + $this->preprocessors = $preprocessors; + } + + /** + * @inheritdoc + */ + public function transform(string $value): string + { + $value = $this->escape($value); + foreach ($this->preprocessors as $preprocessor) { + $value = $preprocessor->process($value); + } + + return $value; + } + + /** + * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. + * + * @param string $value + * @return string + */ + private function escape(string $value): string + { + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $replace = '\\\$1'; + + return preg_replace($pattern, $replace, $value); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php new file mode 100644 index 0000000000000..c84ddc69cc7a8 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Value transformer of search term for matching with ES field types. + */ +interface ValueTransformerInterface +{ + /** + * Transform value according to field type. + * + * @param string $value + * @return mixed + */ + public function transform(string $value); +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php new file mode 100644 index 0000000000000..11a35d79ce1fd --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Pool of value transformers. + */ +class ValueTransformerPool +{ + /** + * @var ValueTransformerInterface[] + */ + private $transformers; + + /** + * @param ValueTransformerInterface[] $valueTransformers + */ + public function __construct(array $valueTransformers = []) + { + foreach ($valueTransformers as $valueTransformer) { + if (!$valueTransformer instanceof ValueTransformerInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($valueTransformer)) + ); + } + } + + $this->transformers = $valueTransformers; + } + + /** + * Get value transformer related to field type. + * + * @param string $fieldType + * @return ValueTransformerInterface + */ + public function get(string $fieldType): ValueTransformerInterface + { + return $this->transformers[$fieldType] ?? $this->transformers['default']; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php index 8114feb09d35d..d0ffc6debcd8a 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php @@ -5,14 +5,29 @@ */ namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface as TypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Match as MatchQueryBuilder; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; use Magento\Framework\Search\Request\Query\Match as MatchRequestQuery; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\MockObject\MockObject as MockObject; class MatchTest extends \PHPUnit\Framework\TestCase { + /** + * @var AttributeProvider|MockObject + */ + private $attributeProvider; + + /** + * @var TypeResolver|MockObject + */ + private $fieldTypeResolver; + /** * @var MatchQueryBuilder */ @@ -23,46 +38,63 @@ class MatchTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { + $this->attributeProvider = $this->createMock(AttributeProvider::class); + $this->fieldTypeResolver = $this->createMock(TypeResolver::class); + + $valueTransformerPoolMock = $this->createMock(ValueTransformerPool::class); + $valueTransformerMock = $this->createMock(ValueTransformerInterface::class); + $valueTransformerPoolMock->method('get') + ->willReturn($valueTransformerMock); + $valueTransformerMock->method('transform') + ->willReturnArgument(0); + $this->matchQueryBuilder = (new ObjectManager($this))->getObject( MatchQueryBuilder::class, [ 'fieldMapper' => $this->getFieldMapper(), 'preprocessorContainer' => [], + 'attributeProvider' => $this->attributeProvider, + 'fieldTypeResolver' => $this->fieldTypeResolver, + 'valueTransformerPool' => $valueTransformerPoolMock, ] ); } /** * Tests that method constructs a correct select query. - * @see MatchQueryBuilder::build - * - * @dataProvider queryValuesInvariantsProvider * - * @param string $rawQueryValue - * @param string $errorMessage + * @see MatchQueryBuilder::build */ - public function testBuild($rawQueryValue, $errorMessage) + public function testBuild() { - $this->assertSelectQuery( - $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'), - $errorMessage - ); - } + $attributeAdapter = $this->createMock(AttributeAdapter::class); + $this->attributeProvider->expects($this->once()) + ->method('getByAttributeCode') + ->with('some_field') + ->willReturn($attributeAdapter); + $this->fieldTypeResolver->expects($this->once()) + ->method('getFieldType') + ->with($attributeAdapter) + ->willReturn('text'); + + $rawQueryValue = 'query_value'; + $selectQuery = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'); - /** - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * - * @return array - */ - public function queryValuesInvariantsProvider() - { - return [ - ['query_value', 'Select query field must match simple raw query value.'], - ['query_value+', 'Specifying a trailing plus sign causes InnoDB to report a syntax error.'], - ['query_value-', 'Specifying a trailing minus sign causes InnoDB to report a syntax error.'], - ['query_@value', 'The @ symbol is reserved for use by the @distance proximity search operator.'], - ['query_value+@', 'The @ symbol is reserved for use by the @distance proximity search operator.'], + $expectedSelectQuery = [ + 'bool' => [ + 'must_not' => [ + [ + 'match' => [ + 'some_field' => [ + 'query' => $rawQueryValue, + 'boost' => 43, + ], + ], + ], + ], + ], ]; + $this->assertEquals($expectedSelectQuery, $selectQuery); } /** @@ -76,6 +108,16 @@ public function queryValuesInvariantsProvider() */ public function testBuildMatchQuery($rawQueryValue, $queryValue, $match) { + $attributeAdapter = $this->createMock(AttributeAdapter::class); + $this->attributeProvider->expects($this->once()) + ->method('getByAttributeCode') + ->with('some_field') + ->willReturn($attributeAdapter); + $this->fieldTypeResolver->expects($this->once()) + ->method('getFieldType') + ->with($attributeAdapter) + ->willReturn('text'); + $query = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'should'); $expectedSelectQuery = [ @@ -111,30 +153,6 @@ public function matchProvider() ]; } - /** - * @param array $selectQuery - * @param string $errorMessage - */ - private function assertSelectQuery($selectQuery, $errorMessage) - { - $expectedSelectQuery = [ - 'bool' => [ - 'must_not' => [ - [ - 'match' => [ - 'some_field' => [ - 'query' => 'query_value', - 'boost' => 43, - ], - ], - ], - ], - ], - ]; - - $this->assertEquals($expectedSelectQuery, $selectQuery, $errorMessage); - } - /** * Gets fieldMapper mock object. * diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 4a354a2ea528f..9732ae8226431 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -540,4 +540,22 @@ </argument> </arguments> </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool"> + <arguments> + <argument name="valueTransformers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer</item> + <item name="date" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\DateTransformer</item> + <item name="float" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\FloatTransformer</item> + <item name="integer" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\IntegerTransformer</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer"> + <arguments> + <argument name="preprocessors" xsi:type="array"> + <item name="stopwordsPreprocessor" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\Preprocessor\Stopwords</item> + <item name="synonymsPreprocessor" xsi:type="object">Magento\Search\Adapter\Query\Preprocessor\Synonyms</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Elasticsearch6/etc/di.xml b/app/code/Magento/Elasticsearch6/etc/di.xml index 9999c29c1a257..011dfa1019738 100644 --- a/app/code/Magento/Elasticsearch6/etc/di.xml +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -170,4 +170,36 @@ </argument> </arguments> </type> + + <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + </argument> + </arguments> + </virtualType> + + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> + </arguments> + </type> + + <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + </argument> + </arguments> + </virtualType> </config> diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml new file mode 100644 index 0000000000000..a9100b4730b8c --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml @@ -0,0 +1,30 @@ +<?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="AdminImportProductsActionGroup"> + <arguments> + <argument name="behavior" type="string"/> + <argument name="importFile" type="string"/> + <argument name="importMessage" type="string"/> + </arguments> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <waitForPageLoad stepKey="AdminImportMainSectionLoad"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="{{importFile}}" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <waitForPageLoad stepKey="AdminImportMainSectionLoad2"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="AdminMessagesSection"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="{{importMessage}}" stepKey="seeImportMessage"/> + </actionGroup> + </actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml index ad9e7672ce11a..528ad23aaf2bf 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml @@ -11,5 +11,11 @@ <element name="filterByAttributeCode" type="input" selector="#export_filter_grid_filter_attribute_code"/> <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> <element name="search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="chooseAttribute" type="checkbox" selector="//*[@name='export_filter[{{var}}]']/ancestor::tr//input[@type='checkbox']" parameterized="true"/> + <element name="fillFilter" type="input" selector="//*[@name='export_filter[{{var}}]']/ancestor::tr//input[@type='text']" parameterized="true"/> + <element name="continueBtn" type="button" selector="//*[@id='export_filter_container']/button" timeout="30"/> + <element name="selectByIndex" type="button" selector="//tr[@data-repeat-index='{{var}}']//button" parameterized="true" timeout="30"/> + <element name="download" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Download']" parameterized="true" timeout="30"/> + <element name="delete" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Delete']" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml new file mode 100644 index 0000000000000..ceb4e93e4e9aa --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -0,0 +1,123 @@ +<?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="AdminImportProductsWithAddUpdateBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with add/update behavior."/> + <stories value="Import Products"/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with add/update behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14077"/> + <group value="importExport"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product1 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <field key="name">SimpleProductForTest1</field> + <field key="sku">SimpleProductForTest1</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create Website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + + <!-- Create store group --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> + <!-- Delete all products that replaced products in the before block post import --> + <deleteData stepKey="deleteSimpleProduct1" url="/V1/products/SimpleProductForTest1"/> + <deleteData stepKey="deleteSimpleProduct2" url="/V1/products/SimpleProductForTest2"/> + <deleteData stepKey="deleteSimpleProduct3" url="/V1/products/SimpleProductForTest3"/> + + <!-- Delete category created in the before block --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Delete website created in the before block --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite" before="logoutFromAdmin"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="catalog_import_products.csv"/> + <argument name="importMessage" value="Created: 2, Updated: 1, Deleted: 0"/> + </actionGroup> + + <!-- Assert Simple Product1 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct1OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert Simple Product1 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct1OnEditPage"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert Simple Product2 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct2OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert Simple Product2 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct2OnEditPage"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert Simple Product3 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct3OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + + <!-- Assert Simple Product3 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct3OnEditPage"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + + <!-- Assert SimpleProduct1 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct1Validation"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert SimpleProduct2 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct2Validation"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert SimpleProduct3 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct3Validation"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml new file mode 100644 index 0000000000000..d63a5546716b1 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml @@ -0,0 +1,45 @@ +<?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="AdminImportProductsWithReplaceBehaviorTest" extends="AdminImportProductsWithAddUpdateBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with replace behavior."/> + <stories value="Import Products"/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with replace behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14076"/> + <group value="importExport"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product2 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <field key="name">SimpleProductForTest2</field> + <field key="sku">SimpleProductForTest2</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Simple Product3 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct3"> + <field key="name">SimpleProductForTest3</field> + <field key="sku">SimpleProductForTest3</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <!-- Import products with replace behavior --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Replace"/> + <argument name="importFile" value="catalog_import_products.csv"/> + <argument name="importMessage" value="Created: 3, Updated: 0, Deleted: 3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 87a7cce58e1a5..2821a46f29416 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Indexer\Model; use Magento\Framework\Indexer\ActionFactory; @@ -14,6 +15,8 @@ use Magento\Framework\Indexer\StructureFactory; /** + * Indexer model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface @@ -361,7 +364,7 @@ public function getLatestUpdated() return $this->getView()->getUpdated(); } } - return $this->getState()->getUpdated(); + return $this->getState()->getUpdated() ?: ''; } /** diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 6b7cc12218990..ca2da9585f934 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -164,7 +164,12 @@ public function testGetLatestUpdated($getViewIsEnabled, $getViewGetUpdated, $get } } } else { - $this->assertEquals($getStateGetUpdated, $this->model->getLatestUpdated()); + $getLatestUpdated = $this->model->getLatestUpdated(); + $this->assertEquals($getStateGetUpdated, $getLatestUpdated); + + if ($getStateGetUpdated === null) { + $this->assertNotNull($getLatestUpdated); + } } } @@ -182,7 +187,8 @@ public function getLatestUpdatedDataProvider() [true, '', '06-Jan-1944'], [true, '06-Jan-1944', ''], [true, '', ''], - [true, '06-Jan-1944', '05-Jan-1944'] + [true, '06-Jan-1944', '05-Jan-1944'], + [false, null, null], ]; } diff --git a/app/code/Magento/MessageQueue/Api/PoisonPillCompareInterface.php b/app/code/Magento/MessageQueue/Api/PoisonPillCompareInterface.php new file mode 100644 index 0000000000000..3d5b895575597 --- /dev/null +++ b/app/code/Magento/MessageQueue/Api/PoisonPillCompareInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Api; + +/** + * Interface describes how to describes how to compare poison pill with latest in DB. + * + * @api + */ +interface PoisonPillCompareInterface +{ + /** + * Check if version of current poison pill is latest. + * + * @param int $poisonPillVersion + * @return bool + */ + public function isLatestVersion(int $poisonPillVersion): bool; +} diff --git a/app/code/Magento/MessageQueue/Api/PoisonPillPutInterface.php b/app/code/Magento/MessageQueue/Api/PoisonPillPutInterface.php new file mode 100644 index 0000000000000..02293c99bb3f4 --- /dev/null +++ b/app/code/Magento/MessageQueue/Api/PoisonPillPutInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Api; + +/** + * Command interface describes how to create new version on poison pill. + * + * @api + */ +interface PoisonPillPutInterface +{ + /** + * Put new version of poison pill inside DB. + * + * @return int + * @throws \Exception + */ + public function put(): int; +} diff --git a/app/code/Magento/MessageQueue/Api/PoisonPillReadInterface.php b/app/code/Magento/MessageQueue/Api/PoisonPillReadInterface.php new file mode 100644 index 0000000000000..db97990ebbad5 --- /dev/null +++ b/app/code/Magento/MessageQueue/Api/PoisonPillReadInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Api; + +/** + * Describes how to get latest version of poison pill. + * + * @api + */ +interface PoisonPillReadInterface +{ + /** + * Returns get latest version of poison pill. + * + * @return int + */ + public function getLatestVersion(): int; +} diff --git a/app/code/Magento/MessageQueue/Model/CallbackInvoker.php b/app/code/Magento/MessageQueue/Model/CallbackInvoker.php new file mode 100644 index 0000000000000..f37f2157c3575 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/CallbackInvoker.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\Framework\MessageQueue\CallbackInvokerInterface; +use Magento\Framework\MessageQueue\QueueInterface; +use Magento\MessageQueue\Api\PoisonPillCompareInterface; +use Magento\MessageQueue\Api\PoisonPillReadInterface; + +/** + * Callback invoker + */ +class CallbackInvoker implements CallbackInvokerInterface +{ + /** + * @var PoisonPillReadInterface $poisonPillRead + */ + private $poisonPillRead; + + /** + * @var int $poisonPillVersion + */ + private $poisonPillVersion; + + /** + * @var PoisonPillCompareInterface + */ + private $poisonPillCompare; + + /** + * @param PoisonPillReadInterface $poisonPillRead + * @param PoisonPillCompareInterface $poisonPillCompare + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead, + PoisonPillCompareInterface $poisonPillCompare + ) { + $this->poisonPillRead = $poisonPillRead; + $this->poisonPillCompare = $poisonPillCompare; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.ExitExpression) + */ + public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback) + { + $this->poisonPillVersion = $this->poisonPillRead->getLatestVersion(); + for ($i = $maxNumberOfMessages; $i > 0; $i--) { + do { + $message = $queue->dequeue(); + } while ($message === null && (sleep(1) === 0)); + if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) { + $queue->reject($message); + exit(0); + } + $callback($message); + } + } +} diff --git a/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php b/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php new file mode 100644 index 0000000000000..a8e40ea495002 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\MessageQueue\Api\PoisonPillCompareInterface; +use Magento\MessageQueue\Api\PoisonPillReadInterface; + +/** + * Poison pill compare + */ +class PoisonPillCompare implements PoisonPillCompareInterface +{ + /** + * @var PoisonPillReadInterface + */ + private $poisonPillRead; + + /** + * PoisonPillCompare constructor. + * @param PoisonPillReadInterface $poisonPillRead + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead + ) { + $this->poisonPillRead = $poisonPillRead; + } + + /** + * @inheritdoc + */ + public function isLatestVersion(int $poisonPillVersion): bool + { + return $poisonPillVersion === $this->poisonPillRead->getLatestVersion(); + } +} diff --git a/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php b/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php new file mode 100644 index 0000000000000..283fff8ace7c7 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model\ResourceModel; + +use Magento\MessageQueue\Api\PoisonPillReadInterface; +use Magento\MessageQueue\Api\PoisonPillPutInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; + +/** + * PoisonPill. + */ +class PoisonPill extends AbstractDb implements PoisonPillPutInterface, PoisonPillReadInterface +{ + /** + * Table name. + */ + const QUEUE_POISON_PILL_TABLE = 'queue_poison_pill'; + + /** + * PoisonPill constructor. + * + * @param Context $context + * @param string|null $connectionName + */ + public function __construct( + Context $context, + string $connectionName = null + ) { + parent::__construct($context, $connectionName); + } + + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init(self::QUEUE_POISON_PILL_TABLE, 'version'); + } + + /** + * @inheritdoc + */ + public function put(): int + { + $connection = $this->getConnection(); + $table = $this->getMainTable(); + $connection->insert($table, []); + return (int)$connection->lastInsertId($table); + } + + /** + * @inheritdoc + */ + public function getLatestVersion() : int + { + $select = $this->getConnection()->select()->from( + $this->getTable(self::QUEUE_POISON_PILL_TABLE), + 'version' + )->order( + 'version ' . \Magento\Framework\DB\Select::SQL_DESC + )->limit( + 1 + ); + + $version = (int)$this->getConnection()->fetchOne($select); + + return $version; + } +} diff --git a/app/code/Magento/MessageQueue/etc/db_schema.xml b/app/code/Magento/MessageQueue/etc/db_schema.xml index 7a20d2bd4df5d..9cdf414dd06e1 100644 --- a/app/code/Magento/MessageQueue/etc/db_schema.xml +++ b/app/code/Magento/MessageQueue/etc/db_schema.xml @@ -21,4 +21,12 @@ <column name="message_code"/> </constraint> </table> + <table name="queue_poison_pill" resource="default" engine="innodb" + comment="Sequence table for poison pill versions"> + <column xsi:type="int" name="version" padding="10" unsigned="true" nullable="false" identity="true" + comment="Poison Pill version."/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="version"/> + </constraint> + </table> </schema> diff --git a/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json b/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json index f31981d2ec40f..d9d623a994b37 100644 --- a/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json +++ b/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json @@ -9,5 +9,13 @@ "PRIMARY": true, "QUEUE_LOCK_MESSAGE_CODE": true } + }, + "queue_poison_pill": { + "column": { + "version": true + }, + "constraint": { + "PRIMARY": true + } } -} \ No newline at end of file +} diff --git a/app/code/Magento/MessageQueue/etc/di.xml b/app/code/Magento/MessageQueue/etc/di.xml index c8f2edb862613..22cfea976a722 100644 --- a/app/code/Magento/MessageQueue/etc/di.xml +++ b/app/code/Magento/MessageQueue/etc/di.xml @@ -13,6 +13,10 @@ <preference for="Magento\Framework\MessageQueue\EnvelopeInterface" type="Magento\Framework\MessageQueue\Envelope"/> <preference for="Magento\Framework\MessageQueue\ConsumerInterface" type="Magento\Framework\MessageQueue\Consumer"/> <preference for="Magento\Framework\MessageQueue\MergedMessageInterface" type="Magento\Framework\MessageQueue\MergedMessage"/> + <preference for="Magento\MessageQueue\Api\PoisonPillCompareInterface" type="Magento\MessageQueue\Model\PoisonPillCompare"/> + <preference for="Magento\MessageQueue\Api\PoisonPillPutInterface" type="Magento\MessageQueue\Model\ResourceModel\PoisonPill"/> + <preference for="Magento\MessageQueue\Api\PoisonPillReadInterface" type="Magento\MessageQueue\Model\ResourceModel\PoisonPill"/> + <preference for="Magento\Framework\MessageQueue\CallbackInvokerInterface" type="Magento\MessageQueue\Model\CallbackInvoker"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js index 9b867cd7217b1..3a6d73e304974 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js @@ -15,7 +15,7 @@ define([ opacity: 0.5, // CSS opacity for the 'Place Order' button when it's clicked and then disabled. pleaseWaitLoader: 'span.please-wait', // 'Submitting order information...' Ajax loader. placeOrderSubmit: 'button[type="submit"]', // The 'Place Order' button. - agreements: '#checkout-agreements' // Container for all of the checkout agreements and terms/conditions + agreements: '.checkout-agreements' // Container for all of the checkout agreements and terms/conditions }, /** diff --git a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml index 34a77095d524d..ee0c32633569a 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml @@ -30,5 +30,6 @@ <element name="pageCacheCheckbox" type="checkbox" selector="input[value='full_page']"/> <element name="webServicesConfigCheckbox" type="checkbox" selector="input[value='config_webservice']"/> <element name="translationsCheckbox" type="checkbox" selector="input[value='translate']"/> + <element name="additionalCacheButton" type="button" selector="//*[@id='container']//button[contains(., '{{cacheType}}')]" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml new file mode 100644 index 0000000000000..bd6f7ba362bf4 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -0,0 +1,35 @@ +<?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="FlushStaticFilesCacheButtonVisibilityTest"> + <annotations> + <features value="PageCache"/> + <stories value="Page Cache"/> + <title value="Check visibility of flush static files cache button"/> + <description value="Flush Static Files Cache button visibility"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15454"/> + <group value="production_mode_only"/> + <group value="pagecache"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Log in to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Open Cache Management page --> + <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="amOnPageCacheManagement"/> + <waitForPageLoad stepKey="waitForPageCacheManagementLoad"/> + + <!-- Check 'Flush Static Files Cache' not visible in production mode. --> + <dontSee selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton" /> + </test> +</tests> diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 8f216b64aa9b0..2fcfd2dfadabb 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -532,19 +532,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $this->quoteRepository->save($quote); } catch (\Exception $e) { - if (!empty($this->addressesToSync)) { - foreach ($this->addressesToSync as $addressId) { - $this->addressRepository->deleteById($addressId); - } - } - $this->eventManager->dispatch( - 'sales_model_service_quote_submit_failure', - [ - 'order' => $order, - 'quote' => $quote, - 'exception' => $e - ] - ); + $this->rollbackAddresses($quote, $order, $e); throw $e; } return $order; @@ -611,4 +599,41 @@ protected function _prepareCustomerQuote($quote) $shipping->setIsDefaultBilling(true); } } + + /** + * Remove related to order and quote addresses and submit exception to further processing. + * + * @param Quote $quote + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Exception $e + * @throws \Exception + */ + private function rollbackAddresses( + QuoteEntity $quote, + \Magento\Sales\Api\Data\OrderInterface $order, + \Exception $e + ): void { + try { + if (!empty($this->addressesToSync)) { + foreach ($this->addressesToSync as $addressId) { + $this->addressRepository->deleteById($addressId); + } + } + $this->eventManager->dispatch( + 'sales_model_service_quote_submit_failure', + [ + 'order' => $order, + 'quote' => $quote, + 'exception' => $e, + ] + ); + } catch (\Exception $consecutiveException) { + $message = sprintf( + "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s", + $consecutiveException->getMessage() + ); + + throw new \Exception($message, 0, $e); + } + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 1b32866ed883c..6868ce3f7f1ff 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -67,6 +67,11 @@ public function execute(Quote $cart, array $cartItemData): void { $sku = $this->extractSku($cartItemData); $qty = $this->extractQty($cartItemData); + if ($qty <= 0) { + throw new GraphQlInputException( + __('Please enter a number greater than 0 in this field.') + ); + } $customizableOptions = $this->extractCustomizableOptions($cartItemData); try { diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php index a93c8032c996a..d1dcb4a48a76b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php @@ -60,12 +60,12 @@ public function __construct( public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { - throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } $maskedCartId = $args['input']['cart_id']; if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) { - throw new GraphQlInputException(__('Required parameter "payment_method" is missing')); + throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); } $paymentMethodCode = $args['input']['payment_method']['code']; diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index da7ab97a1b211..d89a118bff94b 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -769,11 +769,12 @@ public function addOrdersCount() */ public function addRevenueToSelect($convertCurrency = false) { - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( !$convertCurrency, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns(['revenue' => $expr]); @@ -791,11 +792,12 @@ public function addSumAvgTotals($storeId = 0) /** * calculate average and total amount */ - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( $storeId, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns( @@ -808,13 +810,15 @@ public function addSumAvgTotals($storeId = 0) } /** - * Get SQL expression for totals + * Get SQL expression for totals. * * @param int $storeId * @param string $baseSubtotalRefunded * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string + * @deprecated + * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( $storeId, @@ -825,10 +829,40 @@ protected function getTotalsExpression( $template = ($storeId != 0) ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)' : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) ' - . ' * main_table.base_to_global_rate)'; + . ' * main_table.base_to_global_rate)'; return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled); } + /** + * Get SQL expression for totals with discount refunded. + * + * @param int $storeId + * @param string $baseSubtotalRefunded + * @param string $baseSubtotalCanceled + * @param string $baseDiscountRefunded + * @param string $baseDiscountCanceled + * @return string + */ + private function getTotalsExpressionWithDiscountRefunded( + $storeId, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ) { + $template = ($storeId != 0) + ? '(main_table.base_subtotal - %2$s - %1$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s))' + : '((main_table.base_subtotal - %1$s - %2$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s)) ' + . ' * main_table.base_to_global_rate)'; + return sprintf( + $template, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ); + } + /** * Sort order by total amount * diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 09b80750da14d..46fdb20dee4a1 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -16,8 +16,8 @@ </resource> <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> - <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::reviews_all" title="All Reviews" translate="title" sortOrder="10"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index 0a2e49450e0cf..7376329471921 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -8,8 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> - <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="20" action="review/product/pending" resource="Magento_Review::pending"/> + <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="All Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js index d1c40959e3ec2..88c61fa38af34 100644 --- a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js +++ b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js @@ -20,7 +20,7 @@ define([ showLoader: false, loaderContext: $('.product.data.items') }).done(function (data) { - $('#product-review-container').html(data); + $('#product-review-container').html(data).trigger('contentUpdated'); $('[data-role="product-review"] .pages a').each(function (index, element) { $(element).click(function (event) { //eslint-disable-line max-nested-callbacks processReviews($(element).attr('href'), true); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php index 3295b244f323e..ceb231248ef5e 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php @@ -1,18 +1,21 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Framework\Registry; use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Service\InvoiceService; +/** + * Create new invoice action. + */ class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** @@ -37,22 +40,32 @@ class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInte */ private $invoiceService; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** * @param Action\Context $context * @param Registry $registry * @param PageFactory $resultPageFactory * @param InvoiceService $invoiceService + * @param OrderRepositoryInterface|null $orderRepository */ public function __construct( Action\Context $context, Registry $registry, PageFactory $resultPageFactory, - InvoiceService $invoiceService + InvoiceService $invoiceService, + OrderRepositoryInterface $orderRepository = null ) { + parent::__construct($context); + $this->registry = $registry; $this->resultPageFactory = $resultPageFactory; - parent::__construct($context); $this->invoiceService = $invoiceService; + $this->orderRepository = $orderRepository ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); } /** @@ -78,14 +91,11 @@ public function execute() { $orderId = $this->getRequest()->getParam('order_id'); $invoiceData = $this->getRequest()->getParam('invoice', []); - $invoiceItems = isset($invoiceData['items']) ? $invoiceData['items'] : []; + $invoiceItems = $invoiceData['items'] ?? []; try { /** @var \Magento\Sales\Model\Order $order */ - $order = $this->_objectManager->create(\Magento\Sales\Model\Order::class)->load($orderId); - if (!$order->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The order no longer exists.')); - } + $order = $this->orderRepository->get($orderId); if (!$order->canInvoice()) { throw new \Magento\Framework\Exception\LocalizedException( diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index 31cb5bb1f60ca..5d3186781e7d7 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Validator.php +++ b/app/code/Magento/Sales/Model/Order/Address/Validator.php @@ -49,8 +49,8 @@ class Validator /** * @param DirectoryHelper $directoryHelper - * @param CountryFactory $countryFactory - * @param EavConfig $eavConfig + * @param CountryFactory $countryFactory + * @param EavConfig $eavConfig */ public function __construct( DirectoryHelper $directoryHelper, @@ -61,6 +61,17 @@ public function __construct( $this->countryFactory = $countryFactory; $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(EavConfig::class); + } + + /** + * Validate address. + * + * @param \Magento\Sales\Model\Order\Address $address + * @return array + */ + public function validate(Address $address) + { + $warnings = []; if ($this->isTelephoneRequired()) { $this->required['telephone'] = 'Phone Number'; @@ -73,16 +84,7 @@ public function __construct( if ($this->isFaxRequired()) { $this->required['fax'] = 'Fax'; } - } - /** - * - * @param \Magento\Sales\Model\Order\Address $address - * @return array - */ - public function validate(Address $address) - { - $warnings = []; foreach ($this->required as $code => $label) { if (!$address->hasData($code)) { $warnings[] = sprintf('"%s" is required. Enter and try again.', $label); @@ -195,7 +197,10 @@ protected function isStateRequired($countryId) } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isTelephoneRequired() { @@ -203,7 +208,10 @@ protected function isTelephoneRequired() } /** + * Check whether company is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isCompanyRequired() { @@ -211,7 +219,10 @@ protected function isCompanyRequired() } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isFaxRequired() { diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml new file mode 100644 index 0000000000000..8108577145421 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.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"> + <!-- Fill Order status form and click save --> + <actionGroup name="AdminOrderStatusFormFillAndSave"> + <arguments> + <argument name="status" type="string" /> + <argument name="label" type="string" /> + </arguments> + + <fillField stepKey="fillStatusCode" selector="{{AdminOrderStatusFormSection.statusCodeField}}" userInput="{{status}}"/> + <fillField stepKey="fillStatusLabel" selector="{{AdminOrderStatusFormSection.statusLabelField}}" userInput="{{label}}"/> + <click stepKey="clickSaveStatus" selector="{{AdminMainActionsSection.save}}"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml new file mode 100644 index 0000000000000..5f69f52987688 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml @@ -0,0 +1,24 @@ +<?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"> + <!-- Search order status grid for item with a specific code and validate data --> + <actionGroup name="AssertOrderStatusExistsInGrid"> + <arguments> + <argument name="status" type="string" /> + <argument name="label" type="string" /> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters"/> + <fillField selector="{{AdminOrderStatusGridSection.statusCodeFilterField}}" userInput="{{status}}" stepKey="fillStatusFilter"/> + <click selector="{{AdminSecondaryGridSection.searchButton}}" stepKey="clickSearch"/> + <see selector="{{AdminOrderStatusGridSection.statusCodeDataColumn}}" userInput="{{status}}" stepKey="seeStatusCodeInGrid"/> + <see selector="{{AdminOrderStatusGridSection.statusLabelDataColumn}}" userInput="{{label}}" stepKey="seeStatusLabelInGrid"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml new file mode 100644 index 0000000000000..5b4c3115744c9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Assert that order status is not saved with duplication error message --> + <actionGroup name="AssertOrderStatusFormSaveDuplicateError"> + <see selector="{{AdminMessagesSection.error}}" userInput="We found another order status with the same order status code." stepKey="seeError"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml new file mode 100644 index 0000000000000..d82f4b9dd25e8 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Assert that order status saved with success message --> + <actionGroup name="AssertOrderStatusFormSaveSuccess"> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the order status." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml new file mode 100644 index 0000000000000..aecd7fcf1b703 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="defaultOrderStatus"> + <data key="status" unique="suffix">order_status</data> + <data key="label" unique="suffix">orderLabel</data> + </entity> + <entity name="duplicatingCodeOrderStatus"> + <data key="status">pending</data> + <data key="label" unique="suffix">orderLabel</data> + </entity> + <entity name="duplicatingLabelOrderStatus"> + <data key="status" unique="suffix">order_status</data> + <data key="label">Suspected Fraud</data> + </entity> +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml new file mode 100644 index 0000000000000..b158e4923074a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderStatusPage" url="sales/order_status" area="admin" module="Magento_Sales"> + <section name="AdminOrderStatusFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml new file mode 100644 index 0000000000000..1058b2d6f2177 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml @@ -0,0 +1,15 @@ +<?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="AdminOrderStatusFormSection"> + <element name="statusCodeField" type="text" selector="#edit_form [name=status]"/> + <element name="statusLabelField" type="text" selector="#edit_form [name=label]"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml new file mode 100644 index 0000000000000..b624639281187 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderStatusGridSection"> + <element name="statusCodeFilterField" type="input" selector="[data-role=filter-form] [name=status]"/> + <element name="statusCodeDataColumn" type="input" selector="[data-role=row] [data-column=status]"/> + <element name="statusLabelDataColumn" type="input" selector="[data-role=row] [data-column=label]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml new file mode 100644 index 0000000000000..40a731410a899 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml @@ -0,0 +1,39 @@ +<?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="AdminCreateOrderStatusDuplicatingCodeTest"> + <annotations> + <stories value="Create order status"/> + <title value="Create order status with duplicating code"/> + <description value="Receive error when creating order status with the code which is already exist"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15432" /> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{duplicatingCodeOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingCodeOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveDuplicateError" stepKey="seeFormSaveDuplicateError"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml new file mode 100644 index 0000000000000..d1381bbb1efb0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml @@ -0,0 +1,45 @@ +<?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="AdminCreateOrderStatusDuplicatingLabelTest"> + <annotations> + <stories value="Create order status"/> + <title value="Create order status with duplicating label"/> + <description value="Create an order status and get success message"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15433" /> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{duplicatingLabelOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingLabelOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> + + <!-- Verify the order status grid page shows the order status we just created --> + <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> + <argument name="status" value="{{duplicatingLabelOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingLabelOrderStatus.label}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml new file mode 100644 index 0000000000000..c2daaac84dd42 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml @@ -0,0 +1,45 @@ +<?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="AdminCreateOrderStatusTest"> + <annotations> + <stories value="Create custom order status"/> + <title value="Create custom order status"/> + <description value="Tests opening admin order status page, create a new order status with success message"/> + <testCaseId value="MC-15431" /> + <severity value="AVERAGE"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> + + <!-- Verify the order status grid page shows the order status we just created --> + <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php index 05c99c9f9ef98..87e27fdb2206b 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php @@ -6,12 +6,14 @@ namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\OrderRepositoryInterface; /** * Class NewActionTest * @package Magento\Sales\Controller\Adminhtml\Order\Invoice * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class NewActionTest extends \PHPUnit\Framework\TestCase { @@ -90,6 +92,11 @@ class NewActionTest extends \PHPUnit\Framework\TestCase */ protected $invoiceServiceMock; + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -215,12 +222,15 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); + $this->controller = $objectManager->getObject( \Magento\Sales\Controller\Adminhtml\Order\Invoice\NewAction::class, [ 'context' => $contextMock, 'resultPageFactory' => $this->resultPageFactoryMock, - 'invoiceService' => $this->invoiceServiceMock + 'invoiceService' => $this->invoiceServiceMock, + 'orderRepository' => $this->orderRepositoryMock ] ); } @@ -250,19 +260,17 @@ public function testExecute() $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getId', 'canInvoice']) + ->setMethods(['load', 'canInvoice']) ->getMock(); - $orderMock->expects($this->once()) - ->method('load') - ->with($orderId) - ->willReturnSelf(); - $orderMock->expects($this->once()) - ->method('getId') - ->willReturn($orderId); $orderMock->expects($this->once()) ->method('canInvoice') ->willReturn(true); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + $this->invoiceServiceMock->expects($this->once()) ->method('prepareInvoice') ->with($orderMock, []) @@ -285,11 +293,7 @@ public function testExecute() ->with(true) ->will($this->returnValue($commentText)); - $this->objectManagerMock->expects($this->at(0)) - ->method('create') - ->with(\Magento\Sales\Model\Order::class) - ->willReturn($orderMock); - $this->objectManagerMock->expects($this->at(1)) + $this->objectManagerMock->expects($this->once()) ->method('get') ->with(\Magento\Backend\Model\Session::class) ->will($this->returnValue($this->sessionMock)); @@ -318,19 +322,12 @@ public function testExecuteNoOrder() $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getId', 'canInvoice']) + ->setMethods(['canInvoice']) ->getMock(); - $orderMock->expects($this->once()) - ->method('load') - ->with($orderId) - ->willReturnSelf(); - $orderMock->expects($this->once()) - ->method('getId') - ->willReturn(null); - $this->objectManagerMock->expects($this->at(0)) - ->method('create') - ->with(\Magento\Sales\Model\Order::class) + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) ->willReturn($orderMock); $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 5a5dd925a3098..68fcd17122bd2 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -1015,4 +1015,9 @@ <preference for="Magento\Sales\Api\OrderCustomerDelegateInterface" type="Magento\Sales\Model\Order\OrderCustomerDelegate" /> + <type name="Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker"> + <arguments> + <argument name="productAvailabilityChecks" xsi:type="array" /> + </arguments> + </type> </config> diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index 89ec2b84572fc..cf6301cb31a9c 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -61,6 +61,7 @@ public function __construct( public function loadAttributeOptions() { $attributes = [ + 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 46e794a1954cf..45eee0a4001d1 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -87,7 +87,7 @@ private function queryByPhrase($phrase) { $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], - $phrase, + $this->escapePhrase($phrase), Fulltext::FULLTEXT_MODE_BOOLEAN ); $query = $this->getConnection()->select()->from( @@ -97,6 +97,18 @@ private function queryByPhrase($phrase) return $this->getConnection()->fetchAll($query); } + /** + * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. + * + * @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html + * @param string $phrase + * @return string + */ + private function escapePhrase(string $phrase): string + { + return preg_replace('/@+|[@+-]+$/', '', $phrase); + } + /** * A private helper function to retrieve matching synonym groups per scope * diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml new file mode 100644 index 0000000000000..e0b3d4b850bbb --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml @@ -0,0 +1,30 @@ +<?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"> + <!-- Filter by search query and select --> + <actionGroup name="searchTermFilterBySearchQuery"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForResetFilter"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultLoad"/> + <checkOption selector="{{AdminCatalogSearchTermIndexSection.searchTermRowCheckboxBySearchQuery(searchQuery)}}" stepKey="checkCheckBox"/> + </actionGroup> + + <!-- Delete search term --> + <actionGroup name="deleteSearchTerm"> + <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> + <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> + <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> + <waitForElementVisible selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml new file mode 100644 index 0000000000000..1518adad01347 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SearchTerm" type="searchTerm"> + <data key="query_text" unique="suffix">Query text</data> + <data key="store_id">1</data> + <data key="redirect" unique="suffix">http://example.com/</data> + <data key="display_in_terms">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml b/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml new file mode 100644 index 0000000000000..0bd2dc9be4855 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateSearchTerm" dataType="searchTerm" type="create" auth="adminFormKey" url="/search/term/save/" method="POST"> + <field key="query_text">string</field> + <field key="store_id">integer</field> + <field key="redirect">string</field> + <field key="display_in_terms">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml index 9e5bde9a2be49..81b025c9554e2 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml @@ -15,5 +15,6 @@ <element name="asLowAsLabel" type="text" selector=".minimal-price-link > span"/> <element name="textArea" type="text" selector="li[class='item']"/> <element name="regularPrice" type="text" selector="//span[@class='price-wrapper ']/span[@class='price']"/> + <element name="messageSection" type="text" selector="div .message"/> </section> </sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml new file mode 100644 index 0000000000000..67ccb51bf401e --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml @@ -0,0 +1,83 @@ +<?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="AdminMassDeleteSearchTermEntityTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Delete search term"/> + <title value="Admin mass delete search term entity test"/> + <description value="Admin should be able to Mass Delete Search Term Entity Test"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14767"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create three search term --> + <createData entity="SearchTerm" stepKey="createFirstSearchTerm"/> + <createData entity="SearchTerm" stepKey="createSecondSearchTerm"/> + <createData entity="SearchTerm" stepKey="createThirdSearchTerm"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to the catalog search term page --> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + + <!-- Select all created below search terms --> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByFirstSearchQuery"> + <argument name="searchQuery" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterBySecondSearchQuery"> + <argument name="searchQuery" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByThirdSearchQuery"> + <argument name="searchQuery" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + + <!-- Delete created below search terms --> + <actionGroup ref="deleteSearchTerm" stepKey="deleteSearchTerms"/> + + <!-- Assert search terms are absent on the search term page --> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertFirstSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertSecondSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertThirdSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + + <!-- Go to storefront page --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + + <!-- Verify search term deletion on storefront --> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForFirstSearchTerm"> + <argument name="phrase" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForFirstSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForSecondSearchTerm"> + <argument name="phrase" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForSecondSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForThirdSearchTerm"> + <argument name="phrase" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForThirdSearchTerm"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php index fa83db490e380..c417a202321b2 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php @@ -1,14 +1,27 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; -class AddTrack extends \Magento\Backend\App\Action +/** + * Add new tracking number to shipment controller. + */ +class AddTrack extends Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -18,27 +31,54 @@ class AddTrack extends \Magento\Backend\App\Action const ADMIN_RESOURCE = 'Magento_Sales::shipment'; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader + * @var ShipmentLoader */ protected $shipmentLoader; + /** + * @var ShipmentRepositoryInterface + */ + private $shipmentRepository; + + /** + * @var ShipmentTrackInterfaceFactory + */ + private $trackFactory; + + /** + * @var SerializerInterface + */ + private $serializer; + /** * @param Action\Context $context - * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader + * @param ShipmentLoader $shipmentLoader + * @param ShipmentRepositoryInterface|null $shipmentRepository + * @param ShipmentTrackInterfaceFactory|null $trackFactory + * @param SerializerInterface|null $serializer */ public function __construct( Action\Context $context, - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader + ShipmentLoader $shipmentLoader, + ShipmentRepositoryInterface $shipmentRepository = null, + ShipmentTrackInterfaceFactory $trackFactory = null, + SerializerInterface $serializer = null ) { - $this->shipmentLoader = $shipmentLoader; parent::__construct($context); + + $this->shipmentLoader = $shipmentLoader; + $this->shipmentRepository = $shipmentRepository ?: ObjectManager::getInstance() + ->get(ShipmentRepositoryInterface::class); + $this->trackFactory = $trackFactory ?: ObjectManager::getInstance() + ->get(ShipmentTrackInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(SerializerInterface::class); } /** - * Add new tracking number action + * Add new tracking number action. * - * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @return ResultInterface */ public function execute() { @@ -46,28 +86,29 @@ public function execute() $carrier = $this->getRequest()->getPost('carrier'); $number = $this->getRequest()->getPost('number'); $title = $this->getRequest()->getPost('title'); + if (empty($carrier)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please specify a carrier.')); + throw new LocalizedException(__('Please specify a carrier.')); } if (empty($number)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a tracking number.')); + throw new LocalizedException(__('Please enter a tracking number.')); } + $this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id')); $this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id')); $this->shipmentLoader->setShipment($this->getRequest()->getParam('shipment')); $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); $shipment = $this->shipmentLoader->load(); if ($shipment) { - $track = $this->_objectManager->create( - \Magento\Sales\Model\Order\Shipment\Track::class - )->setNumber( + $track = $this->trackFactory->create()->setNumber( $number )->setCarrierCode( $carrier )->setTitle( $title ); - $shipment->addTrack($track)->save(); + $shipment->addTrack($track); + $this->shipmentRepository->save($shipment); $this->_view->loadLayout(); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Shipments')); @@ -78,16 +119,18 @@ public function execute() 'message' => __('We can\'t initialize shipment for adding tracking number.'), ]; } - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $response = ['error' => true, 'message' => $e->getMessage()]; } catch (\Exception $e) { $response = ['error' => true, 'message' => __('Cannot add tracking number.')]; } - if (is_array($response)) { - $response = $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($response); - $this->getResponse()->representJson($response); - } else { - $this->getResponse()->setBody($response); + + if (\is_array($response)) { + $response = $this->serializer->serialize($response); + + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($response); } + + return $this->resultFactory->create(ResultFactory::TYPE_RAW)->setContents($response); } } diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php index cf3bf68e10416..1c9cdb1fa7d5b 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php @@ -6,127 +6,162 @@ namespace Magento\Shipping\Test\Unit\Controller\Adminhtml\Order\Shipment; -use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\ViewInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Page\Config; +use Magento\Framework\View\Page\Title; +use Magento\Framework\View\Result\Page; +use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; /** - * Class AddTrackTest + * Class AddTrackTest covers AddTrack controller. * - * @package Magento\Shipping\Controller\Adminhtml\Order\Shipment * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AddTrackTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader|\PHPUnit_Framework_MockObject_MockObject + * @var ShipmentLoader|\PHPUnit_Framework_MockObject_MockObject */ - protected $shipmentLoader; + private $shipmentLoader; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack + * @var AddTrack */ - protected $controller; + private $controller; /** - * @var Action\Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; + private $context; /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; + private $request; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; + private $response; /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject + * @var ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManager; + private $view; /** - * @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Page|\PHPUnit_Framework_MockObject_MockObject */ - protected $view; + private $resultPageMock; /** - * @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject + * @var Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultPageMock; + private $pageConfigMock; /** - * @var \Magento\Framework\View\Page\Config|\PHPUnit_Framework_MockObject_MockObject + * @var Title|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageConfigMock; + private $pageTitleMock; /** - * @var \Magento\Framework\View\Page\Title|\PHPUnit_Framework_MockObject_MockObject + * @var ShipmentTrackInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageTitleMock; + private $trackFactory; + /** + * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $rawResult; + + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->shipmentLoader = $this->getMockBuilder( - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class + ShipmentLoader::class ) ->disableOriginalConstructor() ->setMethods(['setShipmentId', 'setOrderId', 'setShipment', 'setTracking', 'load']) ->getMock(); $this->context = $this->createPartialMock( - \Magento\Backend\App\Action\Context::class, + Context::class, [ 'getRequest', 'getResponse', 'getRedirect', 'getObjectManager', 'getTitle', - 'getView' + 'getView', + 'getResultFactory' ] ); $this->response = $this->createPartialMock( - \Magento\Framework\App\ResponseInterface::class, + ResponseInterface::class, ['setRedirect', 'sendResponse', 'setBody'] ); - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + $this->request = $this->getMockBuilder(Http::class) ->disableOriginalConstructor()->getMock(); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); - $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) + $this->view = $this->createMock(ViewInterface::class); + $this->resultPageMock = $this->getMockBuilder(Page::class) ->disableOriginalConstructor() ->getMock(); - $this->pageConfigMock = $this->getMockBuilder(\Magento\Framework\View\Page\Config::class) + $this->pageConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->pageTitleMock = $this->getMockBuilder(\Magento\Framework\View\Page\Title::class) + $this->pageTitleMock = $this->getMockBuilder(Title::class) ->disableOriginalConstructor() ->getMock(); + $this->trackFactory = $this->getMockBuilder(ShipmentTrackInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); + $this->rawResult = $this->getMockBuilder(ResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setContents']) + ->getMockForAbstractClass(); + $resultFactory = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); $this->context->expects($this->once()) ->method('getRequest') ->will($this->returnValue($this->request)); $this->context->expects($this->once()) ->method('getResponse') ->will($this->returnValue($this->response)); - $this->context->expects($this->once()) - ->method('getObjectManager') - ->will($this->returnValue($this->objectManager)); $this->context->expects($this->once()) ->method('getView') ->will($this->returnValue($this->view)); + $resultFactory->expects($this->once()) + ->method('create') + ->willReturn($this->rawResult); + $this->context->expects($this->once()) + ->method('getResultFactory') + ->willReturn($resultFactory); $this->controller = $objectManagerHelper->getObject( - \Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack::class, + AddTrack::class, [ 'context' => $this->context, 'shipmentLoader' => $this->shipmentLoader, 'request' => $this->request, 'response' => $this->response, - 'view' => $this->view + 'view' => $this->view, + 'trackFactory' => $this->trackFactory, ] ); } @@ -144,7 +179,7 @@ public function testExecute() $tracking = []; $shipmentData = ['items' => [], 'send_email' => '']; $shipment = $this->createPartialMock( - \Magento\Sales\Model\Order\Shipment::class, + Shipment::class, ['addTrack', '__wakeup', 'save'] ); $this->request->expects($this->any()) @@ -152,8 +187,10 @@ public function testExecute() ->will( $this->returnValueMap( [ - ['order_id', null, $orderId], ['shipment_id', null, $shipmentId], - ['shipment', null, $shipmentData], ['tracking', null, $tracking], + ['order_id', null, $orderId], + ['shipment_id', null, $shipmentId], + ['shipment', null, $shipmentData], + ['tracking', null, $tracking], ] ) ); @@ -183,14 +220,13 @@ public function testExecute() $this->shipmentLoader->expects($this->once()) ->method('load') ->will($this->returnValue($shipment)); - $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + $track = $this->getMockBuilder(Track::class) ->disableOriginalConstructor() ->setMethods(['__wakeup', 'setNumber', 'setCarrierCode', 'setTitle']) ->getMock(); - $this->objectManager->expects($this->atLeastOnce()) + $this->trackFactory->expects($this->once()) ->method('create') - ->with(\Magento\Sales\Model\Order\Shipment\Track::class) - ->will($this->returnValue($track)); + ->willReturn($track); $track->expects($this->once()) ->method('setNumber') ->with($number) @@ -206,8 +242,8 @@ public function testExecute() $this->view->expects($this->once()) ->method('loadLayout') ->will($this->returnSelf()); - $layout = $this->createMock(\Magento\Framework\View\LayoutInterface::class); - $menuBlock = $this->createPartialMock(\Magento\Framework\View\Element\BlockInterface::class, ['toHtml']); + $layout = $this->createMock(LayoutInterface::class); + $menuBlock = $this->createPartialMock(BlockInterface::class, ['toHtml']); $html = 'html string'; $this->view->expects($this->once()) ->method('getLayout') @@ -235,9 +271,10 @@ public function testExecute() $this->pageConfigMock->expects($this->any()) ->method('getTitle') ->willReturn($this->pageTitleMock); - $this->response->expects($this->once()) - ->method('setBody') - ->with($html); - $this->assertNull($this->controller->execute()); + $this->rawResult->expects($this->once()) + ->method('setContents') + ->with($html) + ->willReturnSelf(); + $this->assertInstanceOf(ResultInterface::class, $this->controller->execute()); } } diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index ccc3c65491422..19f104c9f3790 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -100,18 +100,24 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ private $eventManager; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory * @param \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory * @param \Magento\Config\Model\ResourceModel\Config\Data $configDataResource - * @param \Magento\Store\Model\Store $store - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param ResourceModel\Store\CollectionFactory $storeListFactory + * @param StoreManagerInterface $storeManager + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -125,13 +131,16 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - \Magento\Framework\Event\ManagerInterface $eventManager = null + \Magento\Framework\Event\ManagerInterface $eventManager = null, + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { $this->_configDataResource = $configDataResource; $this->_storeListFactory = $storeListFactory; $this->_storeManager = $storeManager; $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); parent::__construct( $context, $registry, @@ -244,6 +253,8 @@ public function getStoreCodes() } /** + * Get stores count + * * @return int */ public function getStoresCount() @@ -349,6 +360,8 @@ public function isCanDelete() } /** + * Get default store id + * * @return mixed */ public function getDefaultStoreId() @@ -365,6 +378,8 @@ public function setDefaultStoreId($defaultStoreId) } /** + * Get root category id + * * @return mixed */ public function getRootCategoryId() @@ -381,6 +396,8 @@ public function setRootCategoryId($rootCategoryId) } /** + * Get website id + * * @return mixed */ public function getWebsiteId() @@ -397,7 +414,7 @@ public function setWebsiteId($websiteId) } /** - * @return $this + * @inheritdoc */ public function beforeDelete() { @@ -445,6 +462,7 @@ public function afterSave() $this->_storeManager->reinitStores(); $this->eventManager->dispatch($this->_eventPrefix . '_save', ['group' => $group]); }); + $this->pillPut->put(); return parent::afterSave(); } @@ -473,7 +491,7 @@ public function getIdentities() } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -507,7 +525,7 @@ public function setCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -515,7 +533,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\GroupExtensionInterface $extensionAttributes @@ -524,7 +542,7 @@ public function setExtensionAttributes( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeType() @@ -533,7 +551,7 @@ public function getScopeType() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeTypeName() diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index c1ad5bdcfc068..b2a515b198b11 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -326,6 +326,11 @@ class Store extends AbstractExtensibleModel implements */ private $eventManager; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -352,6 +357,7 @@ class Store extends AbstractExtensibleModel implements * @param bool $isCustomEntryPoint * @param array $data optional generic object data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -380,7 +386,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, $isCustomEntryPoint = false, array $data = [], - \Magento\Framework\Event\ManagerInterface $eventManager = null + \Magento\Framework\Event\ManagerInterface $eventManager = null, + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -401,6 +408,8 @@ public function __construct( $this->websiteRepository = $websiteRepository; $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); parent::__construct( $context, $registry, @@ -1077,6 +1086,7 @@ public function afterSave() $this->getResource()->addCommitCallback(function () use ($event, $store) { $this->eventManager->dispatch($event, ['store' => $store]); }); + $this->pillPut->put(); return parent::afterSave(); } diff --git a/app/code/Magento/Store/Model/Website.php b/app/code/Magento/Store/Model/Website.php index c9a7d0013fe06..383b36fd63228 100644 --- a/app/code/Magento/Store/Model/Website.php +++ b/app/code/Magento/Store/Model/Website.php @@ -159,6 +159,11 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement */ protected $_currencyFactory; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -174,6 +179,7 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -190,7 +196,8 @@ public function __construct( \Magento\Directory\Model\CurrencyFactory $currencyFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { parent::__construct( $context, @@ -208,10 +215,12 @@ public function __construct( $this->_websiteFactory = $websiteFactory; $this->_storeManager = $storeManager; $this->_currencyFactory = $currencyFactory; + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); } /** - * init model + * Init model * * @return void */ @@ -495,6 +504,8 @@ public function getWebsiteGroupStore() } /** + * Get default group id + * * @return mixed */ public function getDefaultGroupId() @@ -511,6 +522,8 @@ public function setDefaultGroupId($defaultGroupId) } /** + * Get code + * * @return mixed */ public function getCode() @@ -543,7 +556,7 @@ public function setName($name) } /** - * @return $this + * @inheritdoc */ public function beforeDelete() { @@ -581,7 +594,7 @@ public function afterSave() if ($this->isObjectNew()) { $this->_storeManager->reinitStores(); } - + $this->pillPut->put(); return parent::afterSave(); } @@ -635,8 +648,7 @@ public function getDefaultStore() } /** - * Retrieve default stores select object - * Select fields website_id, store_id + * Retrieve default stores select object, select fields website_id, store_id * * @param bool $withDefault include/exclude default admin website * @return \Magento\Framework\DB\Select @@ -671,7 +683,7 @@ public function getIdentities() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeType() @@ -680,7 +692,7 @@ public function getScopeType() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeTypeName() @@ -689,7 +701,7 @@ public function getScopeTypeName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -697,7 +709,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\WebsiteExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml index ef8d77c8824ff..ca614ec24138c 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml @@ -36,4 +36,30 @@ <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabFromCurrentUrl"/> </actionGroup> + + <actionGroup name="AssertWebsiteInGrid"> + <arguments> + <argument name="websiteName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <seeElement selector="{{AdminStoresGridSection.websiteName(websiteName)}}" stepKey="seeAssertWebsiteInGrid"/> + </actionGroup> + + <actionGroup name="AssertWebsiteForm"> + <arguments> + <argument name="websiteName" type="string"/> + <argument name="websiteCode" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.websiteName(websiteName)}}" stepKey="clickWebsiteFirstRowInGrid"/> + <waitForPageLoad stepKey="waitTillWebsiteFormPageIsOpened"/> + <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabWebsiteIdFromCurrentUrl"/> + <seeInCurrentUrl url="/system_store/editWebsite/website_id/{$grabWebsiteIdFromCurrentUrl}" stepKey="seeWebsiteId"/> + <seeInField selector="{{AdminNewWebsiteSection.name}}" userInput="{{websiteName}}" stepKey="seeAssertWebsiteName"/> + <seeInField selector="{{AdminNewWebsiteSection.code}}" userInput="{{websiteCode}}" stepKey="seeAssertWebsiteCode"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml index 58e1781d69eab..cf2cabdcc2399 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -26,4 +26,35 @@ <waitForPageLoad stepKey="waitForSuccessMessage"/> <see userInput="You deleted the store view." stepKey="seeDeleteMessage"/> </actionGroup> -</actionGroups> + <actionGroup name="DeleteCustomStoreViewBackupEnabledYesActionGroup"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickEditExistingStoreViewRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewButtonOnEditStorePage"/> + <selectOption userInput="Yes" selector="{{AdminStoreBackupOptionsSection.createBackupSelect}}" stepKey="setCreateDbBackupToYes"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewButtonOnDeleteStorePage"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreViewDelete"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="The database was backed up." stepKey="seeAssertDatabaseBackedUpMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="You deleted the store view." stepKey="seeAssertSuccessDeleteStoreViewMessage"/> + </actionGroup> + <actionGroup name="AssertStoreViewNotInGrid"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertStoreViewNotInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml index 58fd0a3f0bc2b..1721e3185402e 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml @@ -19,6 +19,7 @@ <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> <waitForPageLoad stepKey="waitForStoreToLoad"/> <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditWebsitePage"/> + <waitForPageLoad stepKey="waitForDeleteStoreGroupSectionLoad" time="30"/> <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteWebsiteButton"/> <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml new file mode 100644 index 0000000000000..f11394c643ad7 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml @@ -0,0 +1,21 @@ +<?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="StoreFrontProductValidationActionGroup"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url(product.urlKey)}}" stepKey="seeProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{product.name}}" stepKey="seeProductInStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{product.sku}}" stepKey="seeCorrectSku"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{product.price}}" stepKey="seeCorrectPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml index f636336524f01..ae605256a2819 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -23,4 +23,8 @@ <data key="name" unique="suffix">Custom Website</data> <data key="code" unique="suffix">custom_website</data> </entity> -</entities> + <entity name="updateCustomWebsite" extends="customWebsite"> + <data key="name" unique="suffix">website_upd</data> + <data key="code" unique="suffix">code_upd</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml index 592af42f2de30..d7006fd01b2ff 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -23,5 +23,6 @@ <element name="firstRow" type="textarea" selector="(//*[@id='storeGrid_table']/tbody/tr)[1]"/> <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div"/> <element name="emptyText" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> + <element name="websiteName" type="text" selector="//td[@class='a-left col-website_title ']/a[contains(.,'{{websiteName}}')]" parameterized="true"/> </section> -</sections> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml new file mode 100644 index 0000000000000..1608d0b7b5a25 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateWebsiteTest"> + <annotations> + <stories value="Create Website"/> + <title value="Create Website and Verify Store Form"/> + <description value="Test log in to Stores and Create Website Test"/> + <testCaseId value="MC-14302"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create website and AssertWebsiteSuccessSaveMessage--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + + <!--Search created website in grid and verify AssertWebsiteInGrid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeWebsiteInGrid"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Verify website name and websitecode on website form (AssertWebsiteForm and AssertWebsiteOnStoreForm)--> + <actionGroup ref="AssertWebsiteForm" stepKey="seeWebsiteForm"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml new file mode 100644 index 0000000000000..fc1dcb5ee1a24 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteStoreViewTest"> + <annotations> + <stories value="Delete Store View"/> + <title value="Delete Store View and Save Backup"/> + <description value="Test log in to Stores and Delete Store View"/> + <testCaseId value="MC-14303"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store view--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete custom store view and verify AssertStoreSuccessDeleteMessage And BackupMessage--> + <actionGroup ref="DeleteCustomStoreViewBackupEnabledYesActionGroup" stepKey="deleteCustomStoreView"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + + <!--Verify deleted store view not present in grid and verify AssertStoreNotInGrid Message--> + <actionGroup ref="AssertStoreViewNotInGrid" stepKey="verifyDeletedStoreViewNotInGrid"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + + <!--Go to backup index page and verify AssertBackupInGrid--> + <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupIndexPage"/> + <waitForPageLoad stepKey="waitForBackupIndexPageLoad"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{WebSetupWizardBackup.name}}" stepKey="seeBackupInGrid"/> + <!--Delete database backup--> + <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> + <argument name="backup" value="WebSetupWizardBackup"/> + </actionGroup> + + <!--Go to storefront and verify AssertStoreNotOnFrontend--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <dontSee selector="{{StorefrontHeaderSection.storeViewList(storeViewData.name)}}" stepKey="dontSeeAssertStoreViewNameOnStorefront"/> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml new file mode 100644 index 0000000000000..6b666126569ae --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateWebsiteTest"> + <annotations> + <stories value="Update Website"/> + <title value="Update Website and Verify Store Form"/> + <description value="Test log in to Stores and Update Website Test"/> + <testCaseId value="MC-14301"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created custom website in grid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeWebsiteInGrid"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.websiteName(customWebsite.name)}}" stepKey="clickWebsiteFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForWebsiteFormPageToOpen"/> + <!--Update website name and website code as per data created and verify AssertWebsiteSuccessSaveMessage--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{updateCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{updateCustomWebsite.code}}"/> + </actionGroup> + + <!--Search updated custom website(from above step) in grid and verify AssertWebsiteInGrid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeUpdatedWebsiteInGrid"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertWebsiteForm and AssertWebsiteOnStoreForm)--> + <actionGroup ref="AssertWebsiteForm" stepKey="seeUpdatedWebsiteForm"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{updateCustomWebsite.code}}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index ebaa32b95f48b..da408f105ccb6 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-message-queue": "*", "magento/module-catalog": "*", "magento/module-config": "*", "magento/module-directory": "*", diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index b37628e54aa30..511fe30f79dcd 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -22,6 +22,8 @@ use Magento\Theme\Model\Design\Config\FileUploader\FileProcessor; /** + * File Backend + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class File extends BackendFile @@ -88,36 +90,29 @@ public function beforeSave() { $values = $this->getValue(); $value = reset($values) ?: []; - if (!isset($value['file'])) { + + // Need to check name when it is uploaded in the media gallary + $file = $value['file'] ?? $value['name'] ?? null; + if (!isset($file)) { throw new LocalizedException( __('%1 does not contain field \'file\'', $this->getData('field_config/field')) ); } if (isset($value['exists'])) { - $this->setValue($value['file']); + $this->setValue($file); return $this; } - $filename = basename($value['file']); - $result = $this->_mediaDirectory->copyFile( - $this->getTmpMediaPath($filename), - $this->_getUploadDir() . '/' . $filename - ); - if ($result) { - $this->_mediaDirectory->delete($this->getTmpMediaPath($filename)); - if ($this->_addWhetherScopeInfo()) { - $filename = $this->_prependScopeInfo($filename); - } - $this->setValue($filename); - } else { - $this->unsValue(); - } + $this->updateMediaDirectory(basename($file), $value['url']); return $this; } /** - * @return array + * After Load + * + * @return File + * @throws LocalizedException */ public function afterLoad() { @@ -166,6 +161,8 @@ protected function getUploadDirPath($uploadDir) } /** + * Get Value + * * @return array */ public function getValue() @@ -231,4 +228,49 @@ private function getMime() } return $this->mime; } + + /** + * Get Relative Media Path + * + * @param string $path + * @return string + */ + private function getRelativeMediaPath(string $path): string + { + return str_replace('/pub/media/', '', $path); + } + + /** + * Move file to the correct media directory + * + * @param string $filename + * @param string $url + * @throws LocalizedException + */ + private function updateMediaDirectory(string $filename, string $url) + { + $relativeMediaPath = $this->getRelativeMediaPath($url); + $tmpMediaPath = $this->getTmpMediaPath($filename); + $mediaPath = $this->_mediaDirectory->isFile($relativeMediaPath) ? $relativeMediaPath : $tmpMediaPath; + $destinationMediaPath = $this->_getUploadDir() . '/' . $filename; + + $result = $mediaPath === $destinationMediaPath; + if (!$result) { + $result = $this->_mediaDirectory->copyFile( + $mediaPath, + $destinationMediaPath + ); + } + if ($result) { + if ($mediaPath === $tmpMediaPath) { + $this->_mediaDirectory->delete($mediaPath); + } + if ($this->_addWhetherScopeInfo()) { + $filename = $this->_prependScopeInfo($filename); + } + $this->setValue($filename); + } else { + $this->unsValue(); + } + } } diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml new file mode 100644 index 0000000000000..6b98686574321 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.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="NavigateToFaviconMediaFolderActionGroup"> + <arguments> + <argument name="StoreFolder" type="string"/> + </arguments> + <conditionalClick selector="{{MediaGallerySection.StorageRootArrow}}" dependentSelector="{{MediaGallerySection.checkIfArrowExpand}}" stepKey="clickArrowIfClosed" visible="true"/> + <waitForElement selector="{{AdminDesignConfigSection.faviconArrow}}" stepKey="waitForFaviconFolder"/> + <conditionalClick selector="{{AdminDesignConfigSection.faviconArrow}}" dependentSelector="{{AdminDesignConfigSection.checkIfFaviconArrowExpand}}" stepKey="clickFaviconArrowIfClosed" visible="true"/> + <waitForElement selector="{{AdminDesignConfigSection.storesArrow}}" stepKey="waitForStoresFolder"/> + <conditionalClick selector="{{AdminDesignConfigSection.storesArrow}}" dependentSelector="{{AdminDesignConfigSection.checkIfStoresArrowExpand}}" stepKey="clickStoresArrowIfClosed" visible="true"/> + <waitForElement selector="{{StoreFolder}}" stepKey="waitForStoreFolder"/> + <click selector="{{StoreFolder}}" stepKey="clickOnCreatedFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml index e90548a7c94e9..c2652f33f7606 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml @@ -14,10 +14,22 @@ <element name="watermarkSection" type="text" selector="[data-index='watermark'] .admin__fieldset-wrapper-content"/> <element name="imageUploadInputByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//input" parameterized="true"/> <element name="imageUploadPreviewByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader-preview')]//img" parameterized="true"/> + <element name="addSelectedFromMediaGallery" type="input" selector="//button[contains(@title,'Add Selected')]"/> + <element name="htmlHeaderSection" type="text" selector="[data-index='head']"/> + <element name="selectFromGalleryByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//label[contains(text(), 'Select from Gallery')]" parameterized="true"/> + <element name="imageUploadFromMediaGallery" type="input" selector="//input[contains(@class,'fileupload')]" /> + <element name="saveConfiguration" type="input" selector="//button[contains(@title, 'Save Configuration')]" /> + <element name="successNotification" type="text" selector="//div[contains(@data-ui-id, 'messages-message-success')]" /> + <element name="useDefaultByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//span[contains(text(), 'Use Default Value')]" parameterized="true" /> <element name="logoSectionHeader" type="text" selector="[data-index='email']"/> <element name="logoSection" type="text" selector="[data-index='email'] .admin__fieldset-wrapper-content"/> <element name="logoUpload" type ="input" selector="[name='email_logo']" /> <element name="logoWrapperOpen" type ="text" selector="[data-index='email'] [data-state-collapsible ='closed']"/> <element name="logoPreview" type ="text" selector="[alt ='magento-logo.png']"/> + <element name="faviconArrow" type="button" selector="#ZmF2aWNvbg-- > .jstree-icon" /> + <element name="checkIfFaviconArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbg--' and contains(@class,'jstree-closed')]" /> + <element name="storesArrow" type="button" selector="#ZmF2aWNvbi9zdG9yZXM- > .jstree-icon" /> + <element name="checkIfStoresArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbi9zdG9yZXM-' and contains(@class,'jstree-closed')]" /> + <element name="storeLink" type="button" selector="#ZmF2aWNvbi9zdG9yZXMvMQ-- > a"/> </section> </sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml new file mode 100644 index 0000000000000..f46328ac151b1 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml @@ -0,0 +1,78 @@ +<?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="AdminDesignConfigMediaGalleryImageUploadTest"> + <annotations> + <features value="Content"/> + <stories value="Content"/> + <title value="MC-5784: Image fields using imageUploader UIComponent cannot use gallery image"/> + <description value="Admin should be able to use Image Uploader to add Gallery Images"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13832"/> + <group value="Content"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Edit Store View--> + <comment userInput="Edit Store View" stepKey="editStoreViewComment"/> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage" /> + <waitForPageLoad stepKey="waitForPageload1"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView"/> + <waitForPageLoad stepKey="waitForPageload2"/> + <scrollTo selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="scrollToHtmlHeadSection"/> + <click selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="openHtmlHeadSection"/> + <!--Upload Image--> + <comment userInput="Upload Image" stepKey="uploadImageComment"/> + <click selector="{{AdminDesignConfigSection.selectFromGalleryByFieldsetName('Head')}}" stepKey="openMediaGallery"/> + <actionGroup ref="VerifyMediaGalleryStorageActions" stepKey="verifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="attachImage" stepKey="selectImageFromMediaStorage"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="saveImage" stepKey="insertImage"/> + <click selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="saveConfiguration"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.successNotification}}" stepKey="waitForSuccessNotification"/> + <waitForPageLoad stepKey="waitForPageloadSuccess"/> + <!--Edit Store View--> + <comment userInput="Edit Store View" stepKey="editStoreViewComment2"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView2"/> + <waitForPageLoad stepKey="waitForPageload3"/> + <scrollTo selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="scrollToHtmlHeadSection2"/> + <click selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="openHtmlHeadSection2"/> + <!--Save Default Configuration--> + <comment userInput="Save Default Configuration" stepKey="saveDefaultConfigurationComment"/> + <click selector="{{AdminDesignConfigSection.useDefaultByFieldsetName('Head')}}" stepKey="clickUseDefault"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="waitForWrapperToClose2"/> + <click selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="saveConfiguration2"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.successNotification}}" stepKey="waitForSuccessNotification2"/> + <waitForPageLoad stepKey="waitForPageloadSuccess2"/> + <!--Delete Image: will be in both root and favicon--> + <comment userInput="Delete Image" stepKey="deleteImageComment"/> + <actionGroup ref="navigateToMediaGallery" stepKey="navigateToMediaGallery"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder2"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="DeleteImageFromStorageActionGroup" stepKey="deleteImageFromStorage"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="NavigateToFaviconMediaFolderActionGroup" stepKey="navigateToFolder3"> + <argument name="StoreFolder" value="{{AdminDesignConfigSection.storeLink}}"/> + </actionGroup> + <actionGroup ref="DeleteImageFromStorageActionGroup" stepKey="deleteImageFromStorage2"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js index 1b6dd9f1c57ec..0eaacdc32567b 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js @@ -20,6 +20,26 @@ define([ } }, + /** + * Initializes observable properties of instance + * + * @returns {Abstract} Chainable. + */ + initObservable: function () { + this._super(); + + /** + * equalityComparer function + * + * @returns boolean. + */ + this.value.equalityComparer = function (oldValue, newValue) { + return !oldValue && !newValue || oldValue === newValue; + }; + + return this; + }, + /** * @param {String} value */ diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js b/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js index 87b5b2f5fe8fe..038907c21224d 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js @@ -79,7 +79,7 @@ define([ /** * Extracts and parses data stored in localStorage by the - * key specified in 'root' varaible. + * key specified in 'root' variable. * * @returns {Object} */ diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index d765f842a0895..97b47f77beeab 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -920,12 +920,12 @@ define([ ], 'validate-per-page-value-list': [ function (value) { - var isValid = utils.isEmpty(value), + var isValid = true, values = value.split(','), i; - if (isValid) { - return true; + if (utils.isEmpty(value)) { + return isValid; } for (i = 0; i < values.length; i++) { diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index d8ceb16d71fdc..2ac1bdd712114 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -105,42 +105,18 @@ protected function doFindOneByData(array $data) $result = null; $requestPath = $data[UrlRewrite::REQUEST_PATH]; - - $data[UrlRewrite::REQUEST_PATH] = [ + $decodedRequestPath = urldecode($requestPath); + $data[UrlRewrite::REQUEST_PATH] = array_unique([ rtrim($requestPath, '/'), rtrim($requestPath, '/') . '/', - ]; + rtrim($decodedRequestPath, '/'), + rtrim($decodedRequestPath, '/') . '/', + ]); $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); - - if (count($resultsFromDb) === 1) { - $resultFromDb = current($resultsFromDb); - $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; - - // If request path matches the DB value or it's redirect - we can return result from DB - $canReturnResultFromDb = ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath - || in_array((int)$resultFromDb[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true)); - - // Otherwise return 301 redirect to request path from DB results - $result = $canReturnResultFromDb ? $resultFromDb : [ - UrlRewrite::ENTITY_TYPE => 'custom', - UrlRewrite::ENTITY_ID => '0', - UrlRewrite::REQUEST_PATH => $requestPath, - UrlRewrite::TARGET_PATH => $resultFromDb[UrlRewrite::REQUEST_PATH], - UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, - UrlRewrite::STORE_ID => $resultFromDb[UrlRewrite::STORE_ID], - UrlRewrite::DESCRIPTION => null, - UrlRewrite::IS_AUTOGENERATED => '0', - UrlRewrite::METADATA => null, - ]; - } else { - // If we have 2 results - return the row that matches request path - foreach ($resultsFromDb as $resultFromDb) { - if ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath) { - $result = $resultFromDb; - break; - } - } + if ($resultsFromDb) { + $urlRewrite = $this->extractMostRelevantUrlRewrite($requestPath, $resultsFromDb); + $result = $this->prepareUrlRewrite($requestPath, $urlRewrite); } return $result; @@ -149,6 +125,75 @@ protected function doFindOneByData(array $data) return $this->connection->fetchRow($this->prepareSelect($data)); } + /** + * Extract most relevant url rewrite from url rewrites list + * + * @param string $requestPath + * @param array $urlRewrites + * @return array|null + */ + private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRewrites): ?array + { + $prioritizedUrlRewrites = []; + foreach ($urlRewrites as $urlRewrite) { + switch (true) { + case $urlRewrite[UrlRewrite::REQUEST_PATH] === $requestPath: + $priority = 1; + break; + case $urlRewrite[UrlRewrite::REQUEST_PATH] === urldecode($requestPath): + $priority = 2; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim($requestPath, '/'): + $priority = 3; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim(urldecode($requestPath), '/'): + $priority = 4; + break; + default: + $priority = 5; + break; + } + $prioritizedUrlRewrites[$priority] = $urlRewrite; + } + ksort($prioritizedUrlRewrites); + + return array_shift($prioritizedUrlRewrites); + } + + /** + * Prepare url rewrite + * + * If request path matches the DB value or it's redirect - we can return result from DB + * Otherwise return 301 redirect to request path from DB results + * + * @param string $requestPath + * @param array $urlRewrite + * @return array + */ + private function prepareUrlRewrite(string $requestPath, array $urlRewrite): array + { + $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; + $canReturnResultFromDb = ( + in_array($urlRewrite[UrlRewrite::REQUEST_PATH], [$requestPath, urldecode($requestPath)], true) + || in_array((int) $urlRewrite[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true) + ); + if (!$canReturnResultFromDb) { + $urlRewrite = [ + UrlRewrite::ENTITY_TYPE => 'custom', + UrlRewrite::ENTITY_ID => '0', + UrlRewrite::REQUEST_PATH => $requestPath, + UrlRewrite::TARGET_PATH => $urlRewrite[UrlRewrite::REQUEST_PATH], + UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, + UrlRewrite::STORE_ID => $urlRewrite[UrlRewrite::STORE_ID], + UrlRewrite::DESCRIPTION => null, + UrlRewrite::IS_AUTOGENERATED => '0', + UrlRewrite::METADATA => null, + ]; + } + + return $urlRewrite; + } + /** * Delete old URLs from DB. * diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml index e9651a3f26e94..50b83641e19a9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml @@ -83,10 +83,10 @@ <argument name="description" type="string"/> </arguments> <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> - <click selector="{{AdminUrlRewriteEditSection.storeValue('storeValue')}}" stepKey="clickOnStoreValue"/> + <click selector="{{AdminUrlRewriteEditSection.storeValue(storeValue)}}" stepKey="clickOnStoreValue"/> <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> <click selector="{{AdminUrlRewriteEditSection.redirectType}}" stepKey="selectRedirectType"/> - <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue('redirectTypeValue')}}" stepKey="selectRedirectTypeValue"/> + <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue(redirectTypeValue)}}" stepKey="selectRedirectTypeValue"/> <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml index 77cf80ca95ac5..3692e82072afc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml @@ -24,6 +24,7 @@ <data key="redirect_type_label">Temporary (302)</data> <data key="store_id">1</data> <data key="store">Default Store View</data> + <data key="description">Update Url Rewrite</data> </entity> <entity name="customPermanentUrlRewrite" type="urlRewrite"> <data key="request_path" unique="prefix">wishlist</data> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..ea370d8419583 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,49 @@ +<?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="AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="URL Rewrite"/> + <title value="Update Product URL Rewrites"/> + <description value="Login as Admin and update product UrlRewrite and add Temporary redirect type "/> + <testCaseId value="MC-5351"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created product in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="{{updateUrlRewrite.store}}"/> + <argument name="requestPath" value="{{updateUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="{{updateUrlRewrite.redirect_type_label}}"/> + <argument name="description" value="{{updateUrlRewrite.description}}"/> + </actionGroup> + + <!-- Assert product Url Rewrite in StoreFront --> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="assertProductUrlRewriteInStoreFront"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="{{updateUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php new file mode 100644 index 0000000000000..fb0113eb6ae75 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\Model\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Wishlist\Model\Product\AttributeValueProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * AttributeValueProviderTest + */ +class AttributeValueProviderTest extends TestCase +{ + /** + * @var AttributeValueProvider|PHPUnit_Framework_MockObject_MockObject + */ + private $attributeValueProvider; + + /** + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactoryMock; + + /** + * @var Product|PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var AdapterInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->productCollectionFactoryMock = $this->createPartialMock( + CollectionFactory::class, + ['create'] + ); + $this->attributeValueProvider = new AttributeValueProvider( + $this->productCollectionFactoryMock + ); + } + + /** + * Get attribute text when the flat table is disabled + * + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + * @dataProvider attributeDataProvider + */ + public function testGetAttributeTextWhenFlatIsDisabled(int $productId, string $attributeCode, string $attributeText) + { + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getFirstItem' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(false); + $productCollection->expects($this->any()) + ->method('getFirstItem') + ->willReturn($this->productMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * Get attribute text when the flat table is enabled + * + * @dataProvider attributeDataProvider + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + */ + public function testGetAttributeTextWhenFlatIsEnabled(int $productId, string $attributeCode, string $attributeText) + { + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->any()) + ->method('fetchRow') + ->willReturn([ + $attributeCode => $attributeText + ]); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getConnection' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(true); + $productCollection->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * @return array + */ + public function attributeDataProvider(): array + { + return [ + [1, 'attribute_code', 'Attribute Text'] + ]; + } +} diff --git a/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php new file mode 100644 index 0000000000000..5e4c6b39f3c36 --- /dev/null +++ b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\ViewModel; + +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * ViewModel for Wishlist Cart Block + */ +class AllowedQuantity implements ArgumentInterface +{ + /** + * @var StockRegistry + */ + private $stockRegistry; + + /** + * @var ItemInterface + */ + private $item; + + /** + * @param StockRegistry $stockRegistry + */ + public function __construct(StockRegistry $stockRegistry) + { + $this->stockRegistry = $stockRegistry; + } + + /** + * Set product configuration item + * + * @param ItemInterface $item + * @return self + */ + public function setItem(ItemInterface $item): self + { + $this->item = $item; + return $this; + } + + /** + * Get product configuration item + * + * @return ItemInterface + */ + public function getItem(): ItemInterface + { + return $this->item; + } + + /** + * Get min and max qty for wishlist form. + * + * @return array + */ + public function getMinMaxQty(): array + { + $product = $this->getItem()->getProduct(); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $params = []; + + $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); + } else { + $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; + } + + return $params; + } +} diff --git a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml index b20bc6a4e00ba..d4c3cc7fadd84 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml @@ -41,6 +41,7 @@ </block> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.cart" template="Magento_Wishlist::item/column/cart.phtml" cacheable="false"> <arguments> + <argument name="allowedQuantityViewModel" xsi:type="object">Magento\Wishlist\ViewModel\AllowedQuantity</argument> <argument name="title" translate="true" xsi:type="string">Add to Cart</argument> </arguments> </block> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 9ea0d1a823235..6cb32d70ee1d8 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml @@ -11,6 +11,9 @@ /** @var \Magento\Wishlist\Model\Item $item */ $item = $block->getItem(); $product = $item->getProduct(); +/** @var \Magento\Wishlist\ViewModel\AllowedQuantity $viewModel */ +$viewModel = $block->getData('allowedQuantityViewModel'); +$allowedQty = $viewModel->setItem($item)->getMinMaxQty(); ?> <?php foreach ($block->getChildNames() as $childName): ?> <?= /* @noEscape */ $block->getLayout()->renderElement($childName, false) ?> @@ -21,7 +24,7 @@ $product = $item->getProduct(); <div class="field qty"> <label class="label" for="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> - <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true}" + <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>"> </div> </div> diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index e3a788af2ea7e..792928ab61aaf 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -13,6 +13,7 @@ use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; use Magento\Wishlist\Model\Wishlist; use Magento\Wishlist\Model\WishlistFactory; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; /** * Fetches the Wishlist data according to the GraphQL schema @@ -51,6 +52,10 @@ public function resolve( ) { $customerId = $context->getUserId(); + /* Guest checking */ + if (!$customerId && 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } /** @var Wishlist $wishlist */ $wishlist = $this->wishlistFactory->create(); $this->wishlistResource->load($wishlist, $customerId, 'customer_id'); diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less index 08434727ccc9c..8499ecaa48c12 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less @@ -45,6 +45,10 @@ .page-actions { @_page-action__indent: 1.3rem; + &.floating-header { + &:extend(.page-actions-buttons all); + } + .page-main-actions & { &._fixed { left: @page-wrapper__indent-left; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index d8fd1db5ced47..5698afdaac7ae 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -545,7 +545,6 @@ & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); cursor: pointer; - background: @color-white; left: 0; position: absolute; top: 0; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less index 4479c070a4e17..8dec680b58726 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less @@ -55,31 +55,3 @@ } } } - -// -// Desktop -// _____________________________________________ - -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { - // ToDo UI: remove with global blank theme .field.required update - .opc-wrapper { - .fieldset { - > .field { - &.required, - &._required { - position: relative; - - > label { - padding-right: 25px; - - &:after { - margin-left: @indent__s; - position: absolute; - top: 9px; - } - } - } - } - } - } -} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less index 5f8134193c67f..35445b0989e86 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -209,6 +209,13 @@ .fieldset { > .field { margin: 0 0 @indent__base; + + &.choice { + &:before { + padding: 0; + width: 0; + } + } &.type { .control { diff --git a/app/etc/di.xml b/app/etc/di.xml index 19543375aad58..d0b45ea16c855 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -38,7 +38,7 @@ <preference for="Magento\Framework\Locale\ListsInterface" type="Magento\Framework\Locale\TranslatedLists" /> <preference for="Magento\Framework\Locale\AvailableLocalesInterface" type="Magento\Framework\Locale\Deployed\Codes" /> <preference for="Magento\Framework\Locale\OptionInterface" type="Magento\Framework\Locale\Deployed\Options" /> - <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Backend\Database" /> + <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Proxy" /> <preference for="Magento\Framework\Api\AttributeTypeResolverInterface" type="Magento\Framework\Reflection\AttributeTypeResolver" /> <preference for="Magento\Framework\Api\Search\SearchResultInterface" type="Magento\Framework\Api\Search\SearchResult" /> <preference for="Magento\Framework\Api\Search\SearchCriteriaInterface" type="Magento\Framework\Api\Search\SearchCriteria"/> @@ -1757,4 +1757,11 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockTimeout" xsi:type="number">10000</argument> + <argument name="delayTimeout" xsi:type="number">20</argument> + </arguments> + </type> </config> diff --git a/dev/tests/acceptance/tests/_data/catalog_import_products.csv b/dev/tests/acceptance/tests/_data/catalog_import_products.csv new file mode 100644 index 0000000000000..7732f15d4ce3a --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_import_products.csv @@ -0,0 +1,4 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +SimpleProductForTest1,,Default,simple,"Default","base,second_website",SimpleProductAfterImport1,,,1.0000,1,"Taxable Goods","Catalog, Search",250.0000,,,,simple-product-for-test-1,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest2,,Default,simple,"Default",base,SimpleProductAfterImport2,,,1.0000,1,"Taxable Goods","Catalog, Search",300.0000,,,,simple-product-for-test-2,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest3,,Default,simple,"Default","base,second_website",SimpleProductAfterImport3,,,1.0000,1,"Taxable Goods","Catalog, Search",350.0000,,,,simple-product-for-test-3,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php index 1ff0b53dda0bb..ad5d71cb08605 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php @@ -21,8 +21,8 @@ public function testGetCurrency() currency { base_currency_code base_currency_symbol - default_display_currecy_code - default_display_currecy_symbol + default_display_currency_code + default_display_currency_symbol available_currency_codes exchange_rates { currency_to @@ -36,8 +36,8 @@ public function testGetCurrency() $this->assertArrayHasKey('currency', $result); $this->assertArrayHasKey('base_currency_code', $result['currency']); $this->assertArrayHasKey('base_currency_symbol', $result['currency']); - $this->assertArrayHasKey('default_display_currecy_code', $result['currency']); - $this->assertArrayHasKey('default_display_currecy_symbol', $result['currency']); + $this->assertArrayHasKey('default_display_currency_code', $result['currency']); + $this->assertArrayHasKey('default_display_currency_symbol', $result['currency']); $this->assertArrayHasKey('available_currency_codes', $result['currency']); $this->assertArrayHasKey('exchange_rates', $result['currency']); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php index 1e92a2e497bed..d9ab8db62a195 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php @@ -59,6 +59,22 @@ public function testAddSimpleProductToCart() self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Please enter a number greater than 0 in this field. + */ + public function testAddSimpleProductToCartWithNegativeQty() + { + $sku = 'simple'; + $qty = -2; + $maskedQuoteId = $this->getMaskedQuoteId(); + + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($query); + } + /** * @return string */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php index a7287ada093b8..ba640bc3402ba 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php @@ -7,10 +7,8 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -25,19 +23,9 @@ class GetAvailablePaymentMethodsTest extends GraphQlAbstract private $customerTokenService; /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; + private $getMaskedQuoteIdByReservedOrderId; /** * @inheritdoc @@ -45,55 +33,60 @@ class GetAvailablePaymentMethodsTest extends GraphQlAbstract protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testGetCartWithPaymentMethods() + public function testGetAvailablePaymentMethods() { - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId('test_order_item_with_items'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); - self::assertGreaterThan( - 0, - count($response['cart']['available_payment_methods']), - 'There are no available payment methods for customer cart!' - ); } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testGetPaymentMethodsFromGuestCart() + public function testGetAvailablePaymentMethodsFromGuestCart() { - $guestQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId( - 'test_order_with_virtual_product_without_address' - ); - $query = $this->getQuery($guestQuoteMaskedId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $this->expectExceptionMessage( - "The current user cannot perform operations on cart \"$guestQuoteMaskedId\"" + "The current user cannot perform operations on cart \"$maskedQuoteId\"" ); $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testGetPaymentMethodsFromAnotherCustomerCart() + public function testGetAvailablePaymentMethodsFromAnotherCustomerCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId('test_order_item_with_items'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId); $this->expectExceptionMessage( @@ -103,24 +96,31 @@ public function testGetPaymentMethodsFromAnotherCustomerCart() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php - * @magentoApiDataFixture Magento/Payment/_files/disable_all_active_payment_methods.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php */ - public function testGetPaymentMethodsIfPaymentsAreNotSet() + public function testGetAvailablePaymentMethodsIfPaymentsAreNotPresent() { - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId('test_order_item_with_items'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); - self::assertEquals(0, count($response['cart']['available_payment_methods'])); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEmpty($response['cart']['available_payment_methods']); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php + * * @expectedException \Exception * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" */ - public function testGetPaymentMethodsOfNonExistentCart() + public function testGetAvailablePaymentMethodsOfNonExistentCart() { $maskedQuoteId = 'non_existent_masked_id'; $query = $this->getQuery($maskedQuoteId); @@ -132,9 +132,8 @@ public function testGetPaymentMethodsOfNonExistentCart() * @param string $maskedQuoteId * @return string */ - private function getQuery( - string $maskedQuoteId - ): string { + private function getQuery(string $maskedQuoteId): string + { return <<<QUERY { cart(cart_id: "$maskedQuoteId") { @@ -158,16 +157,4 @@ private function getHeaderMap(string $username = 'customer@example.com', string $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; return $headerMap; } - - /** - * @param string $reservedOrderId - * @return string - */ - private function getMaskedQuoteIdByReservedOrderId(string $reservedOrderId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php index 4b1509eef354e..eb62b8c92f310 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -7,10 +7,8 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -20,19 +18,9 @@ class GetCartTest extends GraphQlAbstract { /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; + private $getMaskedQuoteIdByReservedOrderId; /** * @var CustomerTokenServiceInterface @@ -42,19 +30,22 @@ class GetCartTest extends GraphQlAbstract protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php */ public function testGetCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); @@ -63,22 +54,23 @@ public function testGetCart() self::assertCount(2, $response['cart']['items']); self::assertNotEmpty($response['cart']['items'][0]['id']); - self::assertEquals($response['cart']['items'][0]['qty'], 2); - self::assertEquals($response['cart']['items'][0]['product']['sku'], 'simple'); + self::assertEquals(2, $response['cart']['items'][0]['qty']); + self::assertEquals('simple', $response['cart']['items'][0]['product']['sku']); self::assertNotEmpty($response['cart']['items'][1]['id']); - self::assertEquals($response['cart']['items'][1]['qty'], 1); - self::assertEquals($response['cart']['items'][1]['product']['sku'], 'simple_one'); + self::assertEquals(2, $response['cart']['items'][1]['qty']); + self::assertEquals('virtual-product', $response['cart']['items'][1]['product']['sku']); } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php */ public function testGetGuestCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $this->expectExceptionMessage( "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" @@ -87,13 +79,14 @@ public function testGetGuestCart() } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php */ public function testGetAnotherCustomerCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $this->expectExceptionMessage( "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" @@ -103,33 +96,30 @@ public function testGetAnotherCustomerCart() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php + * * @expectedException \Exception * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" */ public function testGetNonExistentCart() { $maskedQuoteId = 'non_existent_masked_id'; - $query = $this->getCartQuery($maskedQuoteId); + $query = $this->getQuery($maskedQuoteId); $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + * * @expectedException \Exception * @expectedExceptionMessage Current user does not have an active cart. */ public function testGetInactiveCart() { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); - $quote->setCustomerId(1); - $quote->setIsActive(false); - $this->quoteResource->save($quote); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); - - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } @@ -138,12 +128,11 @@ public function testGetInactiveCart() * @param string $maskedQuoteId * @return string */ - private function getCartQuery( - string $maskedQuoteId - ) : string { + private function getQuery(string $maskedQuoteId): string + { return <<<QUERY { - cart(cart_id: "$maskedQuoteId") { + cart(cart_id: "{$maskedQuoteId}") { items { id qty @@ -156,18 +145,6 @@ private function getCartQuery( QUERY; } - /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - /** * @param string $username * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php index 67a086311d71a..129375debe068 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; @@ -19,6 +20,11 @@ */ class SetBillingAddressOnCartTest extends GraphQlAbstract { + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + /** * @var QuoteResource */ @@ -42,6 +48,7 @@ class SetBillingAddressOnCartTest extends GraphQlAbstract protected function setUp() { $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->quoteResource = $objectManager->get(QuoteResource::class); $this->quoteFactory = $objectManager->get(QuoteFactory::class); $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); @@ -49,12 +56,14 @@ protected function setUp() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetNewBillingAddress() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -106,12 +115,14 @@ public function testSetNewBillingAddress() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetNewBillingAddressWithUseForShippingParameter() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -181,13 +192,15 @@ public function testSetNewBillingAddressWithUseForShippingParameter() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetBillingAddressFromAddressBook() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -227,14 +240,17 @@ public function testSetBillingAddressFromAddressBook() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception * @expectedExceptionMessage Could not find a address with ID "100" */ public function testSetNotExistedBillingAddressFromAddressBook() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -258,13 +274,15 @@ public function testSetNotExistedBillingAddressFromAddressBook() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetNewBillingAddressAndFromAddressBookAtSameTime() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -304,13 +322,16 @@ public function testSetNewBillingAddressAndFromAddressBookAtSameTime() } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Customer/_files/customer_address.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetBillingAddressToGuestCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -338,6 +359,7 @@ public function testSetBillingAddressToGuestCart() } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php * @magentoApiDataFixture Magento/Customer/_files/customer_address.php * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php @@ -372,9 +394,11 @@ public function testSetBillingAddressToAnotherCustomerCart() } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php * @magentoApiDataFixture Magento/Customer/_files/customer_address.php * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * * @expectedException \Exception * @expectedExceptionMessage Current customer does not have permission to address with ID "1" */ @@ -423,6 +447,60 @@ public function testSetBillingAddressOnNonExistentCart() $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderSetWithoutRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetBillingAddressWithoutRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + {$input} + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlQuery($query); + } + + /** + * @return array + */ + public function dataProviderSetWithoutRequiredParameters(): array + { + return [ + 'missed_billing_address' => [ + 'cart_id: "cart_id_value"', + 'Field SetBillingAddressOnCartInput.billing_address of required type BillingAddressInput!' + . ' was not provided.', + ], + 'missed_cart_id' => [ + 'billing_address: {}', + 'Required parameter "cart_id" is missing' + ] + ]; + } + /** * Verify the all the whitelisted fields for a New Address Object * @@ -480,28 +558,16 @@ private function getHeaderMap(string $username = 'customer@example.com', string } /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - - /** - * @param string $reversedQuoteId + * @param string $reversedOrderId * @param int $customerId * @return string */ private function assignQuoteToCustomer( - string $reversedQuoteId = 'test_order_with_simple_product_without_address', + string $reversedOrderId = 'test_order_with_simple_product_without_address', int $customerId = 1 ): string { $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $this->quoteResource->load($quote, $reversedOrderId, 'reserved_order_id'); $quote->setCustomerId($customerId); $this->quoteResource->save($quote); return $this->quoteIdToMaskedId->execute((int)$quote->getId()); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php index c7da2144adb9e..450a22dd6e9c7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php @@ -7,11 +7,10 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\OfflinePayments\Model\Cashondelivery; use Magento\OfflinePayments\Model\Checkmo; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -21,24 +20,14 @@ class SetPaymentMethodOnCartTest extends GraphQlAbstract { /** - * @var CustomerTokenServiceInterface - */ - private $customerTokenService; - - /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; + private $getMaskedQuoteIdByReservedOrderId; /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface + * @var CustomerTokenServiceInterface */ - private $quoteIdToMaskedId; + private $customerTokenService; /** * @inheritdoc @@ -46,21 +35,23 @@ class SetPaymentMethodOnCartTest extends GraphQlAbstract protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testSetPaymentWithVirtualProduct() + public function testSetPaymentOnCartWithSimpleProduct() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_virtual_product'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('setPaymentMethodOnCart', $response); @@ -70,14 +61,35 @@ public function testSetPaymentWithVirtualProduct() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentOnCartWithSimpleProductAndWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php */ - public function testSetPaymentWithSimpleProduct() + public function testSetPaymentOnCartWithVirtualProduct() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('setPaymentMethodOnCart', $response); @@ -88,103 +100,158 @@ public function testSetPaymentWithSimpleProduct() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * * @expectedException \Exception - * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + * @expectedExceptionMessage The requested Payment Method is not available. */ - public function testSetPaymentWithSimpleProductWithoutAddress() + public function testSetNonExistentPaymentMethod() { - $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 1); + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * * @expectedException \Exception - * @expectedExceptionMessage The requested Payment Method is not available. + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" */ - public function testSetNonExistingPaymentMethod() + public function testSetPaymentOnNonExistentCart() { - $methodCode = 'noway'; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); - - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $maskedQuoteId = 'non_existent_masked_id'; + $query = <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + items { + id + } + } +} +QUERY; $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetPaymentMethodToGuestCart() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $this->expectExceptionMessage( "The current user cannot perform operations on cart \"$maskedQuoteId\"" ); - $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetPaymentMethodToAnotherCustomerCart() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $this->expectExceptionMessage( "The current user cannot perform operations on cart \"$maskedQuoteId\"" ); - $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @expectedException \Exception - * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @param string $input + * @param string $message + * @throws \Exception + * @dataProvider dataProviderSetPaymentMethodWithoutRequiredParameters */ - public function testPaymentMethodOnNonExistentCart() + public function testSetPaymentMethodWithoutRequiredParameters(string $input, string $message) { - $maskedQuoteId = 'non_existent_masked_id'; $query = <<<QUERY -{ - cart(cart_id: "$maskedQuoteId") { - items { - id +mutation { + setPaymentMethodOnCart( + input: { + {$input} + } + ) { + cart { + items { + qty + } } } } QUERY; + $this->expectExceptionMessage($message); $this->graphQlQuery($query, [], '', $this->getHeaderMap()); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_payment_saved.php + * @return array + */ + public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', + 'Required parameter "cart_id" is missing.' + ], + 'missed_payment_method' => [ + 'cart_id: "test"', + 'Required parameter "code" for "payment_method" is missing.' + ], + 'missed_payment_method_code' => [ + 'cart_id: "test", payment_method: {code: ""}', + 'Required parameter "code" for "payment_method" is missing.' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php */ public function testReSetPayment() { - /** @var \Magento\Quote\Model\Quote $quote */ - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1_with_payment'); - $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $methodCode = Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE; + $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('setPaymentMethodOnCart', $response); self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); } @@ -193,7 +260,7 @@ public function testReSetPayment() * @param string $methodCode * @return string */ - private function prepareMutationQuery( + private function getQuery( string $maskedQuoteId, string $methodCode ) : string { @@ -215,34 +282,6 @@ private function prepareMutationQuery( QUERY; } - /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - - /** - * @param string $reversedQuoteId - * @param int $customerId - * @return string - */ - private function assignQuoteToCustomer( - string $reversedQuoteId, - int $customerId - ): string { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - $quote->setCustomerId($customerId); - $this->quoteResource->save($quote); - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - /** * @param string $username * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index ae184b8930c07..5ff29d20b34d7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; @@ -34,6 +35,11 @@ class SetShippingAddressOnCartTest extends GraphQlAbstract */ private $quoteIdToMaskedId; + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + /** * @var CustomerTokenServiceInterface */ @@ -45,16 +51,19 @@ protected function setUp() $this->quoteResource = $objectManager->get(QuoteResource::class); $this->quoteFactory = $objectManager->get(QuoteFactory::class); $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ - public function testSetNewShippingAddress() + public function testSetNewShippingAddressOnCartWithSimpleProduct() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -108,14 +117,17 @@ public function testSetNewShippingAddress() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * * @expectedException \Exception * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. */ - public function testSetNewShippingAddressOnQuoteWithVirtualProducts() + public function testSetNewShippingAddressOnCartWithVirtualProduct() { - $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_virtual_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -152,13 +164,15 @@ public function testSetNewShippingAddressOnQuoteWithVirtualProducts() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetShippingAddressFromAddressBook() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -196,14 +210,17 @@ public function testSetShippingAddressFromAddressBook() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception * @expectedExceptionMessage Could not find a address with ID "100" */ public function testSetNonExistentShippingAddressFromAddressBook() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -229,13 +246,15 @@ public function testSetNonExistentShippingAddressFromAddressBook() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetNewShippingAddressAndFromAddressBookAtSameTime() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -276,9 +295,11 @@ public function testSetNewShippingAddressAndFromAddressBookAtSameTime() } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php * @magentoApiDataFixture Magento/Customer/_files/customer_address.php * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * * @expectedException \Exception * @expectedExceptionMessage Current customer does not have permission to address with ID "1" */ @@ -311,9 +332,11 @@ public function testSetShippingAddressIfCustomerIsNotOwnerOfAddress() } /** + * _security * @magentoApiDataFixture Magento/Customer/_files/three_customers.php * @magentoApiDataFixture Magento/Customer/_files/customer_address.php * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * * @expectedException \Exception */ public function testSetShippingAddressToAnotherCustomerCart() @@ -343,13 +366,15 @@ public function testSetShippingAddressToAnotherCustomerCart() $this->expectExceptionMessage( "The current user cannot perform operations on cart \"$maskedQuoteId\"" ); - $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @dataProvider dataProviderUpdateWithMissedRequiredParameters * @param string $input * @param string $message @@ -357,7 +382,7 @@ public function testSetShippingAddressToAnotherCustomerCart() */ public function testSetNewShippingAddressWithMissedRequiredParameters(string $input, string $message) { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -386,7 +411,7 @@ public function testSetNewShippingAddressWithMissedRequiredParameters(string $in /** * @return array */ - public function dataProviderUpdateWithMissedRequiredParameters() + public function dataProviderUpdateWithMissedRequiredParameters(): array { return [ 'shipping_addresses' => [ @@ -402,13 +427,16 @@ public function dataProviderUpdateWithMissedRequiredParameters() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception * @expectedExceptionMessage You cannot specify multiple shipping addresses. */ public function testSetMultipleNewShippingAddresses() { - $maskedQuoteId = $this->assignQuoteToCustomer(); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -513,16 +541,16 @@ private function getHeaderMap(string $username = 'customer@example.com', string } /** - * @param string $reversedQuoteId + * @param string $reversedOrderId * @param int $customerId * @return string */ private function assignQuoteToCustomer( - string $reversedQuoteId = 'test_order_with_simple_product_without_address', + string $reversedOrderId = 'test_order_with_simple_product_without_address', int $customerId = 1 ): string { $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $this->quoteResource->load($quote, $reversedOrderId, 'reserved_order_id'); $quote->setCustomerId($customerId); $this->quoteResource->save($quote); return $this->quoteIdToMaskedId->execute((int)$quote->getId()); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php index eae7a0374d210..b5634f51cb366 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -13,7 +13,6 @@ use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\Quote\Model\Quote; /** * Test for setting shipping methods on cart for customer @@ -117,33 +116,6 @@ public function testSetMultipleShippingMethods() $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); } - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_shipping_method.php - */ - public function testReSetShippingMethod() - { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); - $methodCode = 'flatrate'; - $carrierCode = 'flatrate'; - $quote = $this->getQuoteByReversedQuoteId('test_order_1'); - $shippingAddressId = $quote->getShippingAddress()->getId(); - - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode, $carrierCode, $shippingAddressId); - $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); - - self::assertArrayHasKey('setShippingMethodsOnCart', $response); - self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); - self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); - foreach ($response['setShippingMethodsOnCart']['cart']['shipping_addresses'] as $address) { - self::assertArrayHasKey('address_id', $address); - if ($address['address_id'] == $shippingAddressId) { - self::assertArrayHasKey('selected_shipping_method', $address); - self::assertEquals($methodCode, $address['selected_shipping_method']['method_code']); - self::assertEquals($carrierCode, $address['selected_shipping_method']['carrier_code']); - } - } - } - /** * @param string $maskedQuoteId * @param string $shippingMethodCode @@ -163,15 +135,18 @@ private function prepareMutationQuery( setShippingMethodsOnCart(input: { cart_id: "$maskedQuoteId", - shipping_methods: [{ + shipping_addresses: [{ cart_address_id: $shippingAddressId - method_code: "$shippingMethodCode" - carrier_code: "$shippingCarrierCode" + shipping_method: { + method_code: "$shippingMethodCode" + carrier_code: "$shippingCarrierCode" + } }] }) { + cart { + cart_id, shipping_addresses { - address_id selected_shipping_method { carrier_code method_code @@ -187,42 +162,30 @@ private function prepareMutationQuery( } /** - * @param string $reversedQuoteId + * @param string $reversedOrderId * @return string * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + private function getMaskedQuoteIdByReservedOrderId(string $reversedOrderId): string { $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $this->quoteResource->load($quote, $reversedOrderId, 'reserved_order_id'); return $this->quoteIdToMaskedId->execute((int)$quote->getId()); } /** - * @param string $reversedQuoteId - * @return Quote - */ - private function getQuoteByReversedQuoteId(string $reversedQuoteId): Quote - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $quote; - } - - /** - * @param string $reversedQuoteId + * @param string $reversedOrderId * @param int $customerId * @return string * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ private function assignQuoteToCustomer( - string $reversedQuoteId, + string $reversedOrderId, int $customerId ): string { $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $this->quoteResource->load($quote, $reversedOrderId, 'reserved_order_id'); $quote->setCustomerId($customerId); $this->quoteResource->save($quote); return $this->quoteIdToMaskedId->execute((int)$quote->getId()); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php new file mode 100644 index 0000000000000..c5a4e8af02a58 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get masked quote id by reserved order id + */ +class GetMaskedQuoteIdByReservedOrderId +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + $this->quoteIdToMaskedId = $quoteIdToMaskedId; + } + + /** + * Get masked quote id by reserved order id + * + * @param string $reversedOrderId + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function execute(string $reversedOrderId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedOrderId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php index c4dd9af490c99..8271a76d88f12 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php @@ -7,9 +7,7 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -19,19 +17,9 @@ class GetAvailablePaymentMethodsTest extends GraphQlAbstract { /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; + private $getMaskedQuoteIdByReservedOrderId; /** * @inheritdoc @@ -39,43 +27,39 @@ class GetAvailablePaymentMethodsTest extends GraphQlAbstract protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testGetCartWithPaymentMethods() + public function testGetAvailablePaymentMethods() { - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlQuery($query); self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); - - self::assertEquals('free', $response['cart']['available_payment_methods'][1]['code']); - self::assertEquals( - 'No Payment Information Required', - $response['cart']['available_payment_methods'][1]['title'] - ); - self::assertGreaterThan( - 0, - count($response['cart']['available_payment_methods']), - 'There are no available payment methods for guest cart!' - ); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testGetPaymentMethodsFromCustomerCart() + public function testGetAvailablePaymentMethodsFromCustomerCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId('test_order_1'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId); $this->expectExceptionMessage( @@ -85,23 +69,28 @@ public function testGetPaymentMethodsFromCustomerCart() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/Payment/_files/disable_all_active_payment_methods.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php */ - public function testGetPaymentMethodsIfPaymentsAreNotSet() + public function testGetAvailablePaymentMethodsIfPaymentsAreNotPresent() { - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlQuery($query); - self::assertEquals(0, count($response['cart']['available_payment_methods'])); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEmpty($response['cart']['available_payment_methods']); } /** * @expectedException \Exception * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" */ - public function testGetPaymentMethodsOfNonExistentCart() + public function testGetAvailablePaymentMethodsOfNonExistentCart() { $maskedQuoteId = 'non_existent_masked_id'; $query = $this->getQuery($maskedQuoteId); @@ -112,9 +101,8 @@ public function testGetPaymentMethodsOfNonExistentCart() * @param string $maskedQuoteId * @return string */ - private function getQuery( - string $maskedQuoteId - ): string { + private function getQuery(string $maskedQuoteId): string + { return <<<QUERY { cart(cart_id: "$maskedQuoteId") { @@ -126,16 +114,4 @@ private function getQuery( } QUERY; } - - /** - * @param string $reservedOrderId - * @return string - */ - private function getMaskedQuoteIdByReservedOrderId(string $reservedOrderId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php index 916d5951b0ff2..6b1f540e4d47a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -7,10 +7,7 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -20,41 +17,27 @@ class GetCartTest extends GraphQlAbstract { /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; - - /** - * @var CustomerTokenServiceInterface - */ - private $customerTokenService; + private $getMaskedQuoteIdByReservedOrderId; protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); - $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php */ public function testGetCart() { - $maskedQuoteId = $this->unAssignCustomerFromQuote('test_order_item_with_items'); - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlQuery($query); @@ -63,21 +46,23 @@ public function testGetCart() self::assertCount(2, $response['cart']['items']); self::assertNotEmpty($response['cart']['items'][0]['id']); - self::assertEquals($response['cart']['items'][0]['qty'], 2); - self::assertEquals($response['cart']['items'][0]['product']['sku'], 'simple'); + self::assertEquals(2, $response['cart']['items'][0]['qty']); + self::assertEquals('simple', $response['cart']['items'][0]['product']['sku']); self::assertNotEmpty($response['cart']['items'][1]['id']); - self::assertEquals($response['cart']['items'][1]['qty'], 1); - self::assertEquals($response['cart']['items'][1]['product']['sku'], 'simple_one'); + self::assertEquals(2, $response['cart']['items'][1]['qty']); + self::assertEquals('virtual-product', $response['cart']['items'][1]['product']['sku']); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php */ public function testGetCustomerCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_item_with_items'); - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $this->expectExceptionMessage( "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" @@ -92,25 +77,22 @@ public function testGetCustomerCart() public function testGetNonExistentCart() { $maskedQuoteId = 'non_existent_masked_id'; - $query = $this->getCartQuery($maskedQuoteId); + $query = $this->getQuery($maskedQuoteId); $this->graphQlQuery($query); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + * * @expectedException \Exception * @expectedExceptionMessage Current user does not have an active cart. */ public function testGetInactiveCart() { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); - $quote->setIsActive(false); - $this->quoteResource->save($quote); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); - - $query = $this->getCartQuery($maskedQuoteId); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); $this->graphQlQuery($query); } @@ -119,12 +101,11 @@ public function testGetInactiveCart() * @param string $maskedQuoteId * @return string */ - private function getCartQuery( - string $maskedQuoteId - ) : string { + private function getQuery(string $maskedQuoteId): string + { return <<<QUERY { - cart(cart_id: "$maskedQuoteId") { + cart(cart_id: "{$maskedQuoteId}") { items { id qty @@ -136,31 +117,4 @@ private function getCartQuery( } QUERY; } - - /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - - /** - * @param string $reversedQuoteId - * @param int $customerId - * @return string - */ - private function unAssignCustomerFromQuote( - string $reversedQuoteId - ): string { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - $quote->setCustomerId(0); - $this->quoteResource->save($quote); - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php index 27de0d12e413d..3808ce38b9d7b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -7,9 +7,7 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -19,34 +17,24 @@ class SetBillingAddressOnCartTest extends GraphQlAbstract { /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; + private $getMaskedQuoteIdByReservedOrderId; protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetNewBillingAddress() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -98,11 +86,13 @@ public function testSetNewBillingAddress() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ public function testSetNewBillingAddressWithUseForShippingParameter() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -172,11 +162,13 @@ public function testSetNewBillingAddressWithUseForShippingParameter() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php */ public function testSetBillingAddressToCustomerCart() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -215,13 +207,17 @@ public function testSetBillingAddressToCustomerCart() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * _security + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception * @expectedExceptionMessage The current customer isn't authorized. */ public function testSetBillingAddressFromAddressBook() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -263,6 +259,59 @@ public function testSetBillingAddressOnNonExistentCart() $this->graphQlQuery($query); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderSetWithoutRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetBillingAddressWithoutRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + {$input} + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlQuery($query); + } + + /** + * @return array + */ + public function dataProviderSetWithoutRequiredParameters(): array + { + return [ + 'missed_billing_address' => [ + 'cart_id: "cart_id_value"', + 'Field SetBillingAddressOnCartInput.billing_address of required type BillingAddressInput!' + . ' was not provided.', + ], + 'missed_cart_id' => [ + 'billing_address: {}', + 'Required parameter "cart_id" is missing' + ] + ]; + } + /** * Verify the all the whitelisted fields for a New Address Object * @@ -285,16 +334,4 @@ private function assertNewAddressFields(array $addressResponse, string $addressT $this->assertResponseFields($addressResponse, $assertionMap); } - - /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php index 182bbaf618505..8f37f00c3db7f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php @@ -7,10 +7,9 @@ namespace Magento\GraphQl\Quote\Guest; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\OfflinePayments\Model\Cashondelivery; use Magento\OfflinePayments\Model\Checkmo; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -20,19 +19,9 @@ class SetPaymentMethodOnCartTest extends GraphQlAbstract { /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; + private $getMaskedQuoteIdByReservedOrderId; /** * @inheritdoc @@ -40,21 +29,21 @@ class SetPaymentMethodOnCartTest extends GraphQlAbstract protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ - public function testSetPaymentWithVirtualProduct() + public function testSetPaymentOnCartWithSimpleProduct() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_virtual_product'); - $this->unAssignCustomerFromQuote('test_order_with_virtual_product'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlQuery($query); self::assertArrayHasKey('setPaymentMethodOnCart', $response); @@ -64,15 +53,33 @@ public function testSetPaymentWithVirtualProduct() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. */ - public function testSetPaymentWithSimpleProduct() + public function testSetPaymentOnCartWithSimpleProductAndWithoutAddress() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); - $this->unAssignCustomerFromQuote('test_order_1'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testSetPaymentOnCartWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlQuery($query); self::assertArrayHasKey('setPaymentMethodOnCart', $response); @@ -82,43 +89,56 @@ public function testSetPaymentWithSimpleProduct() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * * @expectedException \Exception - * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + * @expectedExceptionMessage The requested Payment Method is not available. */ - public function testSetPaymentWithSimpleProductWithoutAddress() + public function testSetNonExistentPaymentMethod() { - $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $this->graphQlQuery($query); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php * @expectedException \Exception - * @expectedExceptionMessage The requested Payment Method is not available. + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" */ - public function testSetNonExistingPaymentMethod() + public function testSetPaymentOnNonExistentCart() { - $methodCode = 'noway'; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); - $this->unAssignCustomerFromQuote('test_order_1'); - - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $maskedQuoteId = 'non_existent_masked_id'; + $query = <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + items { + id + } + } +} +QUERY; $this->graphQlQuery($query); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php */ public function testSetPaymentMethodToCustomerCart() { $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $query = $this->getQuery($maskedQuoteId, $methodCode); $this->expectExceptionMessage( "The current user cannot perform operations on cart \"$maskedQuoteId\"" @@ -127,39 +147,78 @@ public function testSetPaymentMethodToCustomerCart() } /** - * @expectedException \Exception - * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetPaymentMethodWithoutRequiredParameters + * @throws \Exception */ - public function testSetPaymentOnNonExistentCart() + public function testSetPaymentMethodWithoutRequiredParameters(string $input, string $message) { - $maskedQuoteId = 'non_existent_masked_id'; $query = <<<QUERY -{ - cart(cart_id: "$maskedQuoteId") { - items { - id +mutation { + setPaymentMethodOnCart( + input: { + {$input} + } + ) { + cart { + items { + qty + } } } } QUERY; + $this->expectExceptionMessage($message); $this->graphQlQuery($query); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_payment_saved.php + * @return array + */ + public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', + 'Required parameter "cart_id" is missing.' + ], + 'missed_payment_method' => [ + 'cart_id: "test"', + 'Required parameter "code" for "payment_method" is missing.' + ], + 'missed_payment_method_code' => [ + 'cart_id: "test", payment_method: {code: ""}', + 'Required parameter "code" for "payment_method" is missing.' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php */ public function testReSetPayment() { - /** @var \Magento\Quote\Model\Quote $quote */ - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_1_with_payment'); - $this->unAssignCustomerFromQuote('test_order_1_with_payment'); - $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $methodCode = Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE; + $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlQuery($query); self::assertArrayHasKey('setPaymentMethodOnCart', $response); self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); } @@ -168,20 +227,18 @@ public function testReSetPayment() * @param string $methodCode * @return string */ - private function prepareMutationQuery( + private function getQuery( string $maskedQuoteId, string $methodCode ) : string { return <<<QUERY mutation { - setPaymentMethodOnCart(input: - { - cart_id: "{$maskedQuoteId}", - payment_method: { - code: "{$methodCode}" - } - }) { - + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + } + }) { cart { selected_payment_method { code @@ -191,31 +248,4 @@ private function prepareMutationQuery( } QUERY; } - - /** - * @param string $reversedQuoteId - * @param int $customerId - * @return string - */ - private function unAssignCustomerFromQuote( - string $reversedQuoteId - ): string { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - $quote->setCustomerId(0); - $this->quoteResource->save($quote); - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - - /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index 33aeb2b902a8f..e21d9ed64d491 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -7,14 +7,9 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Multishipping\Helper\Data; -use Magento\Quote\Model\QuoteFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\TestFramework\ObjectManager; /** * Test for set shipping addresses on cart mutation @@ -22,34 +17,24 @@ class SetShippingAddressOnCartTest extends GraphQlAbstract { /** - * @var QuoteResource + * @var GetMaskedQuoteIdByReservedOrderId */ - private $quoteResource; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; + private $getMaskedQuoteIdByReservedOrderId; protected function setUp() { $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quoteFactory = $objectManager->get(QuoteFactory::class); - $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php */ - public function testSetNewShippingAddress() + public function testSetNewShippingAddressOnCartWithSimpleProduct() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -103,13 +88,16 @@ public function testSetNewShippingAddress() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * * @expectedException \Exception * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. */ - public function testSetNewShippingAddressOnQuoteWithVirtualProducts() + public function testSetNewShippingAddressOnCartWithVirtualProduct() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_virtual_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -146,13 +134,17 @@ public function testSetNewShippingAddressOnQuoteWithVirtualProducts() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * _security + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception * @expectedExceptionMessage The current customer isn't authorized. */ public function testSetShippingAddressFromAddressBook() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -178,13 +170,17 @@ public function testSetShippingAddressFromAddressBook() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception */ public function testSetShippingAddressToCustomerCart() { - $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 1); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -209,12 +205,14 @@ public function testSetShippingAddressToCustomerCart() $this->expectExceptionMessage( "The current user cannot perform operations on cart \"$maskedQuoteId\"" ); - $this->graphQlQuery($query); } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @dataProvider dataProviderUpdateWithMissedRequiredParameters * @param string $input * @param string $message @@ -222,7 +220,7 @@ public function testSetShippingAddressToCustomerCart() */ public function testSetNewShippingAddressWithMissedRequiredParameters(string $input, string $message) { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -251,7 +249,7 @@ public function testSetNewShippingAddressWithMissedRequiredParameters(string $in /** * @return array */ - public function dataProviderUpdateWithMissedRequiredParameters() + public function dataProviderUpdateWithMissedRequiredParameters(): array { return [ 'shipping_addresses' => [ @@ -266,13 +264,16 @@ public function dataProviderUpdateWithMissedRequiredParameters() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * * @expectedException \Exception * @expectedExceptionMessage You cannot specify multiple shipping addresses. */ public function testSetMultipleNewShippingAddresses() { - $maskedQuoteId = $this->getMaskedQuoteIdByReversedQuoteId('test_order_with_simple_product_without_address'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = <<<QUERY mutation { @@ -343,32 +344,4 @@ private function assertNewShippingAddressFields(array $shippingAddressResponse): $this->assertResponseFields($shippingAddressResponse, $assertionMap); } - - /** - * @param string $reversedQuoteId - * @return string - */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - - /** - * @param string $reversedQuoteId - * @param int $customerId - * @return string - */ - private function assignQuoteToCustomer( - string $reversedQuoteId = 'test_order_with_simple_product_without_address', - int $customerId = 1 - ): string { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - $quote->setCustomerId($customerId); - $this->quoteResource->save($quote); - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php index 14662148b32dc..bf08ce8adce6c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -7,7 +7,6 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; @@ -110,34 +109,6 @@ public function testSetMultipleShippingMethods() $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); } - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_shipping_method.php - */ - public function testReSetShippingMethod() - { - $maskedQuoteId = $this->unAssignCustomerFromQuoteAndShippingAddress('test_order_1'); - $methodCode = 'flatrate'; - $carrierCode = 'flatrate'; - $quote = $this->getQuoteByReversedQuoteId('test_order_1'); - $shippingAddress = $quote->getShippingAddress(); - $shippingAddressId = $shippingAddress->getId(); - $query = $this->prepareMutationQuery($maskedQuoteId, $methodCode, $carrierCode, $shippingAddressId); - - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey('setShippingMethodsOnCart', $response); - self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); - self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); - foreach ($response['setShippingMethodsOnCart']['cart']['shipping_addresses'] as $address) { - self::assertArrayHasKey('address_id', $address); - if ($address['address_id'] == $shippingAddressId) { - self::assertArrayHasKey('selected_shipping_method', $address); - self::assertEquals($methodCode, $address['selected_shipping_method']['method_code']); - self::assertEquals($carrierCode, $address['selected_shipping_method']['carrier_code']); - } - } - } - /** * @param string $maskedQuoteId * @param string $shippingMethodCode @@ -157,15 +128,16 @@ private function prepareMutationQuery( setShippingMethodsOnCart(input: { cart_id: "$maskedQuoteId", - shipping_methods: [{ + shipping_addresses: [{ cart_address_id: $shippingAddressId - method_code: "$shippingMethodCode" - carrier_code: "$shippingCarrierCode" + shipping_method: { + method_code: "$shippingMethodCode" + carrier_code: "$shippingCarrierCode" + } }] }) { cart { shipping_addresses { - address_id selected_shipping_method { carrier_code method_code @@ -181,48 +153,14 @@ private function prepareMutationQuery( } /** - * @param string $reversedQuoteId - * @param int $customerId - * @return string - */ - private function unAssignCustomerFromQuoteAndShippingAddress( - string $reversedQuoteId - ): string { - /** @var Quote $quote */ - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - $quote->setCustomerId(0); - - $shippingAddress = $quote->getShippingAddress(); - $shippingAddress->setCustomerId(0); - $shippingAddress->setCustomerAddressId(0); - - $this->quoteResource->save($quote); - - return $this->quoteIdToMaskedId->execute((int)$quote->getId()); - } - - /** - * @param string $reversedQuoteId - * @return Quote - */ - private function getQuoteByReversedQuoteId(string $reversedQuoteId): Quote - { - $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); - - return $quote; - } - - /** - * @param string $reversedQuoteId + * @param string $reversedOrderId * @return string * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ - private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + private function getMaskedQuoteIdByReservedOrderId(string $reversedOrderId): string { $quote = $this->quoteFactory->create(); - $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $this->quoteResource->load($quote, $reversedOrderId, 'reserved_order_id'); return $this->quoteIdToMaskedId->execute((int)$quote->getId()); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/OfflineShipping/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetOfflineShippingMethodsOnCartTest.php similarity index 96% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/OfflineShipping/SetOfflineShippingMethodsOnCartTest.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetOfflineShippingMethodsOnCartTest.php index 80acbbdb64230..cf32003340a66 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/OfflineShipping/SetOfflineShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetOfflineShippingMethodsOnCartTest.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\GraphQl\OfflineShipping; +namespace Magento\GraphQl\Quote; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; @@ -53,7 +53,7 @@ protected function setUp() /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - * @magentoApiDataFixture Magento/OfflineShipping/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php * @magentoApiDataFixture Magento/OfflineShipping/_files/tablerates_weight.php * * @param string $carrierCode @@ -103,7 +103,7 @@ public function offlineShippingMethodDataProvider() /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - * @magentoApiDataFixture Magento/OfflineShipping/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php */ public function testSetShippingMethodTwiceInOneRequest() { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..463f2c4af101f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Ups; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting "UPS" shipping method on cart + */ +class SetUpsShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * Defines carrier code for "UPS" shipping method + */ + const CARRIER_CODE = 'ups'; + + /** + * Defines method code for the "Ground" UPS shipping + */ + const CARRIER_METHOD_CODE_GROUND = 'GND'; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Ups/_files/enable_ups_shipping_method.php + */ + public function testSetUpsShippingMethod() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $shippingAddressId = (int)$quote->getShippingAddress()->getId(); + + $query = $this->getAddUpsShippingMethodQuery( + $maskedQuoteId, + $shippingAddressId, + self::CARRIER_CODE, + self::CARRIER_METHOD_CODE_GROUND + ); + + $response = $this->sendRequestWithToken($query); + $addressesInformation = $response['setShippingMethodsOnCart']['cart']['shipping_addresses']; + $expectedResult = [ + 'carrier_code' => self::CARRIER_CODE, + 'method_code' => self::CARRIER_METHOD_CODE_GROUND, + 'label' => 'United Parcel Service - Ground', + ]; + self::assertEquals($addressesInformation[0]['selected_shipping_method'], $expectedResult); + } + + /** + * Generates query for setting the specified shipping method on cart + * + * @param int $shippingAddressId + * @param string $maskedQuoteId + * @param string $carrierCode + * @param string $methodCode + * @return string + */ + private function getAddUpsShippingMethodQuery( + string $maskedQuoteId, + int $shippingAddressId, + string $carrierCode, + string $methodCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "$maskedQuoteId" + shipping_methods: [ + { + cart_address_id: $shippingAddressId + carrier_code: "$carrierCode" + method_code: "$methodCode" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + } + } + } + } +} +QUERY; + } + + /** + * Sends a GraphQL request with using a bearer token + * + * @param string $query + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function sendRequestWithToken(string $query): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + + return $this->graphQlQuery($query, [], '', $headerMap); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php index d570fc09b7714..4aac5d9445934 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -93,6 +93,36 @@ public function testGetCustomerWishlist(): void $this->assertEquals($wishlistItemProduct->getName(), $response['wishlist']['items'][0]['product']['name']); } + /** + * @expectedException \Exception + * @expectedExceptionMessage The current user cannot perform operations on wishlist + */ + public function testGetGuestWishlist() + { + $query = + <<<QUERY +{ + wishlist { + items_count + name + sharing_code + updated_at + items { + id + qty + description + added_at + product { + sku + name + } + } + } +} +QUERY; + $this->graphQlQuery($query); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php index 82e3e297dc456..03492f7ae1a9e 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php @@ -195,6 +195,13 @@ class ConditionsElement extends SimpleElement */ protected $exception; + /** + * Condition option text selector. + * + * @var string + */ + private $conditionOptionTextSelector = '//option[normalize-space(text())="%s"]'; + /** * @inheritdoc */ @@ -282,10 +289,16 @@ protected function addCondition($type, ElementInterface $context) $count = 0; do { - $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); - try { - $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select')->setValue($type); + $specificType = $newCondition->find( + sprintf($this->conditionOptionTextSelector, $type), + Locator::SELECTOR_XPATH + )->isPresent(); + $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); + $condition = $specificType + ? $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'selectcondition') + : $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select'); + $condition->setValue($type); $isSetType = true; } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { $isSetType = false; diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php new file mode 100644 index 0000000000000..15a799eac5188 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectconditionElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php index 3020e69c06399..fefe0d2c126e5 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php @@ -65,6 +65,13 @@ class ExportAdvancedPricingTest extends Injectable */ private $catalogProductIndex; + /** + * Cron command + * + * @var Cron + */ + private $cron; + /** * Run cron before tests running * @@ -85,18 +92,21 @@ public function __prepare( * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex * @param CatalogProductIndex $catalogProductIndexPage + * @param Cron $cron * @return void */ public function __inject( TestStepFactory $stepFactory, FixtureFactory $fixtureFactory, AdminExportIndex $adminExportIndex, - CatalogProductIndex $catalogProductIndexPage + CatalogProductIndex $catalogProductIndexPage, + Cron $cron ) { $this->stepFactory = $stepFactory; $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; $this->catalogProductIndex = $catalogProductIndexPage; + $this->cron = $cron; } /** @@ -130,8 +140,12 @@ public function test( if ($website) { $website->persist(); $this->setupCurrencyForCustomWebsite($website, $currencyCustomWebsite); + $this->cron->run(); + $this->cron->run(); } $products = $this->prepareProducts($products, $website); + $this->cron->run(); + $this->cron->run(); $this->adminExportIndex->open(); $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData = $this->fixtureFactory->createByCode( diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml index d069499da4aab..07646c2aceda8 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml @@ -50,7 +50,6 @@ <constraint name="Magento\AdvancedPricingImportExport\Test\Constraint\AssertExportAdvancedPricing"/> </variation> <variation name="ExportAdvancedPricingTestVariation4" summary="Trying export product data for product available on main website with default currency and custom website with different currency" ticketId="MAGETWO-46153"> - <data name="issue" xsi:type="string">MC-13864 Consumer always read config from memory</data> <data name="configData" xsi:type="string">price_scope_website</data> <data name="exportData" xsi:type="string">csv_with_advanced_pricing</data> <data name="products/0" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml index 3ad8cff31eaf8..bfbe233b9dc1b 100644 --- a/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation4" summary="Export bundle products" ticketId="MAGETWO-30602"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">bundleProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php index 2035e7e83200f..30a323eebb736 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php @@ -187,6 +187,8 @@ public function assignCategory($parentCategoryName, $childCategoryName) */ public function expandAllCategories() { + $this->getTemplateBlock()->waitLoader(); $this->_rootElement->find($this->expandAll)->click(); + $this->getTemplateBlock()->waitLoader(); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php index 090042140a548..48769126d88ea 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php @@ -48,7 +48,7 @@ public function getSelectSortType() public function getSortType() { $content = $this->_rootElement->find($this->sorter)->getText(); - return explode("\n", $content); + return array_values(array_filter(array_map('trim', explode("\n", $content)))); } /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml index c6a66beac7c79..ea6808ee2a7f5 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml @@ -96,6 +96,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForAssignedProducts" /> </variation> <variation name="CreateCategoryEntityTestVariation5_Anchor_MostOfFields"> + <data name="tag" xsi:type="string">test_type:acceptance_test</data> <data name="description" xsi:type="string">Create anchor subcategory with all fields</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php index e55558482c1f3..b5cd056fb99ad 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php @@ -104,6 +104,8 @@ public function test( $exportData->persist(); $this->adminExportIndex->getExportForm()->fill($exportData); $this->adminExportIndex->getFilterExport()->clickContinue(); + $this->cron->run(); + $this->cron->run(); $this->assertExportProduct->processAssert($export, $exportedFields, $products); } diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml index b94f21371496a..be22eab8ac717 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation1" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -27,6 +28,7 @@ </data> </variation> <variation name="ExportProductsTestVariation2" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -43,6 +45,7 @@ <constraint name="Magento\CatalogImportExport\Test\Constraint\AssertExportProductDate" /> </variation> <variation name="ExportProductsTestVariation3" summary="Export simple product with custom attribute" ticketId="MAGETWO-46121"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -58,7 +61,7 @@ </data> </variation> <variation name="ExportProductsTestVariation5" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> - <data name="issue" xsi:type="string">>MC-13864 Consumer always read config from memory</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml index edb0aad954fbb..77e5e2b91d93f 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ImportProductsTest" summary="Import products"> <variation name="ImportProductVariation1" ticketId="MAGETWO-47724" summary="Import Products with Add/Update Behavior"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="import/data" xsi:type="array"> <item name="entity" xsi:type="string">Products</item> <item name="behavior" xsi:type="string">Add/Update</item> @@ -38,6 +39,7 @@ <constraint name="Magento\CatalogImportExport\Test\Constraint\AssertProductsInGrid" /> </variation> <variation name="ImportProductVariation2" ticketId="MAGETWO-47719" summary="Import Products assigned to different websites with Replace Behavior"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="import/data/entity" xsi:type="string">Products</data> <data name="import/data/behavior" xsi:type="string">Replace</data> <data name="import/data/validation_strategy" xsi:type="string">Stop on Error</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml index 0437e0a5e999b..8c465544a3283 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\CreateSearchTermEntityTest" summary="Create Search Term" ticketId="MAGETWO-26165"> <variation name="CreateSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerm/data/query_text/value" xsi:type="string">catalogProductSimple::sku</data> <data name="searchTerm/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="searchTerm/data/redirect" xsi:type="string">http://example.com/</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml index a9cc0dfd34f9f..8fdd7ef715521 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\DeleteSearchTermEntityTest" summary="Delete Search Term" ticketId="MAGETWO-26491"> <variation name="DeleteSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerm/dataset" xsi:type="string">default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermSuccessDeleteMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml index 3bf4e521c4a04..3ef2b65c0224b 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\MassDeleteSearchTermEntityTest" summary="Mass Delete Search Term" ticketId="MAGETWO-26599"> <variation name="MassDeleteSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerms" xsi:type="string">catalogSearchQuery::default,catalogSearchQuery::default,catalogSearchQuery::default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermSuccessMassDeleteMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermMassActionsNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml index 398054f1f0ed3..8b15da5ecd2ef 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml @@ -14,7 +14,7 @@ <data name="category/data/include_in_menu" xsi:type="string">Yes</data> <data name="category/data/name" xsi:type="string">Subcategory%isolation%</data> <data name="category/data/url_key" xsi:type="string">subcategory-%isolation%</data> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1, mftf_migrated:yes</data> <constraint name="Magento\CatalogUrlRewrite\Test\Constraint\AssertCategoryUrlDuplicateErrorMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml index 1116821f756a9..8110ed1ed00b1 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogUrlRewrite\Test\TestCase\CreateDuplicateUrlProductEntity" summary="Create Simple Product" ticketId="MAGETWO-69427"> <variation name="CreateDuplicateUrlProductEntityTestVariation1" summary="Create Duplicate Url Product"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1, mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml index 8b2460718097c..b4c97a11b9145 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -9,7 +9,7 @@ <testCase name="Magento\Checkout\Test\TestCase\UpdateProductFromMiniShoppingCartEntityTest" summary="Update Product from Mini Shopping Cart" ticketId="MAGETWO-29812"> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation1" summary="Update Product Qty on Mini Shopping Cart" ticketId=" MAGETWO-35536"> <data name="issue" xsi:type="string">https://github.com/magento-engcom/msi/issues/1624</data> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0, mftf_migrated:yes</data> <data name="originalProduct/0" xsi:type="string">catalogProductSimple::default</data> <data name="checkoutData/dataset" xsi:type="string">simple_order_qty_2</data> <data name="use_minicart_to_edit_qty" xsi:type="boolean">true</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml index e0ea721a51f1b..5caa3ba9b924e 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\UpdateShoppingCartTest" summary="Update Shopping Cart" ticketId="MAGETWO-25081"> <variation name="UpdateShoppingCartTestVariation1"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0,mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">default</data> <data name="product/data/price/value" xsi:type="string">100</data> <data name="product/data/checkout_data/qty" xsi:type="string">3</data> @@ -20,7 +20,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInShoppingCart" /> </variation> <variation name="UpdateShoppingCartTestVariation2"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0,mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">with_two_custom_option</data> <data name="product/data/price/value" xsi:type="string">50</data> <data name="product/data/checkout_data/qty" xsi:type="string">11</data> diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml index d304d305a7265..a266b09278ddb 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> <page name="MultishippingCheckoutOverview" mca="multishipping/checkout/overview" module="Magento_Checkout"> - <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator="#checkout-agreements" strategy="css selector"/> + <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator=".checkout-agreements" strategy="css selector"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml index 93240586ec92c..0a2ce7ab7f183 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml @@ -30,6 +30,7 @@ </data> </variation> <variation name="ExportProductsTestVariation8" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> @@ -45,7 +46,7 @@ </data> </variation> <variation name="ExportProductsTestVariation9" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> - <data name="issue" xsi:type="string">>MC-13864 Consumer always read config from memory</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml index 70a912a3b5ffe..e88e5161e474e 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\VerifyDisabledCustomerGroupFieldTest" summary="Check that field is disabled in system Customer Group" ticketId="MAGETWO-52481"> <variation name="VerifyDisabledCustomerGroupField1" summary="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerGroup/dataset" xsi:type="string">NOT_LOGGED_IN</data> <data name="disabledFields" xsi:type="array"> <item name="0" xsi:type="string">customer_group_code</item> diff --git a/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml index cffcdbf45a6dc..a110dc6a89f8c 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation6" summary="Export grouped product with special price" ticketId="MAGETWO-46116"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">groupedProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml b/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml index cbdce59057195..bc529729f1217 100644 --- a/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml +++ b/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\PageCache\Test\TestCase\FlushStaticFilesCacheButtonVisibilityTest" summary="Flush Static Files Cache button visibility" ticketId="MAGETWO-39934"> <variation name="FlushStaticFilesCacheButtonVisibilityTest"> - <data name="tag" xsi:type="string">severity:S3</data> + <data name="tag" xsi:type="string">severity:S3, mftf_migrated:yes</data> <constraint name="Magento\PageCache\Test\Constraint\AssertFlushStaticFilesCacheButtonVisibility" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml index 8445a21604cdd..4d96ebd2edbe3 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest70"> - <data name="menuItem" xsi:type="string">Marketing > Reviews</data> + <data name="menuItem" xsi:type="string">Marketing > All Reviews</data> <data name="pageTitle" xsi:type="string">Reviews</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml index 38ce04fa56d81..e05d0fea6b129 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml @@ -8,17 +8,20 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\CreateCustomOrderStatusEntityTest" summary="Create Custom Order Status Entity" ticketId="MAGETWO-23412"> <variation name="CreateCustomOrderStatusEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">order_status%isolation%</data> <data name="orderStatus/data/label" xsi:type="string">orderLabel%isolation%</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusInGrid" /> </variation> <variation name="CreateCustomOrderStatusEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">pending</data> <data name="orderStatus/data/label" xsi:type="string">orderLabel%isolation%</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusDuplicateStatus" /> </variation> <variation name="CreateCustomOrderStatusEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">order_status%isolation%</data> <data name="orderStatus/data/label" xsi:type="string">Suspected Fraud</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusSuccessCreateMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml index 5a547f69280e1..e35ef853d1b68 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\CreateWebsiteEntityTest" summary="Create Website" ticketId="MAGETWO-27665"> <variation name="CreateWebsiteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="website/data/name" xsi:type="string">website_%isolation%</data> <data name="website/data/code" xsi:type="string">code_%isolation%</data> <constraint name="Magento\Store\Test\Constraint\AssertWebsiteSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml index 306a9fd2024a4..cd37c555fdb1d 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\DeleteStoreEntityTest" summary="Delete Store View" ticketId="MAGETWO-27942"> <variation name="DeleteStoreEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="store/dataset" xsi:type="string">custom</data> <data name="createBackup" xsi:type="string">Yes</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessDeleteAndBackupMessages" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml index ac857ad035f44..5db0e7f8baad4 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateWebsiteEntityTest" summary="Update Website" ticketId="MAGETWO-27690"> <variation name="UpdateWebsiteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="websiteOrigin/dataset" xsi:type="string">custom_website</data> <data name="website/data/name" xsi:type="string">website_upd%isolation%</data> <data name="website/data/code" xsi:type="string">code_upd%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml index 60de554d594d2..8f12930aa417b 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\UpdateProductUrlRewriteEntityTest" summary="Update Product URL Rewrites" ticketId="MAGETWO-24819"> <variation name="UpdateProductUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">product/%catalogProductSimple::product_100_dollar%</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">test_%isolation%.html</data> diff --git a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php index 9ca351aa1cf98..32240e68ae73e 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php @@ -95,17 +95,9 @@ public function initialize() $this->amqpHelper->deleteConnection($connectionName); } $this->amqpHelper->clearQueue("async.operations.all"); - foreach ($this->consumers as $consumer) { - foreach ($this->getConsumerProcessIds($consumer) as $consumerProcessId) { - exec("kill {$consumerProcessId}"); - } - } - foreach ($this->consumers as $consumer) { - if (!$this->getConsumerProcessIds($consumer)) { - exec("{$this->getConsumerStartCommand($consumer, true)} > /dev/null &"); - } - sleep(5); - } + + $this->stopConsumers(); + $this->startConsumers(); if (file_exists($this->logFilePath)) { // try to remove before failing the test @@ -230,4 +222,19 @@ public function getPublisher() { return $this->publisher; } + + /** + * Start consumers + * + * @return void + */ + public function startConsumers(): void + { + foreach ($this->consumers as $consumer) { + if (!$this->getConsumerProcessIds($consumer)) { + exec("{$this->getConsumerStartCommand($consumer, true)} > /dev/null &"); + } + sleep(5); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php index 8aeee9cf12494..e11c5ce5d9cf3 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php @@ -87,41 +87,6 @@ public function testMassactionDefaultValues() $this->assertFalse($blockEmpty->isAvailable()); } - public function testGetJavaScript() - { - $this->loadLayout(); - - $javascript = $this->_block->getJavaScript(); - - $expectedItemFirst = '#"option_id1":{"label":"Option One",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"complete":"Test","id":"option_id1"}#'; - $this->assertRegExp($expectedItemFirst, $javascript); - - $expectedItemSecond = '#"option_id2":{"label":"Option Two",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"confirm":"Are you sure\?","id":"option_id2"}#'; - $this->assertRegExp($expectedItemSecond, $javascript); - } - - public function testGetJavaScriptWithAddedItem() - { - $this->loadLayout(); - - $input = [ - 'id' => 'option_id3', - 'label' => 'Option Three', - 'url' => '*/*/option3', - 'block_name' => 'admin.test.grid.massaction.option3', - ]; - $expected = '#"option_id3":{"id":"option_id3","label":"Option Three",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"block_name":"admin.test.grid.massaction.option3"}#'; - - $this->_block->addItem($input['id'], $input); - $this->assertRegExp($expected, $this->_block->getJavaScript()); - } - /** * @param string $mageMode * @param int $expectedCount @@ -213,21 +178,4 @@ public function getItemsDataProvider() ] ]; } - - public function testGridContainsMassactionColumn() - { - $this->loadLayout(); - $this->_layout->getBlock('admin.test.grid')->toHtml(); - - $gridMassactionColumn = $this->_layout->getBlock('admin.test.grid') - ->getColumnSet() - ->getChildBlock('massaction'); - - $this->assertNotNull($gridMassactionColumn, 'Massaction column does not exist in the grid column set'); - $this->assertInstanceOf( - \Magento\Backend\Block\Widget\Grid\Column::class, - $gridMassactionColumn, - 'Massaction column is not an instance of \Magento\Backend\Block\Widget\Column' - ); - } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php index 07af21505f180..89f1e5e5d53d6 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php @@ -21,6 +21,8 @@ public function testAjaxBlockAction() public function testTunnelAction() { + $this->markTestSkipped('MAGETWO-98800: TunnelAction fails when Google Chart API is not available'); + $testUrl = \Magento\Backend\Block\Dashboard\Graph::API_URL . '?cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'; $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $testUrl); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index a2967878402d0..3ec8c806dcbb1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -5,13 +5,49 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\PublisherConsumerController; /** * @magentoAppArea adminhtml */ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** @var PublisherConsumerController */ + private $publisherConsumerController; + private $consumers = ['product_action_attribute.update']; + + protected function setUp() + { + $this->publisherConsumerController = Bootstrap::getObjectManager()->create(PublisherConsumerController::class, [ + 'consumers' => $this->consumers, + 'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt", + 'maxMessages' => null, + 'appInitParams' => Bootstrap::getInstance()->getAppInitParams() + ]); + + try { + $this->publisherConsumerController->startConsumers(); + } catch (\Magento\TestFramework\MessageQueue\EnvironmentPreconditionException $e) { + $this->markTestSkipped($e->getMessage()); + } catch (\Magento\TestFramework\MessageQueue\PreconditionFailedException $e) { + $this->fail( + $e->getMessage() + ); + } + + parent::setUp(); + } + + protected function tearDown() + { + $this->publisherConsumerController->stopConsumers(); + parent::tearDown(); + } + /** * @covers \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::execute * @@ -20,7 +56,7 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr */ public function testSaveActionRedirectsSuccessfully() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); @@ -59,13 +95,14 @@ public function testSaveActionRedirectsSuccessfully() */ public function testSaveActionChangeVisibility($attributes) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepository $repository */ + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); $product->setOrigData(); - $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); $product->save(); /** @var $session \Magento\Backend\Model\Session */ @@ -75,15 +112,29 @@ public function testSaveActionChangeVisibility($attributes) $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); + /** @var \Magento\Catalog\Model\Category $category */ - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = Bootstrap::getObjectManager()->get( \Magento\Catalog\Block\Product\ListProduct::class ); + $this->publisherConsumerController->waitForAsynchronousResult( + function () use ($repository) { + sleep(3); + return $repository->get( + 'simple', + false, + null, + true + )->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE; + }, + [] + ); + $category = $categoryFactory->create()->load(2); $layer = $listProduct->getLayer(); $layer->setCurrentCategory($category); @@ -105,7 +156,7 @@ public function testSaveActionChangeVisibility($attributes) */ public function testValidateActionWithMassUpdate($attributes) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); @@ -156,8 +207,8 @@ public function validateActionDataProvider() public function saveActionVisibilityAttrDataProvider() { return [ - ['arguments' => ['visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH]], - ['arguments' => ['visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_CATALOG]] + ['arguments' => ['visibility' => Visibility::VISIBILITY_BOTH]], + ['arguments' => ['visibility' => Visibility::VISIBILITY_IN_CATALOG]] ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index 7954e2c36227f..476f01eb277df 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -12,6 +12,11 @@ class ProductTest extends TestCase { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @var Product */ @@ -29,7 +34,8 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->get(Product::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->model = $this->objectManager->create(Product::class); } /** @@ -42,11 +48,29 @@ public function testGetAttributeRawValue() $sku = 'simple'; $attribute = 'name'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); - + $product = $this->productRepository->get($sku); $actual = $this->model->getAttributeRawValue($product->getId(), $attribute, null); self::assertEquals($product->getName(), $actual); } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store catalog/price/scope 1 + */ + public function testUpdateStoreSpecificSpecialPrice() + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple', true, 1); + $this->assertEquals(5.99, $product->getSpecialPrice()); + + $product->setSpecialPrice(''); + $this->model->save($product); + $product = $this->productRepository->get('simple', false, 1, true); + $this->assertEmpty($product->getSpecialPrice()); + + $product = $this->productRepository->get('simple', false, 0, true); + $this->assertEquals(5.99, $product->getSpecialPrice()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php similarity index 84% rename from dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php rename to dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php index cbc0476efd1b5..a9ab0e11312b2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php @@ -3,13 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + /* Delete attribute with text_attribute code */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class ); $attribute->load('text_attribute', 'attribute_code'); $attribute->delete(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php index 168073bc6ab74..c57c7c3fd6a92 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php index 168073bc6ab74..c57c7c3fd6a92 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php new file mode 100644 index 0000000000000..d3f0e9fa2a5ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogUrlRewrite\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class ProcessUrlRewriteOnChangeVisibilityObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ManagerInterface + */ + private $eventManager; + + /** + * Set up + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->eventManager = $this->objectManager->create(ManagerInterface::class); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @magentoAppIsolation enabled + */ + public function testMakeProductInvisibleViaMassAction() + { + /** @var \Magento\Catalog\Model\Product $product*/ + $product = $this->productRepository->get('product1'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(0); + + $testStore = $storeManager->getStore('test'); + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => '1', + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_NOT_VISIBLE ], + 'product_ids' => [$product->getId()] + ] + ); + + $actual = $this->getActualResults($productFilter); + $this->assertCount(0, $actual); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php + * @magentoAppIsolation enabled + */ + public function testMakeProductVisibleViaMassAction() + { + /** @var \Magento\Catalog\Model\Product $product*/ + $product = $this->productRepository->get('product1'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(0); + + $testStore = $storeManager->getStore('test'); + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + $actual = $this->getActualResults($productFilter); + $this->assertCount(0, $actual); + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH ], + 'product_ids' => [$product->getId()] + ] + ); + + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => '1', + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/products_invisible.php + * @magentoAppIsolation enabled + */ + public function testErrorOnDuplicatedUrlKey() + { + $skus = ['product1', 'product2']; + foreach ($skus as $sku) { + /** @var \Magento\Catalog\Model\Product $product */ + $productIds[] = $this->productRepository->get($sku)->getId(); + } + $this->expectException(UrlAlreadyExistsException::class); + $this->expectExceptionMessage('Can not change the visibility of the product with SKU equals "product2". ' + . 'URL key "product-1" for specified store already exists.'); + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH ], + 'product_ids' => $productIds + ] + ); + } + + /** + * @param array $filter + * @return array + */ + private function getActualResults(array $filter) + { + /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $actualResults = []; + foreach ($urlFinder->findAllByData($filter) as $url) { + $actualResults[] = [ + 'request_path' => $url->getRequestPath(), + 'target_path' => $url->getTargetPath(), + 'is_auto_generated' => (int)$url->getIsAutogenerated(), + 'redirect_type' => $url->getRedirectType(), + 'store_id' => $url->getStoreId() + ]; + } + return $actualResults; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php new file mode 100644 index 0000000000000..d50b29383b1ef --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +\Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); + +require __DIR__ . '/../../Store/_files/store.php'; + +/** @var $installer CategorySetup */ +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$storeManager = $objectManager->get(StoreManagerInterface::class); +$storeManager->setCurrentStore(0); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setStoreId(0) + ->setWebsiteIds([1]) + ->setName('Product1') + ->setSku('product1') + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setUrlKey('product-1') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php new file mode 100644 index 0000000000000..bcf399cb5e552 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('product1', true); + if ($product->getId()) { + $productRepository->delete($product); + } +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/store_rollback.php'; +require __DIR__ . '/../../Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php new file mode 100644 index 0000000000000..c72d9c8284db3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +\Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); + +/** @var $installer CategorySetup */ +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$skus = ['product1', 'product2']; +foreach ($skus as $sku) { + /** @var $product \Magento\Catalog\Model\Product */ + $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setStoreId(0) + ->setWebsiteIds([1]) + ->setName('Product1') + ->setSku($sku) + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setUrlKey('product-1') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php new file mode 100644 index 0000000000000..d3d17542aa6ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$skus = ['product1', 'product2']; +foreach ($skus as $sku) { + try { + $product = $productRepository->get($sku, true); + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (NoSuchEntityException $e) { + } +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/store_rollback.php'; +require __DIR__ . '/../../Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php index 994076badddae..60ccdb88676aa 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php @@ -22,6 +22,7 @@ class ResetQuoteAddressesTest extends \PHPUnit\Framework\TestCase /** * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php * + * @magentoAppArea frontend * @return void */ public function testAfterRemoveItem(): void diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php index 6bb7d6ac568fc..a3da32e0d6c40 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php @@ -367,4 +367,18 @@ public function dateDataProvider() [['from' => '2000-02-01T00:00:00Z', 'to' => ''], 0], ]; } + + public function filterByAttributeValuesDataProvider() + { + $variations = parent::filterByAttributeValuesDataProvider(); + + $variations['quick search by date'] = [ + 'quick_search_container', + [ + 'search_term' => '2000-10-30', + ], + ]; + + return $variations; + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php new file mode 100644 index 0000000000000..e64b3c505acf1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +/** + * \Magento\Framework\Lock\Backend\File test case + */ +class FileLockTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Lock\Backend\FileLock + */ + private $model; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + \Magento\Framework\Lock\Backend\FileLock::class, + ['path' => '/tmp'] + ); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php new file mode 100644 index 0000000000000..8d0caad5d55e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\App\DeploymentConfig\FileReader; +use Magento\Framework\Stdlib\ArrayManager; + +/** + * \Magento\Framework\Lock\Backend\Zookeeper test case + */ +class ZookeeperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FileReader + */ + private $configReader; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var LockBackendFactory + */ + private $lockBackendFactory; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var ZookeeperLock + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + if (!extension_loaded('zookeeper')) { + $this->markTestSkipped('php extension Zookeeper is not installed.'); + } + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->configReader = $this->objectManager->get(FileReader::class); + $this->lockBackendFactory = $this->objectManager->create(LockBackendFactory::class); + $this->arrayManager = $this->objectManager->create(ArrayManager::class); + $config = $this->configReader->load(ConfigFilePool::APP_ENV); + + if ($this->arrayManager->get('lock/provider', $config) !== 'zookeeper') { + $this->markTestSkipped('Zookeeper is not configured during installation.'); + } + + $this->model = $this->lockBackendFactory->create(); + $this->assertInstanceOf(ZookeeperLock::class, $this->model); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php index b09af48b5f943..f4f3337a253c0 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php @@ -20,6 +20,10 @@ CategorySetup::class, ['resourceName' => 'catalog_setup'] ); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); + $selectOptions = []; $selectAttributes = []; foreach (range(1, 2) as $index) { @@ -30,7 +34,7 @@ $selectAttribute->setData( [ 'attribute_code' => 'select_attribute_' . $index, - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'entity_type_id' => $productEntityTypeId, 'is_global' => 1, 'is_user_defined' => 1, 'frontend_input' => 'select', @@ -56,7 +60,8 @@ ); $selectAttribute->save(); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $selectAttribute->getId()); + $installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $selectAttribute->getId()); + /** @var $selectOptions Collection */ $selectOption = Bootstrap::getObjectManager()->create( Collection::class @@ -65,6 +70,26 @@ $selectAttributes[$index] = $selectAttribute; $selectOptions[$index] = $selectOption; } + +$dateAttribute = Bootstrap::getObjectManager()->create(Attribute::class); +$dateAttribute->setData( + [ + 'attribute_code' => 'date_attribute', + 'entity_type_id' => $productEntityTypeId, + 'is_global' => 1, + 'is_filterable' => 1, + 'backend_type' => 'datetime', + 'frontend_input' => 'date', + 'frontend_label' => 'Test Date', + 'is_searchable' => 1, + 'is_filterable_in_search' => 1, + ] +); +$dateAttribute->save(); +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $dateAttribute->getId()); + +$productAttributeSetId = $installer->getAttributeSetId($productEntityTypeId, 'Default'); /* Create simple products per each first attribute option */ foreach ($selectOptions[1] as $option) { /** @var $product Product */ @@ -74,7 +99,7 @@ $product->setTypeId( Type::TYPE_SIMPLE )->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') + $productAttributeSetId )->setWebsiteIds( [1] )->setName( @@ -92,6 +117,7 @@ )->setStockData( ['use_config_manage_stock' => 1, 'qty' => 5, 'is_in_stock' => 1] )->save(); + Bootstrap::getObjectManager()->get( Action::class )->updateAttributes( @@ -99,6 +125,7 @@ [ $selectAttributes[1]->getAttributeCode() => $option->getId(), $selectAttributes[2]->getAttributeCode() => $selectOptions[2]->getLastItem()->getId(), + $dateAttribute->getAttributeCode() => '10/30/2000', ], $product->getStoreId() ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php index 18a5372d06d98..fd413726b2637 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php @@ -13,6 +13,7 @@ $registry = Bootstrap::getObjectManager()->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); + /** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $productCollection = Bootstrap::getObjectManager() ->create(Product::class) @@ -20,17 +21,26 @@ foreach ($productCollection as $product) { $product->delete(); } + /** @var $attribute Attribute */ $attribute = Bootstrap::getObjectManager()->create( Attribute::class ); /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); foreach (range(1, 2) as $index) { - $attribute->loadByCode($installer->getEntityTypeId('catalog_product'), 'select_attribute_' . $index); + $attribute->loadByCode($productEntityTypeId, 'select_attribute_' . $index); if ($attribute->getId()) { $attribute->delete(); } } +$attribute->loadByCode($productEntityTypeId, 'date_attribute'); +if ($attribute->getId()) { + $attribute->delete(); +} + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php new file mode 100644 index 0000000000000..d23381e33d436 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('simple'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php new file mode 100644 index 0000000000000..f597d0bffa7ce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('virtual-product'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php new file mode 100644 index 0000000000000..86174a7753f4e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CartManagementInterface $cartManagement */ +$cartManagement = Bootstrap::getObjectManager()->get(CartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$cartId = $cartManagement->createEmptyCartForCustomer(1); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('test_quote'); +$cartRepository->save($cart); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($cartId) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php new file mode 100644 index 0000000000000..0d5d5f067e35a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Payment/_files/disable_all_active_payment_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php similarity index 93% rename from dev/tests/integration/testsuite/Magento/Payment/_files/disable_all_active_payment_methods.php rename to dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php index ac811e67c580a..8e6d4b8f74b86 100644 --- a/dev/tests/integration/testsuite/Magento/Payment/_files/disable_all_active_payment_methods.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 declare(strict_types=1); use Magento\Config\Model\Config; diff --git a/dev/tests/integration/testsuite/Magento/Payment/_files/disable_all_active_payment_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php similarity index 92% rename from dev/tests/integration/testsuite/Magento/Payment/_files/disable_all_active_payment_methods_rollback.php rename to dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php index 4a56888058e4d..092826d1fd3f7 100644 --- a/dev/tests/integration/testsuite/Magento/Payment/_files/disable_all_active_payment_methods_rollback.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 declare(strict_types=1); use Magento\Framework\App\Config\ScopeConfigInterface; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php new file mode 100644 index 0000000000000..9c15589ba82e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('payment/banktransfer/active', 1); +$configWriter->save('payment/cashondelivery/active', 1); +$configWriter->save('payment/checkmo/active', 1); +$configWriter->save('payment/purchaseorder/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php new file mode 100644 index 0000000000000..61b7ed9737ff9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('payment/banktransfer/active'); +$configWriter->delete('payment/cashondelivery/active'); +$configWriter->delete('payment/checkmo/active'); +$configWriter->delete('payment/purchaseorder/active'); diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/enable_offline_shipping_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php similarity index 89% rename from dev/tests/integration/testsuite/Magento/OfflineShipping/_files/enable_offline_shipping_methods.php rename to dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php index 08694bf8c7d0b..ebc41da9b1b3c 100644 --- a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/enable_offline_shipping_methods.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 declare(strict_types=1); use Magento\Framework\App\Config\Storage\Writer; diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/enable_offline_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php similarity index 86% rename from dev/tests/integration/testsuite/Magento/OfflineShipping/_files/enable_offline_shipping_methods_rollback.php rename to dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php index 31a16f42adfce..384ffbdf51f3c 100644 --- a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/enable_offline_shipping_methods_rollback.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 declare(strict_types=1); use Magento\Framework\App\Config\Storage\Writer; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php new file mode 100644 index 0000000000000..6a9ed898c3161 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var GuestCartManagementInterface $guestCartManagement */ +$guestCartManagement = Bootstrap::getObjectManager()->get(GuestCartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId */ +$maskedQuoteIdToQuoteId = Bootstrap::getObjectManager()->get(MaskedQuoteIdToQuoteIdInterface::class); + +$cartHash = $guestCartManagement->createEmptyCart(); +$cartId = $maskedQuoteIdToQuoteId->execute($cartHash); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('test_quote'); +$cartRepository->save($cart); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php new file mode 100644 index 0000000000000..0d5d5f067e35a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php new file mode 100644 index 0000000000000..b5704f82879b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->setIsActive(false); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php new file mode 100644 index 0000000000000..a973a982b1150 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\OfflinePayments\Model\Checkmo; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Api\Data\PaymentInterfaceFactory; +use Magento\Quote\Api\PaymentMethodManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var PaymentInterfaceFactory $paymentFactory */ +$paymentFactory = Bootstrap::getObjectManager()->get(PaymentInterfaceFactory::class); +/** @var PaymentMethodManagementInterface $paymentMethodManagement */ +$paymentMethodManagement = Bootstrap::getObjectManager()->get(PaymentMethodManagementInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + +$payment = $paymentFactory->create([ + 'data' => [ + PaymentInterface::KEY_METHOD => Checkmo::PAYMENT_METHOD_CHECKMO_CODE, + ] +]); +$paymentMethodManagement->set($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php new file mode 100644 index 0000000000000..e17b9e61f82db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 75477, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$shippingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php new file mode 100644 index 0000000000000..48d3356525f49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Block\Adminhtml\Subscriber; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + * @see \Magento\Newsletter\Block\Adminhtml\Subscriber\Grid + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var null|\Magento\Framework\ObjectManagerInterface + */ + private $objectManager = null; + /** + * @var null|\Magento\Framework\View\LayoutInterface + */ + private $layout = null; + + /** + * Set up layout. + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->layout = $this->objectManager->create(\Magento\Framework\View\LayoutInterface::class); + $this->layout->getUpdate()->load('newsletter_subscriber_grid'); + $this->layout->generateXml(); + $this->layout->generateElements(); + } + + /** + * Check if mass action block exists. + */ + public function testMassActionBlockExists() + { + $this->assertNotFalse( + $this->getMassActionBlock(), + 'Mass action block does not exist in the grid, or it name was changed.' + ); + } + + /** + * Check if mass action id field is correct. + */ + public function testMassActionFieldIdIsCorrect() + { + $this->assertEquals( + 'subscriber_id', + $this->getMassActionBlock()->getMassactionIdField(), + 'Mass action id field is incorrect.' + ); + } + + /** + * Check if function returns correct result. + * + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testMassActionBlockContainsCorrectIdList() + { + $this->assertEquals( + implode(',', $this->getAllSubscriberIdList()), + $this->getMassActionBlock()->getGridIdsJson(), + 'Function returns incorrect result.' + ); + } + + /** + * Retrieve mass action block. + * + * @return bool|\Magento\Backend\Block\Widget\Grid\Massaction + */ + private function getMassActionBlock() + { + return $this->layout->getBlock('adminhtml.newslettrer.subscriber.grid.massaction'); + } + + /** + * Retrieve list of id of all subscribers. + * + * @return array + */ + private function getAllSubscriberIdList() + { + /** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ + $resourceConnection = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + $select = $resourceConnection->getConnection() + ->select() + ->from($resourceConnection->getTableName('newsletter_subscriber')) + ->columns(['subscriber_id' => 'subscriber_id']); + + return $resourceConnection->getConnection()->fetchCol($select); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index b9ba89ba53144..2d0020ba22680 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -48,7 +48,22 @@ public static function loadByPhraseDataProvider() ['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0], ['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0] ] - ] + ], + [ + 'query_value', [] + ], + [ + 'query_value+', [] + ], + [ + 'query_value-', [] + ], + [ + 'query_@value', [] + ], + [ + 'query_value+@', [] + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/enable_ups_shipping_method.php b/dev/tests/integration/testsuite/Magento/Ups/_files/enable_ups_shipping_method.php new file mode 100644 index 0000000000000..5c6c60866fafb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/enable_ups_shipping_method.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/ups/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/enable_ups_shipping_method_rollback.php b/dev/tests/integration/testsuite/Magento/Ups/_files/enable_ups_shipping_method_rollback.php new file mode 100644 index 0000000000000..6d7894879f97b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/enable_ups_shipping_method_rollback.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/ups/active'); diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php new file mode 100644 index 0000000000000..b6055f14e79d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\UrlRewrite\Model; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrites.php + */ +class UrlFinderInterfaceTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var UrlFinderInterface + */ + private $urlFinder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->urlFinder = Bootstrap::getObjectManager()->create(UrlFinderInterface::class); + } + + /** + * @dataProvider findOneDataProvider + * @param string $requestPath + * @param string $targetPath + * @param int $redirectType + */ + public function testFindOneByData(string $requestPath, string $targetPath, int $redirectType) + { + $data = [ + UrlRewrite::REQUEST_PATH => $requestPath, + ]; + $urlRewrite = $this->urlFinder->findOneByData($data); + $this->assertEquals($targetPath, $urlRewrite->getTargetPath()); + $this->assertEquals($redirectType, $urlRewrite->getRedirectType()); + } + + /** + * @return array + */ + public function findOneDataProvider(): array + { + return [ + ['string', 'test_page1', 0], + ['string/', 'string', 301], + ['string_permanent', 'test_page1', 301], + ['string_permanent/', 'test_page1', 301], + ['string_temporary', 'test_page1', 302], + ['string_temporary/', 'test_page1', 302], + ['строка', 'test_page1', 0], + ['строка/', 'строка', 301], + [urlencode('строка'), 'test_page2', 0], + [urlencode('строка') . '/', urlencode('строка'), 301], + ['другая_строка', 'test_page1', 302], + ['другая_строка/', 'test_page1', 302], + [urlencode('другая_строка'), 'test_page1', 302], + [urlencode('другая_строка') . '/', 'test_page1', 302], + ['السلسلة', 'test_page1', 0], + [urlencode('السلسلة'), 'test_page1', 0], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php new file mode 100644 index 0000000000000..9edc6507308ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$rewritesData = [ + [ + 'string', 'test_page1', 0 + ], + [ + 'string_permanent', 'test_page1', \Magento\UrlRewrite\Model\OptionProvider::PERMANENT + ], + [ + 'string_temporary', 'test_page1', \Magento\UrlRewrite\Model\OptionProvider::TEMPORARY + ], + [ + 'строка', 'test_page1', 0 + ], + [ + urlencode('строка'), 'test_page2', 0 + ], + [ + 'другая_строка', 'test_page1', \Magento\UrlRewrite\Model\OptionProvider::TEMPORARY + ], + [ + 'السلسلة', 'test_page1', 0 + ], +]; + +$rewriteResource = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewrite::class); +foreach ($rewritesData as $rewriteData) { + list ($requestPath, $targetPath, $redirectType) = $rewriteData; + $rewrite = $objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); + $rewrite->setEntityType('custom') + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setRedirectType($redirectType); + $rewriteResource->save($rewrite); +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php new file mode 100644 index 0000000000000..a98f947d614e0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$urlRewriteCollection = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection::class); +$collection = $urlRewriteCollection + ->addFieldToFilter('target_path', ['test_page1', 'test_page2']) + ->load() + ->walk('delete'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php new file mode 100644 index 0000000000000..47705262caaf3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Controller; + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Area; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * @magentoAppIsolation enabled + */ +class ShareTest extends AbstractController +{ + /** + * Test share wishlist with correct data + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testSuccessfullyShareWishlist() + { + $this->login(1); + $this->prepareRequestData(); + $this->dispatch('wishlist/index/send/'); + + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Test share wishlist with incorrect data + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testShareWishlistWithoutEmails() + { + $this->login(1); + $this->prepareRequestData(true); + $this->dispatch('wishlist/index/send/'); + + $this->assertSessionMessages( + $this->equalTo(['Please enter an email address.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $session->loginById($customerId); + } + + /** + * Prepares the request with data + * + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $emails = !$invalidData ? 'email-1@example.com,email-2@example.com' : ''; + + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'emails' => $emails, + 'message' => '', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index 96854aa76281f..35ba5803b09cc 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt @@ -213,3 +213,4 @@ Magento/CatalogSearch/Model/ResourceModel/Fulltext Magento/Elasticsearch/Model/Layer/Search Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver Magento/Elasticsearch6/Model/Client +Magento/Config/App/Config/Type diff --git a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php new file mode 100644 index 0000000000000..216d8e9a0a01b --- /dev/null +++ b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Cache; + +use Magento\Framework\Lock\LockManagerInterface; + +/** + * Default mutex that provide concurrent access to cache storage. + */ +class LockGuardedCacheLoader +{ + /** + * @var LockManagerInterface + */ + private $locker; + + /** + * Lifetime of the lock for write in cache. + * + * Value of the variable in milliseconds. + * + * @var int + */ + private $lockTimeout; + + /** + * Timeout between retrieves to load the configuration from the cache. + * + * Value of the variable in milliseconds. + * + * @var int + */ + private $delayTimeout; + + /** + * @param LockManagerInterface $locker + * @param int $lockTimeout + * @param int $delayTimeout + */ + public function __construct( + LockManagerInterface $locker, + int $lockTimeout = 10000, + int $delayTimeout = 20 + ) { + $this->locker = $locker; + $this->lockTimeout = $lockTimeout; + $this->delayTimeout = $delayTimeout; + } + + /** + * Load data. + * + * @param string $lockName + * @param callable $dataLoader + * @param callable $dataCollector + * @param callable $dataSaver + * @return mixed + */ + public function lockedLoadData( + string $lockName, + callable $dataLoader, + callable $dataCollector, + callable $dataSaver + ) { + $cachedData = $dataLoader(); //optimistic read + + while ($cachedData === false && $this->locker->isLocked($lockName)) { + usleep($this->delayTimeout * 1000); + $cachedData = $dataLoader(); + } + + while ($cachedData === false) { + try { + if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + $data = $dataCollector(); + $dataSaver($data); + $cachedData = $data; + } + } finally { + $this->locker->unlock($lockName); + } + + if ($cachedData === false) { + usleep($this->delayTimeout * 1000); + $cachedData = $dataLoader(); + } + } + + return $cachedData; + } + + /** + * Clean data. + * + * @param string $lockName + * @param callable $dataCleaner + * @return void + */ + public function lockedCleanData(string $lockName, callable $dataCleaner) + { + while ($this->locker->isLocked($lockName)) { + usleep($this->delayTimeout * 1000); + } + try { + if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + $dataCleaner(); + } + } finally { + $this->locker->unlock($lockName); + } + } +} diff --git a/lib/internal/Magento/Framework/Locale/Format.php b/lib/internal/Magento/Framework/Locale/Format.php index ca50cdb2440f4..adcffe01b910e 100644 --- a/lib/internal/Magento/Framework/Locale/Format.php +++ b/lib/internal/Magento/Framework/Locale/Format.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Locale; +/** + * Price locale format. + */ class Format implements \Magento\Framework\Locale\FormatInterface { /** @@ -38,7 +41,8 @@ public function __construct( } /** - * Returns the first found number from a string + * Returns the first found number from a string. + * * Parsing depends on given locale (grouping and decimal) * * Examples for input: @@ -100,7 +104,7 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) } $formatter = new \NumberFormatter( - $localeCode . '@currency=' . $currency->getCode(), + $currency->getCode() ? $localeCode . '@currency=' . $currency->getCode() : $localeCode, \NumberFormatter::CURRENCY ); $format = $formatter->getPattern(); diff --git a/lib/internal/Magento/Framework/Locale/Resolver.php b/lib/internal/Magento/Framework/Locale/Resolver.php index b401da8960f05..d058bfd41ab1a 100644 --- a/lib/internal/Magento/Framework/Locale/Resolver.php +++ b/lib/internal/Magento/Framework/Locale/Resolver.php @@ -6,7 +6,12 @@ namespace Magento\Framework\Locale; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +/** + * Manages locale config information. + */ class Resolver implements ResolverInterface { /** @@ -52,26 +57,34 @@ class Resolver implements ResolverInterface */ private $defaultLocalePath; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * @param ScopeConfigInterface $scopeConfig * @param string $defaultLocalePath * @param string $scopeType * @param mixed $locale + * @param DeploymentConfig|null $deploymentConfig */ public function __construct( ScopeConfigInterface $scopeConfig, $defaultLocalePath, $scopeType, - $locale = null + $locale = null, + DeploymentConfig $deploymentConfig = null ) { $this->scopeConfig = $scopeConfig; $this->defaultLocalePath = $defaultLocalePath; $this->scopeType = $scopeType; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->create(DeploymentConfig::class); $this->setLocale($locale); } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocalePath() { @@ -79,7 +92,7 @@ public function getDefaultLocalePath() } /** - * {@inheritdoc} + * @inheritdoc */ public function setDefaultLocale($locale) { @@ -88,12 +101,15 @@ public function setDefaultLocale($locale) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocale() { if (!$this->defaultLocale) { - $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + $locale = false; + if ($this->deploymentConfig->isAvailable() && $this->deploymentConfig->isDbAvailable()) { + $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + } if (!$locale) { $locale = self::DEFAULT_LOCALE; } @@ -103,7 +119,7 @@ public function getDefaultLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function setLocale($locale = null) { @@ -116,7 +132,7 @@ public function setLocale($locale = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getLocale() { @@ -127,7 +143,7 @@ public function getLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function emulate($scopeId) { @@ -147,7 +163,7 @@ public function emulate($scopeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function revert() { diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php index 1141f451c13a5..73a029a5a1411 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php @@ -68,15 +68,17 @@ protected function setUp() /** * @param string $localeCode + * @param string $currencyCode * @param array $expectedResult * @dataProvider getPriceFormatDataProvider */ - public function testGetPriceFormat($localeCode, array $expectedResult): void + public function testGetPriceFormat($localeCode, $currencyCode, array $expectedResult): void { $this->scope->expects($this->once()) ->method('getCurrentCurrency') ->willReturn($this->currency); + $this->currency->method('getCode')->willReturn($currencyCode); $result = $this->formatModel->getPriceFormat($localeCode); $intersection = array_intersect_assoc($result, $expectedResult); $this->assertCount(count($expectedResult), $intersection); @@ -88,18 +90,19 @@ public function testGetPriceFormat($localeCode, array $expectedResult): void */ public function getPriceFormatDataProvider(): array { + $swissGroupSymbol = INTL_ICU_VERSION >= 59.1 ? '’' : '\''; return [ - ['en_US', ['decimalSymbol' => '.', 'groupSymbol' => ',']], - ['de_DE', ['decimalSymbol' => ',', 'groupSymbol' => '.']], - ['de_CH', ['decimalSymbol' => '.', 'groupSymbol' => '\'']], - ['uk_UA', ['decimalSymbol' => ',', 'groupSymbol' => ' ']] + ['en_US', 'USD', ['decimalSymbol' => '.', 'groupSymbol' => ',']], + ['de_DE', 'EUR', ['decimalSymbol' => ',', 'groupSymbol' => '.']], + ['de_CH', 'CHF', ['decimalSymbol' => '.', 'groupSymbol' => $swissGroupSymbol]], + ['uk_UA', 'UAH', ['decimalSymbol' => ',', 'groupSymbol' => ' ']] ]; } /** * - * @param mixed $value - * @param float $expected + * @param mixed $value + * @param float $expected * @dataProvider provideNumbers */ public function testGetNumber($value, $expected): void diff --git a/lib/internal/Magento/Framework/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php index 61818cbb8c53c..dfe6bbb828352 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Cache.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -14,6 +14,11 @@ */ class Cache implements \Magento\Framework\Lock\LockManagerInterface { + /** + * Prefix for marking that key is locked or not. + */ + const LOCK_PREFIX = 'LOCKED_RECORD_INFO_'; + /** * @var FrontendInterface */ @@ -26,12 +31,13 @@ public function __construct(FrontendInterface $cache) { $this->cache = $cache; } + /** * @inheritdoc */ public function lock(string $name, int $timeout = -1): bool { - return $this->cache->save('1', $name, [], $timeout); + return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); } /** @@ -39,7 +45,7 @@ public function lock(string $name, int $timeout = -1): bool */ public function unlock(string $name): bool { - return $this->cache->remove($name); + return $this->cache->remove($this->getIdentifier($name)); } /** @@ -47,6 +53,17 @@ public function unlock(string $name): bool */ public function isLocked(string $name): bool { - return (bool)$this->cache->test($name); + return (bool)$this->cache->test($this->getIdentifier($name)); + } + + /** + * Get cache locked identifier based on cache identifier. + * + * @param string $cacheIdentifier + * @return string + */ + private function getIdentifier(string $cacheIdentifier): string + { + return self::LOCK_PREFIX . $cacheIdentifier; } } diff --git a/lib/internal/Magento/Framework/Lock/Backend/FileLock.php b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php new file mode 100644 index 0000000000000..d168e910a4ab7 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php @@ -0,0 +1,194 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\Filesystem\Driver\File as FileDriver; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Phrase; + +/** + * LockManager using the file system for locks + */ +class FileLock implements LockManagerInterface +{ + /** + * The file driver instance + * + * @var FileDriver + */ + private $fileDriver; + + /** + * The path to the locks storage folder + * + * @var string + */ + private $path; + + /** + * How many microseconds to wait before re-try to acquire a lock + * + * @var int + */ + private $sleepCycle = 100000; + + /** + * The mapping list of the path lock with the file resource + * + * @var array + */ + private $locks = []; + + /** + * @param FileDriver $fileDriver The file driver + * @param string $path The path to the locks storage folder + * @throws RuntimeException Throws RuntimeException if $path is empty + * or cannot create the directory for locks + */ + public function __construct(FileDriver $fileDriver, string $path) + { + if (!$path) { + throw new RuntimeException(new Phrase('The path needs to be a non-empty string.')); + } + + $this->fileDriver = $fileDriver; + $this->path = rtrim($path, '/') . '/'; + + try { + if (!$this->fileDriver->isExists($this->path)) { + $this->fileDriver->createDirectory($this->path); + } + } catch (FileSystemException $exception) { + throw new RuntimeException( + new Phrase('Cannot create the directory for locks: %1', [$this->path]), + $exception + ); + } + } + + /** + * Acquires a lock by name + * + * @param string $name The lock name + * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout + * @return bool Returns true if the lock is acquired, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems + */ + public function lock(string $name, int $timeout = -1): bool + { + try { + $lockFile = $this->getLockPath($name); + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + $skipDeadline = $timeout < 0; + $deadline = microtime(true) + $timeout; + + while (!$this->tryToLock($fileResource)) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->fileDriver->fileClose($fileResource); + return false; + } + usleep($this->sleepCycle); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); + } + + $this->locks[$lockFile] = $fileResource; + return true; + } + + /** + * Checks if a lock exists by name + * + * @param string $name The lock name + * @return bool Returns true if the lock exists, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists + */ + public function isLocked(string $name): bool + { + $lockFile = $this->getLockPath($name); + $result = false; + + try { + if ($this->fileDriver->isExists($lockFile)) { + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + if ($this->tryToLock($fileResource)) { + $result = false; + } else { + $result = true; + } + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); + } + + return $result; + } + + /** + * Remove the lock by name + * + * @param string $name The lock name + * @return bool If the lock is removed returns true, otherwise returns false + */ + public function unlock(string $name): bool + { + $lockFile = $this->getLockPath($name); + + if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { + unset($this->locks[$lockFile]); + return true; + } + + return false; + } + + /** + * Returns the full path to the lock file by name + * + * @param string $name The lock name + * @return string The path to the lock file + */ + private function getLockPath(string $name): string + { + return $this->path . $name; + } + + /** + * Tries to lock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is acquired returns true, otherwise returns false + */ + private function tryToLock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } + + /** + * Tries to unlock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is removed returns true, otherwise returns false + */ + private function tryToUnlock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php new file mode 100644 index 0000000000000..cbba981ae1b51 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php @@ -0,0 +1,280 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Phrase; + +/** + * LockManager using the Zookeeper for locks + */ +class Zookeeper implements LockManagerInterface +{ + /** + * Zookeeper provider + * + * @var \Zookeeper + */ + private $zookeeper; + + /** + * The base path to locks in Zookeeper + * + * @var string + */ + private $path; + + /** + * The name of sequence nodes + * + * @var string + */ + private $lockName = 'lock-'; + + /** + * The host to connect to Zookeeper + * + * @var string + */ + private $host; + + /** + * How many seconds to wait before timing out on connections + * + * @var int + */ + private $connectionTimeout = 2; + + /** + * How many microseconds to wait before recheck connections or nodes + * + * @var int + */ + private $sleepCycle = 100000; + + /** + * The default permissions for Zookeeper nodes + * + * @var array + */ + private $acl = [['perms'=>\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; + + /** + * The mapping list of the lock name with the full lock path + * + * @var array + */ + private $locks = []; + + /** + * The default path to storage locks + */ + const DEFAULT_PATH = '/magento/locks'; + + /** + * @param string $host The host to connect to Zookeeper + * @param string $path The base path to locks in Zookeeper + * @throws RuntimeException + */ + public function __construct(string $host, string $path = self::DEFAULT_PATH) + { + if (!$path) { + throw new RuntimeException( + new Phrase('The path needs to be a non-empty string.') + ); + } + + if (!$host) { + throw new RuntimeException( + new Phrase('The host needs to be a non-empty string.') + ); + } + + $this->host = $host; + $this->path = rtrim($path, '/') . '/'; + } + + /** + * @inheritdoc + * + * You can see the lock algorithm by the link + * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + $skipDeadline = $timeout < 0; + $lockPath = $this->getFullPathToLock($name); + $deadline = microtime(true) + $timeout; + + if (!$this->checkAndCreateParentNode($lockPath)) { + throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); + } + + $lockKey = $this->getProvider() + ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); + + if (!$lockKey) { + throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); + } + + while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->getProvider()->delete($lockKey); + return false; + } + + usleep($this->sleepCycle); + } + + $this->locks[$name] = $lockKey; + + return true; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + if (!isset($this->locks[$name])) { + return false; + } + + return $this->getProvider()->delete($this->locks[$name]); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->isAnyLock($this->getFullPathToLock($name)); + } + + /** + * Gets full path to lock by its name + * + * @param string $name + * @return string + */ + private function getFullPathToLock(string $name): string + { + return $this->path . $name . '/' . $this->lockName; + } + + /** + * Initiolizes and returns Zookeeper provider + * + * @return \Zookeeper + * @throws RuntimeException + */ + private function getProvider(): \Zookeeper + { + if (!$this->zookeeper) { + $this->zookeeper = new \Zookeeper($this->host); + } + + $deadline = microtime(true) + $this->connectionTimeout; + while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { + if ($deadline <= microtime(true)) { + throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); + } + usleep($this->sleepCycle); + } + + return $this->zookeeper; + } + + /** + * Checks and creates base path recursively + * + * @param string $path + * @return bool + * @throws RuntimeException + */ + private function checkAndCreateParentNode(string $path): bool + { + $path = dirname($path); + if ($this->getProvider()->exists($path)) { + return true; + } + + if (!$this->checkAndCreateParentNode($path)) { + return false; + } + + if ($this->getProvider()->create($path, '1', $this->acl)) { + return true; + } + + return $this->getProvider()->exists($path); + } + + /** + * Gets int increment of lock key + * + * @param string $key + * @return int|null + */ + private function getIndex(string $key) + { + if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { + return null; + } + + return intval($matches[1]); + } + + /** + * Checks if there is any sequence node under parent of $fullKey. + * + * At first checks that the $fullKey node is present, if not - returns false. + * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, + * otherwise returns false. + * + * @param string $fullKey The full path without any sequence info + * @param int|null $indexKey The index to compare + * @return bool + * @throws RuntimeException + */ + private function isAnyLock(string $fullKey, int $indexKey = null): bool + { + $parent = dirname($fullKey); + + if (!$this->getProvider()->exists($parent)) { + return false; + } + + $children = $this->getProvider()->getChildren($parent); + + if (null === $indexKey && !empty($children)) { + return true; + } + + foreach ($children as $childKey) { + $childIndex = $this->getIndex($childKey); + + if (null === $childIndex) { + continue; + } + + if ($childIndex < $indexKey) { + return true; + } + } + + return false; + } +} diff --git a/lib/internal/Magento/Framework/Lock/LockBackendFactory.php b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php new file mode 100644 index 0000000000000..b142085ef6563 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock; + +use Magento\Framework\Phrase; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Lock\Backend\Database as DatabaseLock; +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\Backend\Cache as CacheLock; +use Magento\Framework\Lock\Backend\FileLock; + +/** + * The factory to create object that implements LockManagerInterface + */ +class LockBackendFactory +{ + /** + * The Object Manager instance + * + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * The Application deployment configuration + * + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * DB lock provider name + * + * @const string + */ + const LOCK_DB = 'db'; + + /** + * Zookeeper lock provider name + * + * @const string + */ + const LOCK_ZOOKEEPER = 'zookeeper'; + + /** + * Cache lock provider name + * + * @const string + */ + const LOCK_CACHE = 'cache'; + + /** + * File lock provider name + * + * @const string + */ + const LOCK_FILE = 'file'; + + /** + * The list of lock providers with mapping on classes + * + * @var array + */ + private $lockers = [ + self::LOCK_DB => DatabaseLock::class, + self::LOCK_ZOOKEEPER => ZookeeperLock::class, + self::LOCK_CACHE => CacheLock::class, + self::LOCK_FILE => FileLock::class, + ]; + + /** + * @param ObjectManagerInterface $objectManager The Object Manager instance + * @param DeploymentConfig $deploymentConfig The Application deployment configuration + */ + public function __construct( + ObjectManagerInterface $objectManager, + DeploymentConfig $deploymentConfig + ) { + $this->objectManager = $objectManager; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Creates an instance of LockManagerInterface using information from deployment config + * + * @return LockManagerInterface + * @throws RuntimeException + */ + public function create(): LockManagerInterface + { + $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); + $config = $this->deploymentConfig->get('lock/config', []); + + if (!isset($this->lockers[$provider])) { + throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); + } + + if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { + throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); + } + + return $this->objectManager->create($this->lockers[$provider], $config); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Proxy.php b/lib/internal/Magento/Framework/Lock/Proxy.php new file mode 100644 index 0000000000000..2718bf6cb3456 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Proxy.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock; + +use Magento\Framework\Exception\RuntimeException; + +/** + * Proxy for LockManagers + */ +class Proxy implements LockManagerInterface +{ + /** + * The factory to create LockManagerInterface implementation + * + * @var LockBackendFactory + */ + private $factory; + + /** + * A LockManagerInterface implementation + * + * @var LockManagerInterface + */ + private $locker; + + /** + * @param LockBackendFactory $factory The factory to create LockManagerInterface implementation + */ + public function __construct(LockBackendFactory $factory) + { + $this->factory = $factory; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->getLocker()->isLocked($name); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->getLocker()->lock($name, $timeout); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + return $this->getLocker()->unlock($name); + } + + /** + * Gets LockManagerInterface implementation using Factory + * + * @return LockManagerInterface + * @throws RuntimeException + */ + private function getLocker(): LockManagerInterface + { + if (!$this->locker) { + $this->locker = $this->factory->create(); + } + + return $this->locker; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php new file mode 100644 index 0000000000000..62521b9de3082 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Test\Unit\Backend; + +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class ZookeeperTest extends TestCase +{ + /** + * @var ZookeeperProvider + */ + private $zookeeperProvider; + + /** + * @var string + */ + private $host = 'localhost:123'; + + /** + * @var string + */ + private $path = '/some/path'; + + /** + * @inheritdoc + */ + protected function setUp() + { + if (!extension_loaded('zookeeper')) { + $this->markTestSkipped('Test was skipped because php extension Zookeeper is not installed.'); + } + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The path needs to be a non-empty string. + * @return void + */ + public function testConstructionWithPathException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, ''); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The host needs to be a non-empty string. + * @return void + */ + public function testConstructionWithHostException() + { + $this->zookeeperProvider = new ZookeeperProvider('', $this->path); + } + + /** + * @return void + */ + public function testConstructionWithoutException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, $this->path); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php new file mode 100644 index 0000000000000..ebf2f54f3e093 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Test\Unit; + +use Magento\Framework\Lock\Backend\Database as DatabaseLock; +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\Backend\Cache as CacheLock; +use Magento\Framework\Lock\Backend\FileLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\App\DeploymentConfig; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class LockBackendFactoryTest extends TestCase +{ + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfigMock; + + /** + * @var LockBackendFactory + */ + private $factory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->factory = new LockBackendFactory($this->objectManagerMock, $this->deploymentConfigMock); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage Unknown locks provider: someProvider + */ + public function testCreateWithException() + { + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls('someProvider', []); + + $this->factory->create(); + } + + /** + * @param string $lockProvider + * @param string $lockProviderClass + * @param array $config + * @dataProvider createDataProvider + */ + public function testCreate(string $lockProvider, string $lockProviderClass, array $config) + { + $lockManagerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls($lockProvider, $config); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with($lockProviderClass, $config) + ->willReturn($lockManagerMock); + + $this->assertSame($lockManagerMock, $this->factory->create()); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + $data = [ + 'db' => [ + 'lockProvider' => LockBackendFactory::LOCK_DB, + 'lockProviderClass' => DatabaseLock::class, + 'config' => ['prefix' => 'somePrefix'], + ], + 'cache' => [ + 'lockProvider' => LockBackendFactory::LOCK_CACHE, + 'lockProviderClass' => CacheLock::class, + 'config' => [], + ], + 'file' => [ + 'lockProvider' => LockBackendFactory::LOCK_FILE, + 'lockProviderClass' => FileLock::class, + 'config' => ['path' => '/my/path'], + ], + ]; + + if (extension_loaded('zookeeper')) { + $data['zookeeper'] = [ + 'lockProvider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'lockProviderClass' => ZookeeperLock::class, + 'config' => ['host' => 'some host'], + ]; + } + + return $data; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php new file mode 100644 index 0000000000000..c71dad701d715 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Test\Unit; + +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\Lock\Proxy; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\Lock\LockManagerInterface; + +/** + * @inheritdoc + */ +class ProxyTest extends TestCase +{ + /** + * @var LockBackendFactory|MockObject + */ + private $factoryMock; + + /** + * @var LockManagerInterface|MockObject + */ + private $lockerMock; + + /** + * @var Proxy + */ + private $proxy; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->factoryMock = $this->createMock(LockBackendFactory::class); + $this->lockerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->proxy = new Proxy($this->factoryMock); + } + + /** + * @return void + */ + public function testIsLocked() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('isLocked') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->isLocked($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->isLocked($lockName)); + } + + /** + * @return void + */ + public function testLock() + { + $lockName = 'testLock'; + $timeout = 123; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('lock') + ->with($lockName, $timeout) + ->willReturn(true); + + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + } + + /** + * @return void + */ + public function testUnlock() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('unlock') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->unlock($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->unlock($lockName)); + } +} diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php index cb80bc4becaec..48c33c48f12e6 100644 --- a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php @@ -9,7 +9,7 @@ /** * Class CallbackInvoker to invoke callbacks for consumer classes */ -class CallbackInvoker +class CallbackInvoker implements CallbackInvokerInterface { /** * Run short running process diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php new file mode 100644 index 0000000000000..36658f2e4eebe --- /dev/null +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\MessageQueue; + +/** + * Callback invoker interface + */ +interface CallbackInvokerInterface +{ + /** + * Run short running process + * + * @param QueueInterface $queue + * @param int $maxNumberOfMessages + * @param \Closure $callback + * @return void + */ + public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback); +} diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer.php b/lib/internal/Magento/Framework/MessageQueue/Consumer.php index 9bdacfbc1ff6c..8f65a2d8c5ed2 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer.php @@ -38,7 +38,7 @@ class Consumer implements ConsumerInterface private $messageEncoder; /** - * @var CallbackInvoker + * @var CallbackInvokerInterface */ private $invoker; @@ -80,7 +80,7 @@ class Consumer implements ConsumerInterface /** * Initialize dependencies. * - * @param CallbackInvoker $invoker + * @param CallbackInvokerInterface $invoker * @param MessageEncoder $messageEncoder * @param ResourceConnection $resource * @param ConsumerConfigurationInterface $configuration @@ -89,7 +89,7 @@ class Consumer implements ConsumerInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - CallbackInvoker $invoker, + CallbackInvokerInterface $invoker, MessageEncoder $messageEncoder, ResourceConnection $resource, ConsumerConfigurationInterface $configuration, @@ -103,7 +103,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function process($maxNumberOfMessages = null) { diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php index 5e64d737034c8..73841a2a964cc 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php @@ -75,7 +75,7 @@ public function testProcess() ->getMockForAbstractClass(); $configuration->expects($this->atLeastOnce())->method('getHandlers')->willReturn([]); $this->messageStatusProcessor->expects($this->exactly(2))->method('acknowledgeMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) @@ -116,7 +116,7 @@ public function testProcessWithConnectionLostException() $exception = new \Magento\Framework\MessageQueue\ConnectionLostException(__('Exception Message')); $configuration->expects($this->atLeastOnce())->method('getHandlers')->willThrowException($exception); $this->messageStatusProcessor->expects($this->once())->method('acknowledgeMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) @@ -158,7 +158,7 @@ public function testProcessWithException() $configuration->expects($this->atLeastOnce())->method('getHandlers')->willThrowException($exception); $this->messageStatusProcessor->expects($this->once())->method('acknowledgeMessages'); $this->messageStatusProcessor->expects($this->atLeastOnce())->method('rejectMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) diff --git a/lib/internal/Magento/Framework/Oauth/Oauth.php b/lib/internal/Magento/Framework/Oauth/Oauth.php index 5e48fb5ed30f9..919b0e4c86ba0 100644 --- a/lib/internal/Magento/Framework/Oauth/Oauth.php +++ b/lib/internal/Magento/Framework/Oauth/Oauth.php @@ -9,6 +9,9 @@ use Magento\Framework\Encryption\Helper\Security; use Magento\Framework\Phrase; +/** + * Authorization service. + */ class Oauth implements OauthInterface { /** @@ -61,7 +64,7 @@ public static function getSupportedSignatureMethods() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRequestToken($params, $requestUrl, $httpMethod = 'POST') { @@ -74,7 +77,7 @@ public function getRequestToken($params, $requestUrl, $httpMethod = 'POST') } /** - * {@inheritdoc} + * @inheritdoc */ public function getAccessToken($params, $requestUrl, $httpMethod = 'POST') { @@ -102,7 +105,7 @@ public function getAccessToken($params, $requestUrl, $httpMethod = 'POST') } /** - * {@inheritdoc} + * @inheritdoc */ public function validateAccessTokenRequest($params, $requestUrl, $httpMethod = 'POST') { @@ -125,7 +128,7 @@ public function validateAccessTokenRequest($params, $requestUrl, $httpMethod = ' } /** - * {@inheritdoc} + * @inheritdoc */ public function validateAccessToken($accessToken) { @@ -133,7 +136,7 @@ public function validateAccessToken($accessToken) } /** - * {@inheritdoc} + * @inheritdoc */ public function buildAuthorizationHeader( $params, @@ -199,7 +202,7 @@ protected function _validateSignature($params, $consumerSecret, $httpMethod, $re ); if (!Security::compareStrings($calculatedSign, $params['oauth_signature'])) { - throw new Exception(new Phrase('The signatire is invalid. Verify and try again.')); + throw new Exception(new Phrase('The signature is invalid. Verify and try again.')); } } diff --git a/lib/internal/Magento/Framework/Setup/OldDbValidator.php b/lib/internal/Magento/Framework/Setup/OldDbValidator.php index 4c224a6c713ef..018b010e8fe4a 100644 --- a/lib/internal/Magento/Framework/Setup/OldDbValidator.php +++ b/lib/internal/Magento/Framework/Setup/OldDbValidator.php @@ -13,7 +13,7 @@ /** * Old Validator for database * - * Used in order to support backward compatability of modules that are installed + * Used in order to support backward compatibility of modules that are installed * in old way (with Install/Upgrade Schema/Data scripts) */ class OldDbValidator implements UpToDateValidatorInterface diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index 335006555d2f1..6c4746d8218ea 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\View\Element; +use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\App\ObjectManager; /** * Base class for all blocks. @@ -175,14 +178,23 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl */ protected $_cache; + /** + * @var LockGuardedCacheLoader + */ + private $lockQuery; + /** * Constructor * * @param \Magento\Framework\View\Element\Context $context * @param array $data + * @param LockGuardedCacheLoader|null $lockQuery */ - public function __construct(\Magento\Framework\View\Element\Context $context, array $data = []) - { + public function __construct( + \Magento\Framework\View\Element\Context $context, + array $data = [], + LockGuardedCacheLoader $lockQuery = null + ) { $this->_request = $context->getRequest(); $this->_layout = $context->getLayout(); $this->_eventManager = $context->getEventManager(); @@ -204,6 +216,8 @@ public function __construct(\Magento\Framework\View\Element\Context $context, ar $this->jsLayout = $data['jsLayout']; unset($data['jsLayout']); } + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); parent::__construct($data); $this->_construct(); } @@ -658,19 +672,6 @@ public function toHtml() } $html = $this->_loadCache(); - if ($html === false) { - if ($this->hasData('translate_inline')) { - $this->inlineTranslation->suspend($this->getData('translate_inline')); - } - - $this->_beforeToHtml(); - $html = $this->_toHtml(); - $this->_saveCache($html); - - if ($this->hasData('translate_inline')) { - $this->inlineTranslation->resume(); - } - } $html = $this->_afterToHtml($html); /** @var \Magento\Framework\DataObject */ @@ -1083,23 +1084,54 @@ protected function getCacheLifetime() /** * Load block html from cache storage * - * @return string|false + * @return string */ protected function _loadCache() { + $collectAction = function () { + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->suspend($this->getData('translate_inline')); + } + + $this->_beforeToHtml(); + return $this->_toHtml(); + }; + if ($this->getCacheLifetime() === null || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) { - return false; - } - $cacheKey = $this->getCacheKey(); - $cacheData = $this->_cache->load($cacheKey); - if ($cacheData) { - $cacheData = str_replace( - $this->_getSidPlaceholder($cacheKey), - $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), - $cacheData - ); + $html = $collectAction(); + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->resume(); + } + return $html; } - return $cacheData; + $loadAction = function () { + $cacheKey = $this->getCacheKey(); + $cacheData = $this->_cache->load($cacheKey); + if ($cacheData) { + $cacheData = str_replace( + $this->_getSidPlaceholder($cacheKey), + $this->_sidResolver->getSessionIdQueryParam($this->_session) + . '=' + . $this->_session->getSessionId(), + $cacheData + ); + } + return $cacheData; + }; + + $saveAction = function ($data) { + $this->_saveCache($data); + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->resume(); + } + }; + + return (string)$this->lockQuery->lockedLoadData( + $this->getCacheKey(), + $loadAction, + $collectAction, + $saveAction + ); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php index 5f7508438a6ed..dba775ea894f4 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php @@ -6,6 +6,7 @@ namespace Magento\Framework\View\Test\Unit\Element; +use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\Context; use Magento\Framework\Config\View; @@ -13,7 +14,6 @@ use Magento\Framework\Event\ManagerInterface as EventManagerInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Cache\StateInterface as CacheStateInterface; -use Magento\Framework\App\CacheInterface; use Magento\Framework\Session\SidResolverInterface; use Magento\Framework\Session\SessionManagerInterface; @@ -42,11 +42,6 @@ class AbstractBlockTest extends \PHPUnit\Framework\TestCase */ private $cacheStateMock; - /** - * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $cacheMock; - /** * @var SidResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -57,6 +52,11 @@ class AbstractBlockTest extends \PHPUnit\Framework\TestCase */ private $sessionMock; + /** + * @var LockGuardedCacheLoader|\PHPUnit_Framework_MockObject_MockObject + */ + private $lockQuery; + /** * @return void */ @@ -65,7 +65,10 @@ protected function setUp() $this->eventManagerMock = $this->getMockForAbstractClass(EventManagerInterface::class); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->cacheStateMock = $this->getMockForAbstractClass(CacheStateInterface::class); - $this->cacheMock = $this->getMockForAbstractClass(CacheInterface::class); + $this->lockQuery = $this->getMockBuilder(LockGuardedCacheLoader::class) + ->disableOriginalConstructor() + ->setMethods(['lockedLoadData']) + ->getMockForAbstractClass(); $this->sidResolverMock = $this->getMockForAbstractClass(SidResolverInterface::class); $this->sessionMock = $this->getMockForAbstractClass(SessionManagerInterface::class); $contextMock = $this->createMock(Context::class); @@ -78,9 +81,6 @@ protected function setUp() $contextMock->expects($this->once()) ->method('getCacheState') ->willReturn($this->cacheStateMock); - $contextMock->expects($this->once()) - ->method('getCache') - ->willReturn($this->cacheMock); $contextMock->expects($this->once()) ->method('getSidResolver') ->willReturn($this->sidResolverMock); @@ -89,7 +89,11 @@ protected function setUp() ->willReturn($this->sessionMock); $this->block = $this->getMockForAbstractClass( AbstractBlock::class, - ['context' => $contextMock] + [ + 'context' => $contextMock, + 'data' => [], + 'lockQuery' => $this->lockQuery + ] ); } @@ -219,10 +223,7 @@ public function testToHtmlWhenModuleIsDisabled() /** * @param string|bool $cacheLifetime * @param string|bool $dataFromCache - * @param string $dataForSaveCache * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectsDispatchEvent - * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectsCacheLoad - * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectsCacheSave * @param string $expectedResult * @return void * @dataProvider getCacheLifetimeDataProvider @@ -230,10 +231,7 @@ public function testToHtmlWhenModuleIsDisabled() public function testGetCacheLifetimeViaToHtml( $cacheLifetime, $dataFromCache, - $dataForSaveCache, $expectsDispatchEvent, - $expectsCacheLoad, - $expectsCacheSave, $expectedResult ) { $moduleName = 'Test'; @@ -252,13 +250,9 @@ public function testGetCacheLifetimeViaToHtml( ->method('isEnabled') ->with(AbstractBlock::CACHE_GROUP) ->willReturn(true); - $this->cacheMock->expects($expectsCacheLoad) - ->method('load') - ->with(AbstractBlock::CACHE_KEY_PREFIX . $cacheKey) + $this->lockQuery->expects($this->any()) + ->method('lockedLoadData') ->willReturn($dataFromCache); - $this->cacheMock->expects($expectsCacheSave) - ->method('save') - ->with($dataForSaveCache, AbstractBlock::CACHE_KEY_PREFIX . $cacheKey); $this->sidResolverMock->expects($this->any()) ->method('getSessionIdQueryParam') ->with($this->sessionMock) @@ -279,46 +273,31 @@ public function getCacheLifetimeDataProvider() [ 'cacheLifetime' => null, 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->never(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => '', ], [ 'cacheLifetime' => false, 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->never(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => '', ], [ 'cacheLifetime' => 120, 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->once(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => 'dataFromCache', ], [ 'cacheLifetime' => '120string', 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->once(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => 'dataFromCache', ], [ 'cacheLifetime' => 120, 'dataFromCache' => false, - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->once(), - 'expectsCacheSave' => $this->once(), 'expectedResult' => '', ], ]; diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php index b911a38dbb488..4c76087bfea12 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\View\Test\Unit\Element\Html; class LinkTest extends \PHPUnit\Framework\TestCase { + private $objectManager; + + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + } + /** * @var array */ @@ -24,24 +32,8 @@ class LinkTest extends \PHPUnit\Framework\TestCase */ protected $link; - /** - * @param \Magento\Framework\View\Element\Html\Link $link - * @param string $expected - * - * @dataProvider getLinkAttributesDataProvider - */ - public function testGetLinkAttributes($link, $expected) - { - $this->assertEquals($expected, $link->getLinkAttributes()); - } - - /** - * @return array - */ - public function getLinkAttributesDataProvider() + public function testGetLinkAttributes() { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->setMethods(['escapeHtml'])->disableOriginalConstructor()->getMock(); @@ -54,13 +46,19 @@ public function getLinkAttributesDataProvider() $urlBuilderMock->expects($this->any()) ->method('getUrl') - ->will($this->returnArgument('http://site.com/link.html')); + ->willReturn('http://site.com/link.html'); $validtorMock = $this->getMockBuilder(\Magento\Framework\View\Element\Template\File\Validator::class) ->setMethods(['isValid'])->disableOriginalConstructor()->getMock(); + $validtorMock->expects($this->any()) + ->method('isValid') + ->willReturn(false); $scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config::class) ->setMethods(['isSetFlag'])->disableOriginalConstructor()->getMock(); + $scopeConfigMock->expects($this->any()) + ->method('isSetFlag') + ->willReturn(true); $resolverMock = $this->getMockBuilder(\Magento\Framework\View\Element\Template\File\Resolver::class) ->setMethods([])->disableOriginalConstructor()->getMock(); @@ -72,48 +70,48 @@ public function getLinkAttributesDataProvider() $contextMock->expects($this->any()) ->method('getValidator') - ->will($this->returnValue($validtorMock)); + ->willReturn($validtorMock); $contextMock->expects($this->any()) ->method('getResolver') - ->will($this->returnValue($resolverMock)); + ->willReturn($resolverMock); $contextMock->expects($this->any()) ->method('getEscaper') - ->will($this->returnValue($escaperMock)); + ->willReturn($escaperMock); $contextMock->expects($this->any()) ->method('getUrlBuilder') - ->will($this->returnValue($urlBuilderMock)); + ->willReturn($urlBuilderMock); $contextMock->expects($this->any()) ->method('getScopeConfig') - ->will($this->returnValue($scopeConfigMock)); + ->willReturn($scopeConfigMock); /** @var \Magento\Framework\View\Element\Html\Link $linkWithAttributes */ - $linkWithAttributes = $objectManagerHelper->getObject( + $linkWithAttributes = $this->objectManager->getObject( \Magento\Framework\View\Element\Html\Link::class, ['context' => $contextMock] ); + + $this->assertEquals( + 'href="http://site.com/link.html"', + $linkWithAttributes->getLinkAttributes() + ); + /** @var \Magento\Framework\View\Element\Html\Link $linkWithoutAttributes */ - $linkWithoutAttributes = $objectManagerHelper->getObject( + $linkWithoutAttributes = $this->objectManager->getObject( \Magento\Framework\View\Element\Html\Link::class, ['context' => $contextMock] ); - foreach ($this->allowedAttributes as $attribute) { - $linkWithAttributes->setDataUsingMethod($attribute, $attribute); + $linkWithoutAttributes->setDataUsingMethod($attribute, $attribute); } - return [ - 'full' => [ - 'link' => $linkWithAttributes, - 'expected' => 'shape="shape" tabindex="tabindex" onfocus="onfocus" onblur="onblur" id="id"', - ], - 'empty' => [ - 'link' => $linkWithoutAttributes, - 'expected' => '', - ], - ]; + $this->assertEquals( + 'href="http://site.com/link.html" shape="shape" tabindex="tabindex"' + . ' onfocus="onfocus" onblur="onblur" id="id"', + $linkWithoutAttributes->getLinkAttributes() + ); } } diff --git a/lib/web/css/source/lib/_resets.less b/lib/web/css/source/lib/_resets.less index 4499c314ce6ca..08d16842b849c 100644 --- a/lib/web/css/source/lib/_resets.less +++ b/lib/web/css/source/lib/_resets.less @@ -105,13 +105,6 @@ .lib-css(box-shadow, @focus__box-shadow); } } - - input[type="radio"], - input[type="checkbox"] { - &:focus { - box-shadow: none; - } - } } // diff --git a/lib/web/mage/adminhtml/globals.js b/lib/web/mage/adminhtml/globals.js index 12c97fdfcd2c5..683606e576497 100644 --- a/lib/web/mage/adminhtml/globals.js +++ b/lib/web/mage/adminhtml/globals.js @@ -12,7 +12,7 @@ define([ /** * Set of a temporary methods used to provide - * backward compatability with a legacy code. + * backward compatibility with a legacy code. */ window.setLocation = function (url) { window.location.href = url; diff --git a/lib/web/mage/backend/floating-header.js b/lib/web/mage/backend/floating-header.js index 06861277559a4..a6f767259488a 100644 --- a/lib/web/mage/backend/floating-header.js +++ b/lib/web/mage/backend/floating-header.js @@ -48,6 +48,7 @@ define([ this.element.wrapInner($('<div/>', { 'class': 'page-actions-inner', 'data-title': title })); + this.element.removeClass('floating-header'); }, /** diff --git a/lib/web/mage/gallery/gallery.js b/lib/web/mage/gallery/gallery.js index 15c3d01cf2be3..2ed3f8b0d620c 100644 --- a/lib/web/mage/gallery/gallery.js +++ b/lib/web/mage/gallery/gallery.js @@ -141,7 +141,7 @@ define([ this.setupBreakpoints(); this.initFullscreenSettings(); - this.settings.$element.on('mouseup', '.fotorama__stage__frame', function () { + this.settings.$element.on('mousedown', '.fotorama__stage__frame', function () { if ( !$(this).parents('.fotorama__shadows--left, .fotorama__shadows--right').length && !$(this).hasClass('fotorama-video-container') diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index dfa35473176b9..a57191fd4aff4 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1431,10 +1431,14 @@ ], 'validate-per-page-value-list': [ function (v) { - var isValid = !$.mage.isEmpty(v), + var isValid = true, values = v.split(','), i; + if ($.mage.isEmpty(v)) { + return isValid; + } + for (i = 0; i < values.length; i++) { if (!/^[0-9]+$/.test(values[i])) { isValid = false; diff --git a/nginx.conf.sample b/nginx.conf.sample index ce3891627bc8c..aef22cc55fbbe 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -159,6 +159,11 @@ location /media/downloadable/ { location /media/import/ { deny all; } +location /errors/ { + location ~* \.xml$ { + deny all; + } +} # PHP entry point for main application location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { @@ -198,6 +203,6 @@ gzip_types gzip_vary on; # Banned locations (only reached if the earlier PHP entry point regexes don't match) -location ~* (\.php$|\.htaccess$|\.git) { +location ~* (\.php$|\.phtml$|\.htaccess$|\.git) { deny all; } diff --git a/pub/errors/.htaccess b/pub/errors/.htaccess index 3692dd439e2ff..a7b9cbda05893 100644 --- a/pub/errors/.htaccess +++ b/pub/errors/.htaccess @@ -1,4 +1,7 @@ Options None +<FilesMatch "\.(xml|phtml)$"> + Deny from all +</FilesMatch> <IfModule mod_rewrite.c> RewriteEngine Off </IfModule> diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 765d0a616f77c..0e1860405e946 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -27267,843 +27267,6 @@ if (testLabel <stringProp name="ThreadGroup.delay"/> <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/thread_group.jmx</stringProp></ThreadGroup> <hashTree> - <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="Product Grid Mass Actions" enabled="true"> - <intProp name="ThroughputController.style">1</intProp> - <boolProp name="ThroughputController.perThread">false</boolProp> - <intProp name="ThroughputController.maxThroughput">1</intProp> - <stringProp name="ThroughputController.percentThroughput">${productGridMassActionPercentage}</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx</stringProp></ThroughputController> - <hashTree> - <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set Test Label" enabled="true"> - <stringProp name="script"> -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/setup_label.jmx</stringProp></JSR223PreProcessor> - <hashTree/> - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Set Label" enabled="true"> - <stringProp name="BeanShellSampler.query"> - vars.put("testLabel", "Product Grid Mass Actions"); - </stringProp> - <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> - </BeanShellSampler> - <hashTree/> - - <JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Get admin form key PostProcessor" enabled="true"> - <stringProp name="script"> - function getFormKeyFromResponse() - { - var url = prev.getUrlAsString(), - responseCode = prev.getResponseCode(), - formKey = null; - searchPattern = /var FORM_KEY = '(.+)'/; - if (responseCode == "200" && url) { - response = prev.getResponseDataAsString(); - formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; - } - return formKey; - } - - formKey = vars.get("form_key_storage"); - - currentFormKey = getFormKeyFromResponse(); - - if (currentFormKey != null && currentFormKey != formKey) { - vars.put("form_key_storage", currentFormKey); - } - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx</stringProp></JSR223PostProcessor> - <hashTree/> - <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set admin form key PreProcessor" enabled="true"> - <stringProp name="script"> - formKey = vars.get("form_key_storage"); - if (formKey - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' - && sampler.getMethod() == "POST") - { - arguments = sampler.getArguments(); - for (i=0; i<arguments.getArgumentCount(); i++) - { - argument = arguments.getArgument(i); - if (argument.getName() == 'form_key' && argument.getValue() != formKey) { - log.info("admin form key updated: " + argument.getValue() + " => " + formKey); - argument.setValue(formKey); - } - } - } - </stringProp> - <stringProp name="scriptLanguage">javascript</stringProp> - </JSR223PreProcessor> - <hashTree/> - - <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true"> - <collectionProp name="CookieManager.cookies"/> - <boolProp name="CookieManager.clearEachIteration">false</boolProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/http_cookie_manager_without_clear_each_iteration.jmx</stringProp></CookieManager> - <hashTree/> - - <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Admin Login" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/simple_controller.jmx</stringProp> -</GenericController> - <hashTree> - <CriticalSectionController guiclass="CriticalSectionControllerGui" testclass="CriticalSectionController" testname="Admin Login Lock" enabled="true"> - <stringProp name="CriticalSectionController.lockName">get-admin-email</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/lock_controller.jmx</stringProp></CriticalSectionController> - <hashTree> - - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Get Admin Email" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/get_admin_email.jmx</stringProp> - <stringProp name="BeanShellSampler.query"> -adminUserList = props.get("adminUserList"); -adminUserListIterator = props.get("adminUserListIterator"); -adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - -if (adminUsersDistribution == 1) { - adminUser = adminUserList.poll(); -} else { - if (!adminUserListIterator.hasNext()) { - adminUserListIterator = adminUserList.descendingIterator(); - } - - adminUser = adminUserListIterator.next(); -} - -if (adminUser == null) { - SampleResult.setResponseMessage("adminUser list is empty"); - SampleResult.setResponseData("adminUser list is empty","UTF-8"); - IsSuccess=false; - SampleResult.setSuccessful(false); - SampleResult.setStopThread(true); -} -vars.put("admin_user", adminUser); - </stringProp> - <stringProp name="BeanShellSampler.filename"/> - <stringProp name="BeanShellSampler.parameters"/> - <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> - </BeanShellSampler> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SetUp - Login" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_login.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert login form shown" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="-1397214398">Welcome</stringProp> - <stringProp name="-515240035"><title>Magento Admin</title></stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract form key" enabled="true"> - <stringProp name="RegexExtractor.useHeaders">false</stringProp> - <stringProp name="RegexExtractor.refname">admin_form_key</stringProp> - <stringProp name="RegexExtractor.regex"><input name="form_key" type="hidden" value="([^'"]+)" /></stringProp> - <stringProp name="RegexExtractor.template">$1$</stringProp> - <stringProp name="RegexExtractor.default"/> - <stringProp name="RegexExtractor.match_number">1</stringProp> - </RegexExtractor> - <hashTree/> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert form_key extracted" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="2845929">^.+$</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">1</intProp> - <stringProp name="Assertion.scope">variable</stringProp> - <stringProp name="Scope.variable">admin_form_key</stringProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SetUp - Login Submit Form" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="dummy" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">dummy</stringProp> - </elementProp> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - </elementProp> - <elementProp name="login[password]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_password}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">login[password]</stringProp> - </elementProp> - <elementProp name="login[username]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_user}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">login[username]</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/dashboard/</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <stringProp name="HTTPSampler.implementation">Java</stringProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_login_submit_form.jmx</stringProp> - </HTTPSamplerProxy> - <hashTree> - <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract form key" enabled="true"> - <stringProp name="RegexExtractor.useHeaders">false</stringProp> - <stringProp name="RegexExtractor.refname">admin_form_key</stringProp> - <stringProp name="RegexExtractor.regex"><input name="form_key" type="hidden" value="([^'"]+)" /></stringProp> - <stringProp name="RegexExtractor.template">$1$</stringProp> - <stringProp name="RegexExtractor.default"/> - <stringProp name="RegexExtractor.match_number">1</stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_login/admin_retrieve_form_key.jmx</stringProp></RegexExtractor> - <hashTree/> - </hashTree> - </hashTree> - - <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Simple Controller" enabled="true"> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/simple_controller.jmx</stringProp> -</GenericController> - <hashTree> - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Product Pages Count" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - </elementProp> - <elementProp name="namespace" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">product_listing</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">namespace</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="search" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">search</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="filters[placeholder]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">filters[placeholder]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="paging[pageSize]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">20</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">paging[pageSize]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="paging[current]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">paging[current]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="sorting[field]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">entity_id</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">sorting[field]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="sorting[direction]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">asc</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">sorting[direction]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="isAjax" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">isAjax</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/mui/index/render/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_browse_products_grid/get_product_pages_count.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <com.atlantbh.jmeter.plugins.jsonutils.jsonpathassertion.JSONPathAssertion guiclass="com.atlantbh.jmeter.plugins.jsonutils.jsonpathassertion.gui.JSONPathAssertionGui" testclass="com.atlantbh.jmeter.plugins.jsonutils.jsonpathassertion.JSONPathAssertion" testname="Assert total records is not 0" enabled="true"> - <stringProp name="JSON_PATH">$.totalRecords</stringProp> - <stringProp name="EXPECTED_VALUE">0</stringProp> - <boolProp name="JSONVALIDATION">true</boolProp> - <boolProp name="EXPECT_NULL">false</boolProp> - <boolProp name="INVERT">true</boolProp> - </com.atlantbh.jmeter.plugins.jsonutils.jsonpathassertion.JSONPathAssertion> - <hashTree/> - <com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.JSONPathExtractor guiclass="com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.gui.JSONPathExtractorGui" testclass="com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.JSONPathExtractor" testname="Extract total records" enabled="true"> - <stringProp name="VAR">products_number</stringProp> - <stringProp name="JSONPATH">$.totalRecords</stringProp> - <stringProp name="DEFAULT"/> - <stringProp name="VARIABLE"/> - <stringProp name="SUBJECT">BODY</stringProp> - </com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.JSONPathExtractor> - <hashTree/> - <BeanShellPostProcessor guiclass="TestBeanGUI" testclass="BeanShellPostProcessor" testname="Calculate pages count" enabled="true"> - <boolProp name="resetInterpreter">false</boolProp> - <stringProp name="parameters"/> - <stringProp name="filename"/> - <stringProp name="script">var productsPageSize = Integer.parseInt(vars.get("products_page_size")); -var productsTotal = Integer.parseInt(vars.get("products_number")); -var pageCountProducts = Math.round(productsTotal/productsPageSize); - -vars.put("pages_count_product", String.valueOf(pageCountProducts));</stringProp> - </BeanShellPostProcessor> - <hashTree/> - </hashTree> - - <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Set Arguments" enabled="true"> - <stringProp name="BeanShellSampler.query"> -import java.util.Random; -Random random = new Random(); -if (${seedForRandom} > 0) { -random.setSeed(${seedForRandom}); -} -var productsPageSize = Integer.parseInt(vars.get("products_page_size")); -var totalNumberOfPages = Integer.parseInt(vars.get("pages_count_product")); - -// Randomly select a page. -var randomProductsPage = random.nextInt(totalNumberOfPages) + 1; - -// Get the first and last product id on that page. -var lastProductIdOnPage = randomProductsPage * productsPageSize; -var firstProductIdOnPage = lastProductIdOnPage - productsPageSize + 1; - -var randomProductId1 = Math.floor(random.nextInt(productsPageSize)) + firstProductIdOnPage; -var randomProductId2 = Math.floor(random.nextInt(productsPageSize)) + firstProductIdOnPage; -var randomProductId3 = Math.floor(random.nextInt(productsPageSize)) + firstProductIdOnPage; - -vars.put("page_number", String.valueOf(randomProductsPage)); -vars.put("productId1", String.valueOf(randomProductId1)); -vars.put("productId2", String.valueOf(randomProductId2)); -vars.put("productId3", String.valueOf(randomProductId3)); - -var randomQuantity = random.nextInt(1000) + 1; -var randomPrice = random.nextInt(500) + 10; -var randomVisibility = random.nextInt(4) + 1; - -vars.put("quantity", String.valueOf(randomQuantity)); -vars.put("price", String.valueOf(randomPrice)); -vars.put("visibility", String.valueOf(randomVisibility)); - </stringProp> - <stringProp name="BeanShellSampler.filename"/> - <stringProp name="BeanShellSampler.parameters"/> - <boolProp name="BeanShellSampler.resetInterpreter">false</boolProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/setup.jmx</stringProp></BeanShellSampler> - <hashTree/> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Select Products and Update Attributes mass action" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="namespace" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">product_listing</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">namespace</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="search" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value"/> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">search</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="filters[placeholder]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">filters[placeholder]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="paging[pageSize]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${products_page_size}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">paging[pageSize]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="paging[current]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${page_number}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">paging[current]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="sorting[field]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">entity_id</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">sorting[field]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="sorting[direction]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">asc</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">sorting[direction]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="isAjax" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">isAjax</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/mui/index/render/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/display_grid.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="1637639774">totalRecords</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Display Update Attributes" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="selected[0]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${productId1}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">selected[0]</stringProp> - </elementProp> - <elementProp name="selected[1]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${productId2}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">selected[1]</stringProp> - </elementProp> - <elementProp name="selected[2]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${productId3}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">selected[2]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="filters[placeholder]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">filters[placeholder]</stringProp> - <stringProp name="Argument.desc">false</stringProp> - </elementProp> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - <stringProp name="Argument.desc">false</stringProp> - </elementProp> - <elementProp name="namespace" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">product_listing</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">namespace</stringProp> - <stringProp name="Argument.desc">false</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/catalog/product_action_attribute/edit</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/display_update_attributes.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="1862384910">Update Attributes</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Validate Attributes" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="isAjax" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">isAjax</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="product[product_has_weight]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">product[product_has_weight]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="product[use_config_gift_message_available]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">product[use_config_gift_message_available]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="product[use_config_gift_wrapping_available]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">product[use_config_gift_wrapping_available]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="inventory[qty]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${quantity}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">inventory[qty]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="attributes[price]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${price}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">attributes[price]</stringProp> - </elementProp> - <elementProp name="attributes[visibility]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${visibility}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">attributes[visibility]</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/catalog/product_action_attribute/validate</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/change_attributes.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="1853918323">{"error":false}</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Save Attributes" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"> - <elementProp name="isAjax" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">true</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">isAjax</stringProp> - <stringProp name="Argument.desc">false</stringProp> - </elementProp> - <elementProp name="form_key" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${admin_form_key}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">form_key</stringProp> - <stringProp name="Argument.desc">false</stringProp> - </elementProp> - <elementProp name="product[product_has_weight]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">product[product_has_weight]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="product[use_config_gift_message_available]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">product[use_config_gift_message_available]</stringProp> - </elementProp> - <elementProp name="product[use_config_gift_wrapping_available]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">1</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">product[use_config_gift_wrapping_available]</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="inventory[qty]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${quantity}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">inventory[qty]</stringProp> - </elementProp> - <elementProp name="toggle_qty" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">on</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">toggle_price</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="attributes[price]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${price}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">attributes[price]</stringProp> - </elementProp> - <elementProp name="toggle_price" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">on</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">toggle_price</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - <elementProp name="attributes[visibility]" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">${visibility}</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">attributes[visibility]</stringProp> - </elementProp> - <elementProp name="toggle_visibility" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">true</boolProp> - <stringProp name="Argument.value">on</stringProp> - <stringProp name="Argument.metadata">=</stringProp> - <boolProp name="HTTPArgument.use_equals">true</boolProp> - <stringProp name="Argument.name">toggle_visibility</stringProp> - <stringProp name="Argument.desc">true</stringProp> - </elementProp> - </collectionProp> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/catalog/product_action_attribute/save/store/0/active_tab/attributes</stringProp> - <stringProp name="HTTPSampler.method">POST</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">true</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - </HTTPSamplerProxy> - <hashTree> - <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true"> - <collectionProp name="Asserion.test_strings"> - <stringProp name="1848809106">were updated.</stringProp> - </collectionProp> - <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> - <boolProp name="Assertion.assume_success">false</boolProp> - <intProp name="Assertion.test_type">2</intProp> - </ResponseAssertion> - <hashTree/> - </hashTree> -</hashTree> - - <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true"> - <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> - <collectionProp name="Arguments.arguments"/> - </elementProp> - <stringProp name="HTTPSampler.domain"/> - <stringProp name="HTTPSampler.port"/> - <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> - <stringProp name="HTTPSampler.response_timeout">200000</stringProp> - <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> - <stringProp name="HTTPSampler.contentEncoding"/> - <stringProp name="HTTPSampler.path">${base_path}${admin_path}/admin/auth/logout/</stringProp> - <stringProp name="HTTPSampler.method">GET</stringProp> - <boolProp name="HTTPSampler.follow_redirects">true</boolProp> - <boolProp name="HTTPSampler.auto_redirects">false</boolProp> - <boolProp name="HTTPSampler.use_keepalive">true</boolProp> - <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> - <boolProp name="HTTPSampler.monitor">false</boolProp> - <stringProp name="HTTPSampler.embedded_url_re"/> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/setup/admin_logout.jmx</stringProp></HTTPSamplerProxy> - <hashTree> - - <BeanShellPostProcessor guiclass="TestBeanGUI" testclass="BeanShellPostProcessor" testname="Return Admin to Pool" enabled="true"> - <boolProp name="resetInterpreter">false</boolProp> - <stringProp name="parameters"/> - <stringProp name="filename"/> - <stringProp name="script"> - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - </stringProp> - <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx</stringProp></BeanShellPostProcessor> - <hashTree/> - </hashTree> - </hashTree> - - <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="Import Products" enabled="true"> <intProp name="ThroughputController.style">1</intProp> <boolProp name="ThroughputController.perThread">false</boolProp> diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php index afe1a5d9e2591..3f2aedae1373c 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php @@ -50,7 +50,8 @@ class ConfigOptionsList implements ConfigOptionsListInterface private $configOptionsListClasses = [ \Magento\Setup\Model\ConfigOptionsList\Session::class, \Magento\Setup\Model\ConfigOptionsList\Cache::class, - \Magento\Setup\Model\ConfigOptionsList\PageCache::class + \Magento\Setup\Model\ConfigOptionsList\PageCache::class, + \Magento\Setup\Model\ConfigOptionsList\Lock::class, ]; /** diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php new file mode 100644 index 0000000000000..66f41128c46b1 --- /dev/null +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php @@ -0,0 +1,342 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Setup\Model\ConfigOptionsList; + +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\SelectConfigOption; +use Magento\Framework\Setup\Option\TextConfigOption; + +/** + * Deployment configuration options for locks + */ +class Lock implements ConfigOptionsListInterface +{ + /** + * The name of an option to set lock provider + * + * @const string + */ + const INPUT_KEY_LOCK_PROVIDER = 'lock-provider'; + + /** + * The name of an option to set DB prefix + * + * @const string + */ + const INPUT_KEY_LOCK_DB_PREFIX = 'lock-db-prefix'; + + /** + * The name of an option to set Zookeeper host + * + * @const string + */ + const INPUT_KEY_LOCK_ZOOKEEPER_HOST = 'lock-zookeeper-host'; + + /** + * The name of an option to set Zookeeper path + * + * @const string + */ + const INPUT_KEY_LOCK_ZOOKEEPER_PATH = 'lock-zookeeper-path'; + + /** + * The name of an option to set File path + * + * @const string + */ + const INPUT_KEY_LOCK_FILE_PATH = 'lock-file-path'; + + /** + * The configuration path to save lock provider + * + * @const string + */ + const CONFIG_PATH_LOCK_PROVIDER = 'lock/provider'; + + /** + * The configuration path to save DB prefix + * + * @const string + */ + const CONFIG_PATH_LOCK_DB_PREFIX = 'lock/config/prefix'; + + /** + * The configuration path to save Zookeeper host + * + * @const string + */ + const CONFIG_PATH_LOCK_ZOOKEEPER_HOST = 'lock/config/host'; + + /** + * The configuration path to save Zookeeper path + * + * @const string + */ + const CONFIG_PATH_LOCK_ZOOKEEPER_PATH = 'lock/config/path'; + + /** + * The configuration path to save locks directory path + * + * @const string + */ + const CONFIG_PATH_LOCK_FILE_PATH = 'lock/config/path'; + + /** + * The list of lock providers + * + * @var array + */ + private $validLockProviders = [ + LockBackendFactory::LOCK_DB, + LockBackendFactory::LOCK_ZOOKEEPER, + LockBackendFactory::LOCK_CACHE, + LockBackendFactory::LOCK_FILE, + ]; + + /** + * The mapping input keys with their configuration paths + * + * @var array + */ + private $mappingInputKeyToConfigPath = [ + LockBackendFactory::LOCK_DB => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_DB_PREFIX => self::CONFIG_PATH_LOCK_DB_PREFIX, + ], + LockBackendFactory::LOCK_ZOOKEEPER => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_ZOOKEEPER_HOST => self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + ], + LockBackendFactory::LOCK_CACHE => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + ], + LockBackendFactory::LOCK_FILE => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_FILE_PATH => self::CONFIG_PATH_LOCK_FILE_PATH, + ], + ]; + + /** + * The list of default values + * + * @var array + */ + private $defaultConfigValues = [ + self::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, + self::INPUT_KEY_LOCK_DB_PREFIX => null, + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => ZookeeperLock::DEFAULT_PATH, + ]; + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_LOCK_PROVIDER, + SelectConfigOption::FRONTEND_WIZARD_SELECT, + $this->validLockProviders, + self::CONFIG_PATH_LOCK_PROVIDER, + 'Lock provider name', + LockBackendFactory::LOCK_DB + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_DB_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_DB_PREFIX, + 'Installation specific lock prefix to avoid lock conflicts' + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_ZOOKEEPER_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + 'Host and port to connect to Zookeeper cluster. For example: 127.0.0.1:2181' + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + 'The path where Zookeeper will save locks. The default path is: ' . ZookeeperLock::DEFAULT_PATH + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_FILE_PATH, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_FILE_PATH, + 'The path where file locks will be saved.' + ), + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $lockProvider = $this->getLockProvider($options, $deploymentConfig); + + $this->setDefaultConfiguration($configData, $deploymentConfig, $lockProvider); + + foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { + if (isset($options[$input])) { + $configData->set($path, $options[$input]); + } + } + + return $configData; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + $lockProvider = $this->getLockProvider($options, $deploymentConfig); + switch ($lockProvider) { + case LockBackendFactory::LOCK_ZOOKEEPER: + $errors = $this->validateZookeeperConfig($options, $deploymentConfig); + break; + case LockBackendFactory::LOCK_FILE: + $errors = $this->validateFileConfig($options, $deploymentConfig); + break; + case LockBackendFactory::LOCK_CACHE: + case LockBackendFactory::LOCK_DB: + $errors = []; + break; + default: + $errors[] = 'The lock provider ' . $lockProvider . ' does not exist.'; + } + + return $errors; + } + + /** + * Validates File locks configuration + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array + */ + private function validateFileConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $errors = []; + + $path = $options[self::INPUT_KEY_LOCK_FILE_PATH] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_FILE_PATH, + $this->getDefaultValue(self::INPUT_KEY_LOCK_FILE_PATH) + ); + + if (!$path) { + $errors[] = 'The path needs to be a non-empty string.'; + } + + return $errors; + } + + /** + * Validates Zookeeper configuration + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array + */ + private function validateZookeeperConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $errors = []; + + if (!extension_loaded(LockBackendFactory::LOCK_ZOOKEEPER)) { + $errors[] = 'php extension Zookeeper is not installed.'; + } + + $host = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_HOST] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_HOST) + ); + $path = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_PATH] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_PATH) + ); + + if (!$path) { + $errors[] = 'Zookeeper path needs to be a non-empty string.'; + } + + if (!$host) { + $errors[] = 'Zookeeper host is should be set.'; + } + + return $errors; + } + + /** + * Returns the name of lock provider + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return string + */ + private function getLockProvider(array $options, DeploymentConfig $deploymentConfig): string + { + if (!isset($options[self::INPUT_KEY_LOCK_PROVIDER])) { + return (string) $deploymentConfig->get( + self::CONFIG_PATH_LOCK_PROVIDER, + $this->getDefaultValue(self::INPUT_KEY_LOCK_PROVIDER) + ); + } + + return (string) $options[self::INPUT_KEY_LOCK_PROVIDER]; + } + + /** + * Sets default configuration for locks + * + * @param ConfigData $configData + * @param DeploymentConfig $deploymentConfig + * @param string $lockProvider + * @return ConfigData + */ + private function setDefaultConfiguration( + ConfigData $configData, + DeploymentConfig $deploymentConfig, + string $lockProvider + ) { + foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { + $configData->set($path, $deploymentConfig->get($path, $this->getDefaultValue($input))); + } + + return $configData; + } + + /** + * Returns default value by input key + * + * If default value is not set returns null + * + * @param string $inputKey + * @return mixed|null + */ + private function getDefaultValue(string $inputKey) + { + if (isset($this->defaultConfigValues[$inputKey])) { + return $this->defaultConfigValues[$inputKey]; + } else { + return null; + } + } +} diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php new file mode 100644 index 0000000000000..1a46bddf5f21a --- /dev/null +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php @@ -0,0 +1,232 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Setup\Test\Unit\Model\ConfigOptionsList; + +use Magento\Setup\Model\ConfigOptionsList\Lock as LockConfigOptionsList; +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Setup\Option\SelectConfigOption; +use Magento\Framework\Setup\Option\TextConfigOption; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LockTest extends TestCase +{ + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfigMock; + + /** + * @var LockConfigOptionsList + */ + private $lockConfigOptionsList; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->lockConfigOptionsList = new LockConfigOptionsList(); + } + + /** + * @return void + */ + public function testGetOptions() + { + $options = $this->lockConfigOptionsList->getOptions(); + $this->assertSame(5, count($options)); + + $this->assertArrayHasKey(0, $options); + $this->assertInstanceOf(SelectConfigOption::class, $options[0]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER, $options[0]->getName()); + + $this->assertArrayHasKey(1, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[1]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_DB_PREFIX, $options[1]->getName()); + + $this->assertArrayHasKey(2, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[2]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST, $options[2]->getName()); + + $this->assertArrayHasKey(3, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[3]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH, $options[3]->getName()); + + $this->assertArrayHasKey(4, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[4]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH, $options[4]->getName()); + } + + /** + * @param array $options + * @param array $expectedResult + * @dataProvider createConfigDataProvider + */ + public function testCreateConfig(array $options, array $expectedResult) + { + $this->deploymentConfigMock->expects($this->any()) + ->method('get') + ->willReturnArgument(1); + $data = $this->lockConfigOptionsList->createConfig($options, $this->deploymentConfigMock); + $this->assertInstanceOf(ConfigData::class, $data); + $this->assertTrue($data->isOverrideWhenSave()); + $this->assertSame($expectedResult, $data->getData()); + } + + /** + * @return array + */ + public function createConfigDataProvider(): array + { + return [ + 'Check default values' => [ + 'options' => [], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_DB, + 'config' => [ + 'prefix' => null, + ], + ], + ], + ], + 'Check default value for cache lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_CACHE, + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_CACHE, + ], + ], + ], + 'Check default value for zookeeper lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'config' => [ + 'host' => null, + 'path' => ZookeeperLock::DEFAULT_PATH, + ], + ], + ], + ], + 'Check specific db lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, + LockConfigOptionsList::INPUT_KEY_LOCK_DB_PREFIX => 'my_prefix' + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_DB, + 'config' => [ + 'prefix' => 'my_prefix', + ], + ], + ], + ], + 'Check specific zookeeper lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST => '123.45.67.89:10', + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH => '/some/path', + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'config' => [ + 'host' => '123.45.67.89:10', + 'path' => '/some/path', + ], + ], + ], + ], + 'Check specific file lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_FILE, + LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH => '/my/path' + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_FILE, + 'config' => [ + 'path' => '/my/path', + ], + ], + ], + ], + ]; + } + + /** + * @param array $options + * @param array $expectedResult + * @dataProvider validateDataProvider + */ + public function testValidate(array $options, array $expectedResult) + { + $this->deploymentConfigMock->expects($this->any()) + ->method('get') + ->willReturnArgument(1); + $this->assertSame( + $expectedResult, + $this->lockConfigOptionsList->validate($options, $this->deploymentConfigMock) + ); + } + + /** + * @return array + */ + public function validateDataProvider(): array + { + return [ + 'Wrong lock provider' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => 'SomeProvider', + ], + 'expectedResult' => [ + 'The lock provider SomeProvider does not exist.', + ], + ], + 'Empty host and path for Zookeeper' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST => '', + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH => '', + ], + 'expectedResult' => extension_loaded('zookeeper') + ? [ + 'Zookeeper path needs to be a non-empty string.', + 'Zookeeper host is should be set.', + ] + : [ + 'php extension Zookeeper is not installed.', + 'Zookeeper path needs to be a non-empty string.', + 'Zookeeper host is should be set.', + ], + ], + 'Empty path for File lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_FILE, + LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH => '', + ], + 'expectedResult' => [ + 'The path needs to be a non-empty string.', + ], + ], + ]; + } +} diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php index d7f680309c9ef..a85b468cebc92 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php @@ -7,6 +7,7 @@ namespace Magento\Setup\Test\Unit\Model; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Setup\Model\ConfigOptionsList\Lock; use Magento\Setup\Model\ConfigGenerator; use Magento\Setup\Model\ConfigOptionsList; use Magento\Setup\Validator\DbValidator; @@ -82,7 +83,7 @@ public function testCreateOptions() $this->generator->expects($this->once())->method('createXFrameConfig')->willReturn($configDataMock); $this->generator->expects($this->once())->method('createCacheHostsConfig')->willReturn($configDataMock); - $configData = $this->object->createConfig([], $this->deploymentConfig); + $configData = $this->object->createConfig([Lock::INPUT_KEY_LOCK_PROVIDER => 'db'], $this->deploymentConfig); $this->assertGreaterThanOrEqual(6, count($configData)); } @@ -96,7 +97,7 @@ public function testCreateOptionsWithOptionalNull() $this->generator->expects($this->once())->method('createXFrameConfig')->willReturn($configDataMock); $this->generator->expects($this->once())->method('createCacheHostsConfig')->willReturn($configDataMock); - $configData = $this->object->createConfig([], $this->deploymentConfig); + $configData = $this->object->createConfig([Lock::INPUT_KEY_LOCK_PROVIDER => 'db'], $this->deploymentConfig); $this->assertGreaterThanOrEqual(6, count($configData)); } @@ -109,7 +110,8 @@ public function testValidateSuccess() ConfigOptionsListConstants::INPUT_KEY_DB_NAME => 'name', ConfigOptionsListConstants::INPUT_KEY_DB_HOST => 'host', ConfigOptionsListConstants::INPUT_KEY_DB_USER => 'user', - ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass' + ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->prepareValidationMocks(); @@ -127,7 +129,8 @@ public function testValidateInvalidSessionHandler() ConfigOptionsListConstants::INPUT_KEY_DB_NAME => 'name', ConfigOptionsListConstants::INPUT_KEY_DB_HOST => 'host', ConfigOptionsListConstants::INPUT_KEY_DB_USER => 'user', - ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass' + ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->prepareValidationMocks(); @@ -141,7 +144,8 @@ public function testValidateEmptyEncryptionKey() { $options = [ ConfigOptionsListConstants::INPUT_KEY_SKIP_DB_VALIDATION => true, - ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '' + ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->assertEquals( ['Invalid encryption key. Encryption key must be 32 character string without any white space.'], @@ -167,7 +171,8 @@ public function testValidateCacheHosts($hosts, $expectedError) { $options = [ ConfigOptionsListConstants::INPUT_KEY_SKIP_DB_VALIDATION => true, - ConfigOptionsListConstants::INPUT_KEY_CACHE_HOSTS => $hosts + ConfigOptionsListConstants::INPUT_KEY_CACHE_HOSTS => $hosts, + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $result = $this->object->validate($options, $this->deploymentConfig); if ($expectedError) {